diff --git a/document_services.py b/document_services.py
index de073ad4..39b50531 100644
--- a/document_services.py
+++ b/document_services.py
@@ -202,30 +202,71 @@ def resolve(self) -> PHPExpression:
def __str__(self) -> str:
return f"{self.classname}::{self.funcname}()"
-class PHPEnumFormat(PHPClassMemberFunction, PHPString):
- def resolve(self) -> PHPString:
+class PHPEnum():
+ @classmethod
+ def getcases(cls, classname: str) -> dict[str, str]:
# https://regex101.com/r/p5FzCh
casepattern = r"const (\w+) = (\d+|true|false|(['\"]).*?\3)"
- fullbody_pattern = f"class {self.classname} extends Enum {{.*?}}"
+ fullbody_pattern = f"class {classname} extends (Enum|\\w+) {{(.*?)}}"
+
+ cases = {}
- fp = f"lbplanner/classes/enums/{self.classname}.php"
+ fp = f"lbplanner/classes/enums/{classname}.php"
if not path.exists(fp):
warn(f"Couldn't find enum file {fp}")
- return PHPStringLiteral("")
+ return {}
with open(fp, "r") as f:
- matches: list[str] = re.findall(fullbody_pattern, f.read(), re.DOTALL)
+ matches: list[list[str]] = re.findall(fullbody_pattern, f.read(), re.DOTALL)
if len(matches) == 1:
- body = matches[0]
+ if matches[0][0] != 'Enum':
+ cases = cls.getcases(matches[0][0])
+ body = matches[0][1]
else:
- warn(f"couldn't parse enum {self.classname}", matches)
+ warn(f"couldn't parse enum {classname}", matches)
- cases = {}
- matches = re.findall(casepattern, body)
- for match in matches:
- # capitalizing first letter, if exists
- name = "".join([match[0][0].upper(), match[0][1:].lower()])
- cases[name] = match[1].replace("'", '"')
+ matches2: list[str] = re.findall(casepattern, body)
+ for match in matches2:
+ val = match[1].replace("'", '"')
+ cases[match[0]] = val
+
+ return cases
+
+class PHPEnumCase(PHPEnum, PHPString):
+ __slots__ = ('classname', 'casename', 'fp')
+ classname: str
+ casename: str
+ fp: str
+
+ def __init__(self, classname: str, casename: str, fp: str):
+ self.classname = classname
+ self.casename = casename
+ self.fp = fp
+
+ def resolve(self) -> PHPString:
+ cases = self.getcases(self.classname)
+ if self.casename not in cases.keys():
+ warn(f"enum member {self.classname}::{self.casename} not found", cases)
+ return PHPStringLiteral("?")
+
+ val = cases[self.casename]
+
+ if val.startswith('"') and val.endswith('"'):
+ val = val[1:-1]
+
+ return PHPStringLiteral(val)
+
+ def get_value(self) -> str:
+ return self.resolve().get_value()
+
+ def __str__(self) -> str:
+ return f"{self.classname}::{self.casename}"
+
+class PHPEnumFormat(PHPEnum, PHPClassMemberFunction, PHPString):
+ def resolve(self) -> PHPString:
+ cases = self.getcases(self.classname)
+ # capitalizing first letter of each key
+ cases = {"".join([name[0].upper(), name[1:].lower()]): case for name, case in cases.items()}
return PHPStringLiteral("{ " + ", ".join([f"{name} = {value}" for name, value in cases.items()]) + " }")
@@ -506,22 +547,30 @@ def parse_expression(code: str, nr: PHPNameResolution) -> tuple[int, PHPExpressi
assert len(buf) > 0
assert expr is None
i += 2
- iplus = code[i:].index('(')
- funcname = code[i:i + iplus]
+ iplus = 1
+ while 95 <= ord(code[i + iplus].lower()) <= 122: # until it hits non-word character
+ iplus += 1
+ is_func = code[i + iplus] == '('
+ membername = code[i:i + iplus]
classname = "".join(buf)
i += iplus
- assert code[i:i + 2] == '()'
- i += 2
- C: type[PHPClassMemberFunction]
- fp_import: str | None
- if funcname == 'format':
- C = PHPEnumFormat
- fp_import = path.join(path.dirname(__file__), "lbplanner", "enums", f"{classname}.php")
+ if is_func:
+ assert code[i:i + 2] == '()'
+ i += 2
+ C: type[PHPClassMemberFunction]
+ fp_import: str | None
+ if membername == 'format':
+ C = PHPEnumFormat
+ fp_import = path.join(path.dirname(__file__), "lbplanner", "enums", f"{classname}.php")
+ else:
+ C = PHPClassMemberFunction
+ fp_import = find_import(nr, classname)
+ expr = C(classname, membername, fp_import).resolve()
+ buf = []
else:
- C = PHPClassMemberFunction
- fp_import = find_import(nr, classname)
- expr = C(classname, funcname, fp_import).resolve()
- buf = []
+ fp_import = path.join(path.dirname(__file__), "lbplanner", "enums", f"{classname}.php")
+ expr = PHPEnumCase(classname, membername, fp_import).resolve()
+ buf = []
else:
# unkown character? simply bail
if len(buf) > 0:
diff --git a/lbplanner/classes/enums/KANBANCOL_TYPE.php b/lbplanner/classes/enums/KANBANCOL_TYPE.php
new file mode 100644
index 00000000..886db554
--- /dev/null
+++ b/lbplanner/classes/enums/KANBANCOL_TYPE.php
@@ -0,0 +1,63 @@
+.
+
+/**
+ * enum for columns on the kanban board
+ *
+ * @package local_lbplanner
+ * @subpackage enums
+ * @copyright 2025 NecodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+
+namespace local_lbplanner\enums;
+
+// TODO: revert to native enums once we migrate to php8.
+
+use local_lbplanner\polyfill\Enum;
+use local_lbplanner\enums\KANBANCOL_TYPE_NUMERIC;
+
+/**
+ * The types of columns in the kanban board
+ */
+class KANBANCOL_TYPE extends Enum {
+ /**
+ * column "backlog"
+ */
+ const BACKLOG = 'backlog';
+ /**
+ * column "in progress"
+ */
+ const INPROGRESS = 'inprogress';
+ /**
+ * column "todo"
+ */
+ const TODO = 'todo';
+ /**
+ * column "done"
+ */
+ const DONE = 'done';
+
+ /**
+ * Converts from column name to column ID
+ * @param string $str the column name
+ * @return int the column ID
+ * @link KANBANCOL_TYPE_NUMERIC
+ */
+ public static function to_numeric(string $str): int {
+ return KANBANCOL_TYPE_NUMERIC::get(self::name_from($str));
+ }
+}
diff --git a/lbplanner/classes/enums/KANBANCOL_TYPE_NUMERIC.php b/lbplanner/classes/enums/KANBANCOL_TYPE_NUMERIC.php
new file mode 100644
index 00000000..5b0b7133
--- /dev/null
+++ b/lbplanner/classes/enums/KANBANCOL_TYPE_NUMERIC.php
@@ -0,0 +1,63 @@
+.
+
+/**
+ * enum for columns on the kanban board
+ *
+ * @package local_lbplanner
+ * @subpackage enums
+ * @copyright 2025 NecodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+
+namespace local_lbplanner\enums;
+
+// TODO: revert to native enums once we migrate to php8.
+
+use local_lbplanner\polyfill\Enum;
+use local_lbplanner\enums\KANBANCOL_TYPE;
+
+/**
+ * The types of columns in the kanban board
+ */
+class KANBANCOL_TYPE_NUMERIC extends Enum {
+ /**
+ * column "backlog"
+ */
+ const BACKLOG = 0;
+ /**
+ * column "todo"
+ */
+ const TODO = 1;
+ /**
+ * column "in progress"
+ */
+ const INPROGRESS = 2;
+ /**
+ * column "done"
+ */
+ const DONE = 3;
+
+ /**
+ * converts numeric repr to named repr
+ * @param int $num the column number
+ * @return string the column name
+ * @link KANBANCOL_TYPE
+ */
+ public static function to_named(int $num): string {
+ return KANBANCOL_TYPE::get(self::name_from($num));
+ }
+}
diff --git a/lbplanner/classes/enums/KANBANCOL_TYPE_ORNONE.php b/lbplanner/classes/enums/KANBANCOL_TYPE_ORNONE.php
new file mode 100644
index 00000000..e54f4832
--- /dev/null
+++ b/lbplanner/classes/enums/KANBANCOL_TYPE_ORNONE.php
@@ -0,0 +1,39 @@
+.
+
+/**
+ * enum for columns on the kanban board
+ *
+ * @package local_lbplanner
+ * @subpackage enums
+ * @copyright 2025 NecodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+
+namespace local_lbplanner\enums;
+use local_lbplanner\enums\KANBANCOL_TYPE;
+
+// TODO: revert to native enums once we migrate to php8.
+
+/**
+ * The types of columns in the kanban board
+ */
+class KANBANCOL_TYPE_ORNONE extends KANBANCOL_TYPE {
+ /**
+ * nonexistant column - for user setting "don't move to any column"
+ */
+ const NONE = '';
+}
diff --git a/lbplanner/classes/helpers/kanban_helper.php b/lbplanner/classes/helpers/kanban_helper.php
new file mode 100644
index 00000000..3999067c
--- /dev/null
+++ b/lbplanner/classes/helpers/kanban_helper.php
@@ -0,0 +1,83 @@
+.
+
+namespace local_lbplanner\helpers;
+
+use local_lbplanner\enums\KANBANCOL_TYPE_NUMERIC;
+use local_lbplanner\model\kanbanentry;
+
+/**
+ * Helper class for the kanban board
+ *
+ * @package local_lbplanner
+ * @subpackage helpers
+ * @copyright 2025 NecodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+class kanban_helper {
+
+ /**
+ * Table for storing kanban board entries.
+ */
+ const TABLE = 'local_lbplanner_kanbanentries';
+
+ /**
+ * Gets all kanban entries for a user.
+ * @param int $userid ID of the user to look entries up for
+ * @return kanbanentry[] all the kanban entries for this user
+ */
+ public static function get_all_entries_by_user(int $userid): array {
+ global $DB;
+
+ $res = $DB->get_records(self::TABLE, ['userid' => $userid]);
+ $entries = [];
+ foreach ($res as $obj) {
+ array_push($entries, kanbanentry::from_obj($obj));
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Gets specific kanban entry.
+ * @param int $userid ID of the user to look entries up for
+ * @param int $cmid ID of the content-module
+ * @return kanbanentry the kanban entry matching this selection or null if not found
+ */
+ public static function get_entry(int $userid, int $cmid): ?kanbanentry {
+ global $DB;
+
+ $res = $DB->get_record(self::TABLE, ['userid' => $userid, 'cmid' => $cmid]);
+
+ return $res !== false ? kanbanentry::from_obj($res) : null;
+ }
+
+ /**
+ * Sets specific kanban entry.
+ * @param kanbanentry $entry the entry to set
+ */
+ public static function set_entry(kanbanentry $entry): void {
+ global $DB, $CFG;
+
+ $DB->delete_records(self::TABLE, ['userid' => $entry->userid, 'cmid' => $entry->cmid]);
+ if ($entry->column !== KANBANCOL_TYPE_NUMERIC::BACKLOG) {
+ $table = $CFG->prefix . self::TABLE;
+ // Moodle is too stupid to compensate for 'column' being a keyword so I need to shit my own ass manually.
+ $newid = $DB->execute("INSERT INTO {$table} VALUES (null,?,?,?)", [$entry->userid, $entry->cmid, $entry->column]);
+ $entry->set_fresh($newid);
+ }
+ }
+}
diff --git a/lbplanner/classes/helpers/user_helper.php b/lbplanner/classes/helpers/user_helper.php
index 23f7c2b4..ca10e824 100644
--- a/lbplanner/classes/helpers/user_helper.php
+++ b/lbplanner/classes/helpers/user_helper.php
@@ -76,7 +76,7 @@ public static function get_user(int $userid): user {
}
// Register user if not found.
- $eduplanneruser = new user(0, $userid, 'default', 'none', 1, false);
+ $eduplanneruser = new user(0, $userid, 'default', 'none', 1, false, true, null, null, null);
$epid = $DB->insert_record(self::EDUPLANNER_USER_TABLE, $eduplanneruser->prepare_for_db());
$eduplanneruser->set_fresh($epid);
diff --git a/lbplanner/classes/model/kanbanentry.php b/lbplanner/classes/model/kanbanentry.php
new file mode 100644
index 00000000..f6f270a7
--- /dev/null
+++ b/lbplanner/classes/model/kanbanentry.php
@@ -0,0 +1,115 @@
+.
+
+/**
+ * Model for a kanban entry
+ *
+ * @package local_lbplanner
+ * @subpackage model
+ * @copyright 2025 NecodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+
+namespace local_lbplanner\model;
+
+
+use core_external\{external_single_structure, external_value};
+use local_lbplanner\enums\{KANBANCOL_TYPE, KANBANCOL_TYPE_NUMERIC};
+
+/**
+ * Model class for a kanban board entry
+ */
+class kanbanentry {
+ /**
+ * @var int $id ID of kbbe
+ */
+ public int $id;
+ /**
+ * @var int $userid ID of user
+ */
+ public int $userid;
+ /**
+ * @var int $cmid ID of course-module
+ */
+ public int $cmid;
+ /**
+ * @var int $column column number
+ */
+ public int $column;
+
+ /**
+ * Constructs a kbbe
+ * @param int $id ID of kbbe
+ * @param int $userid ID of user
+ * @param int $cmid ID of course-module
+ * @param int $column column number
+ */
+ public function __construct(int $id, int $userid, int $cmid, int $column) {
+ $this->id = $id;
+ $this->userid = $userid;
+ $this->cmid = $cmid;
+ $this->column = $column;
+ }
+
+ /**
+ * Initializes object from a DB object
+ * @param \stdClass $obj the DB obj
+ * @return kanbanentry the kanbanentry obj
+ */
+ public static function from_obj(\stdClass $obj): self {
+ return new self($obj->id, $obj->userid, $obj->cmid, $obj->column);
+ }
+
+ /**
+ * Mark the object as freshly created and sets the new ID
+ * @param int $id the new ID after insertint into the DB
+ */
+ public function set_fresh(int $id) {
+ assert($this->id === 0);
+ assert($id !== 0);
+ $this->id = $id;
+ }
+
+ /**
+ * Prepares data for the API endpoint.
+ *
+ * @return array a representation of this kbbe and its data
+ */
+ public function prepare_for_api(): array {
+ return [
+ 'id' => $this->id,
+ 'userid' => $this->userid,
+ 'cmid' => $this->cmid,
+ 'column' => KANBANCOL_TYPE_NUMERIC::to_named($this->column),
+ ];
+ }
+
+ /**
+ * Returns the data structure of an kanban board entry.
+ *
+ * @return external_single_structure The data structure of a kbbe.
+ */
+ public static function api_structure(): external_single_structure {
+ return new external_single_structure(
+ [
+ 'id' => new external_value(PARAM_INT, 'kanban board ID'),
+ 'userid' => new external_value(PARAM_INT, 'ID of the owner of this entry'),
+ 'cmid' => new external_value(PARAM_INT, 'ID of the course-module'),
+ 'column' => new external_value(PARAM_TEXT, 'which column this module is in '.KANBANCOL_TYPE::format()),
+ ]
+ );
+ }
+}
diff --git a/lbplanner/classes/model/module.php b/lbplanner/classes/model/module.php
index 6e7c77ec..98dd6a18 100644
--- a/lbplanner/classes/model/module.php
+++ b/lbplanner/classes/model/module.php
@@ -106,6 +106,17 @@ public static function from_assignobj(\stdClass $assignobj): self {
return $obj;
}
+ /**
+ * Creates a module object from the course-module ID.
+ * @param int $id the course-module ID
+ * @return module a module object
+ */
+ public static function from_cmid(int $id): self {
+ $obj = new self();
+ $obj->cmid = $id;
+ return $obj;
+ }
+
/**
* Fetches the necessary caches and returns the assignment ID
* @return int assign ID
diff --git a/lbplanner/classes/model/user.php b/lbplanner/classes/model/user.php
index 0621a4c7..7bedd26b 100644
--- a/lbplanner/classes/model/user.php
+++ b/lbplanner/classes/model/user.php
@@ -29,8 +29,7 @@
use core_external\{external_single_structure, external_value};
use user_picture;
-use local_lbplanner\enums\CAPABILITY;
-use local_lbplanner\enums\CAPABILITY_FLAG;
+use local_lbplanner\enums\{CAPABILITY, CAPABILITY_FLAG, KANBANCOL_TYPE, KANBANCOL_TYPE_ORNONE};
use local_lbplanner\helpers\plan_helper;
use local_lbplanner\helpers\user_helper;
@@ -68,6 +67,29 @@ class user {
*/
public bool $ekenabled;
+ /**
+ * @var bool $showcolumncolors Whether column colors should show in kanban board.
+ */
+ public bool $showcolumncolors;
+
+ /**
+ * @var ?string $automovecompletedtasks what kanban column to move completed tasks to (null → don't move)
+ * @see KANBANCOL_TYPE
+ */
+ public ?string $automovecompletedtasks;
+
+ /**
+ * @var ?string $automovesubmittedtasks what kanban column to move submitted tasks to (null → don't move)
+ * @see KANBANCOL_TYPE
+ */
+ public ?string $automovesubmittedtasks;
+
+ /**
+ * @var ?string $automoveoverduetasks what kanban column to move overdue tasks to (null → don't move)
+ * @see KANBANCOL_TYPE
+ */
+ public ?string $automoveoverduetasks;
+
/**
* @var ?\stdClass $mdluser the cached moodle user
*/
@@ -88,6 +110,20 @@ class user {
*/
private ?int $capabilitybitmask;
+ /**
+ * @var string[] DBMIRRORPROPS properties that are mirrored 1:1 between the DB and this object
+ */
+ private const DBMIRRORPROPS = [
+ 'theme',
+ 'colorblindness',
+ 'displaytaskcount',
+ 'ekenabled',
+ 'showcolumncolors',
+ 'automovecompletedtasks',
+ 'automovesubmittedtasks',
+ 'automoveoverduetasks',
+ ];
+
/**
* Constructs a new course
* @param int $lbpid ID of the Eduplanner user
@@ -96,6 +132,10 @@ class user {
* @param string $colorblindness user's colorblindness
* @param bool $displaytaskcount user's display task count
* @param bool $ekenabled whether the user wants to see EK modules
+ * @param bool $showcolumncolors whether column colors should show in kanban board
+ * @param ?string $automovecompletedtasks what kanban column to move completed tasks to (null → don't move)
+ * @param ?string $automovesubmittedtasks what kanban column to move submitted tasks to (null → don't move)
+ * @param ?string $automoveoverduetasks what kanban column to move overdue tasks to (null → don't move)
*/
public function __construct(
int $lbpid,
@@ -103,15 +143,19 @@ public function __construct(
string $theme,
string $colorblindness,
bool $displaytaskcount,
- bool $ekenabled
+ bool $ekenabled,
+ bool $showcolumncolors,
+ ?string $automovecompletedtasks,
+ ?string $automovesubmittedtasks,
+ ?string $automoveoverduetasks,
) {
global $USER;
$this->lbpid = $lbpid;
$this->mdlid = $mdlid;
- $this->set_theme($theme);
- $this->set_colorblindness($colorblindness);
- $this->displaytaskcount = $displaytaskcount;
- $this->ekenabled = $ekenabled;
+ foreach (self::DBMIRRORPROPS as $propname) {
+ $propname = str_replace('_', '', $propname);
+ $this->$propname = $$propname;
+ }
$this->planid = null;
$this->pfp = null;
$this->capabilitybitmask = null;
@@ -130,7 +174,14 @@ public function __construct(
* @return user a representation of this user and its data
*/
public static function from_db(object $obj): self {
- return new self($obj->id, $obj->userid, $obj->theme, $obj->colorblindness, $obj->displaytaskcount, $obj->ekenabled);
+ $vars = get_object_vars($obj);
+ // Rename the two properties that are different in the DB from in this object.
+ $vars['lbpid'] = $vars['id'];
+ $vars['mdlid'] = $vars['userid'];
+ unset($vars['id']);
+ unset($vars['userid']);
+ // Just throw the whole assarr in the constructor. Surely nothing bad will happen.
+ return new self(...$vars);
}
/**
@@ -161,6 +212,7 @@ public function set_fresh(int $lbpid): void {
* @param string $cbn colorblindness
*/
public function set_colorblindness(string $cbn): void {
+ // TODO: remove in favour of setting member directly.
$this->colorblindness = $cbn;
}
@@ -169,6 +221,7 @@ public function set_colorblindness(string $cbn): void {
* @param string $theme theme
*/
public function set_theme(string $theme): void {
+ // TODO: remove in favour of setting member directly.
$this->theme = $theme;
}
@@ -264,10 +317,11 @@ public function prepare_for_db(): object {
$obj = new \stdClass();
$obj->userid = $this->mdlid;
- $obj->theme = $this->theme;
- $obj->colorblindness = $this->colorblindness;
- $obj->displaytaskcount = $this->displaytaskcount;
- $obj->ekenabled = $this->ekenabled;
+
+ foreach (self::DBMIRRORPROPS as $propname) {
+ $phppropname = str_replace('_', '', $propname);
+ $obj->$propname = $this->$phppropname;
+ }
if ($this->lbpid !== 0) {
$obj->id = $this->lbpid;
@@ -335,6 +389,10 @@ public function prepare_for_api(): array {
'planid' => $this->get_planid(),
'colorblindness' => $this->colorblindness,
'displaytaskcount' => $this->displaytaskcount,
+ 'showcolumncolors' => $this->showcolumncolors,
+ 'automovecompletedtasks' => $this->automovecompletedtasks ?? KANBANCOL_TYPE_ORNONE::NONE,
+ 'automovesubmittedtasks' => $this->automovesubmittedtasks ?? KANBANCOL_TYPE_ORNONE::NONE,
+ 'automoveoverduetasks' => $this->automoveoverduetasks ?? KANBANCOL_TYPE_ORNONE::NONE,
'email' => $mdluser->email,
]
);
@@ -358,6 +416,19 @@ public static function api_structure(): external_single_structure {
'planid' => new external_value(PARAM_INT, 'The id of the plan the user is assigned to'),
'colorblindness' => new external_value(PARAM_TEXT, 'The colorblindness of the user'),
'displaytaskcount' => new external_value(PARAM_BOOL, 'Whether the user has the taskcount enabled'),
+ 'showcolumncolors' => new external_value(PARAM_BOOL, 'Whether column colors should show in kanban board'),
+ 'automovecompletedtasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if completed '.KANBANCOL_TYPE_ORNONE::format()
+ ),
+ 'automovesubmittedtasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if submitted '.KANBANCOL_TYPE_ORNONE::format()
+ ),
+ 'automoveoverduetasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if overdue '.KANBANCOL_TYPE_ORNONE::format()
+ ),
'capabilities' => new external_value(PARAM_INT, 'The capabilities of the user represented as a bitmask value'),
'vintage' => new external_value(PARAM_TEXT, 'The vintage of the user', VALUE_DEFAULT),
'email' => new external_value(PARAM_TEXT, 'The email address of the user'),
diff --git a/lbplanner/classes/polyfill/Enum.php b/lbplanner/classes/polyfill/Enum.php
index a0bce32e..82a5d244 100644
--- a/lbplanner/classes/polyfill/Enum.php
+++ b/lbplanner/classes/polyfill/Enum.php
@@ -27,6 +27,7 @@
use ReflectionClass;
use local_lbplanner\polyfill\EnumCase;
+use moodle_exception;
/**
* Class which is meant to serve as a substitute for native enums.
@@ -157,7 +158,14 @@ public static function cases(): array {
public static function format(): string {
$result = "[";
foreach (static::cases() as $case) {
- $result .= "{$case->value}=>{$case->name},";
+ if (is_string($case->value)) {
+ $formattedval = "\"{$case->value}\"";
+ } else if (is_int($case->value)) {
+ $formattedval = $case->value;
+ } else {
+ throw new moodle_exception('unimplemented case value type for Enum::format()');
+ }
+ $result .= "{$formattedval}=>{$case->name},";
}
$result[-1] = ']';
return $result;
diff --git a/lbplanner/db/install.xml b/lbplanner/db/install.xml
index 6585b39f..43c02073 100644
--- a/lbplanner/db/install.xml
+++ b/lbplanner/db/install.xml
@@ -8,6 +8,10 @@
+
+
+
+
@@ -87,7 +91,7 @@
-
+
@@ -146,5 +150,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lbplanner/db/services.php b/lbplanner/db/services.php
index 3433ee50..850a4c37 100644
--- a/lbplanner/db/services.php
+++ b/lbplanner/db/services.php
@@ -19,7 +19,7 @@
*
* @package local_lbplanner
* @subpackage db
- * @copyright 2024 NecodeIT
+ * @copyright 2025 NecodeIT
* @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
*/
@@ -48,7 +48,7 @@
'classname' => 'local_lbplanner_services\user_update_user',
'methodname' => 'update_user',
'classpath' => 'local/lbplanner/services/user/update_user.php',
- 'description' => 'Update the data for a user',
+ 'description' => 'Update the data for a user. null values or unset parameters are left unmodified',
'type' => 'write',
'capabilities' => 'local/lb_planner:student',
'ajax' => true,
@@ -395,6 +395,24 @@
'capabilities' => 'local/lb_planner:slotmaster',
'ajax' => true,
],
+ 'local_lbplanner_kanban_get_board' => [
+ 'classname' => 'local_lbplanner_services\kanban_get_board',
+ 'methodname' => 'get_board',
+ 'classpath' => 'local/lbplanner/services/kanban/get_board.php',
+ 'description' => 'Returns all entries in the kanban board for the current user',
+ 'type' => 'read',
+ 'capabilities' => 'local/lb_planner:student',
+ 'ajax' => true,
+ ],
+ 'local_lbplanner_kanban_move_module' => [
+ 'classname' => 'local_lbplanner_services\kanban_move_module',
+ 'methodname' => 'move_module',
+ 'classpath' => 'local/lbplanner/services/kanban/move_module.php',
+ 'description' => 'Moves a module to a different column on the kanban board',
+ 'type' => 'write',
+ 'capabilities' => 'local/lb_planner:student',
+ 'ajax' => true,
+ ],
];
$services = [
@@ -440,6 +458,8 @@
'local_lbplanner_slots_get_supervisor_slots',
'local_lbplanner_slots_remove_slot_supervisor',
'local_lbplanner_slots_update_slot',
+ 'local_lbplanner_kanban_get_board',
+ 'local_lbplanner_kanban_move_module',
],
'restrictedusers' => 0,
'enabled' => 1,
diff --git a/lbplanner/db/upgrade.php b/lbplanner/db/upgrade.php
index 9eab1bc5..75fcee5e 100644
--- a/lbplanner/db/upgrade.php
+++ b/lbplanner/db/upgrade.php
@@ -19,7 +19,7 @@
*
* @package local_lbplanner
* @subpackage db
- * @copyright 2024 NecodeIT
+ * @copyright 2025 NecodeIT
* @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
*/
@@ -32,9 +32,43 @@
* @return bool true
*/
function xmldb_local_lbplanner_upgrade($oldversion): bool {
+ global $DB;
+ $dbman = $DB->get_manager();
if ($oldversion < 202502110011) {
config_helper::remove_customfield();
config_helper::add_customfield();
}
+ if ($oldversion < 202509020000) {
+ $table = new xmldb_table('local_lbplanner_users');
+ $f1 = new xmldb_field('showcolumncolors', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, false, 1, 'ekenabled');
+ $f2 = new xmldb_field('automovecompletedtasks', XMLDB_TYPE_TEXT, null, null, null, null, null, 'showcolumncolors');
+ $f3 = new xmldb_field('automovesubmittedtasks', XMLDB_TYPE_TEXT, null, null, null, null, null, 'automovecompletedtasks');
+ $f4 = new xmldb_field('automoveoverduetasks', XMLDB_TYPE_TEXT, null, null, null, null, null, 'automovesubmittedtasks');
+
+ $dbman->add_field($table, $f1);
+ $dbman->add_field($table, $f2);
+ $dbman->add_field($table, $f3);
+ $dbman->add_field($table, $f4);
+ upgrade_plugin_savepoint(true, 202509020000, 'local', 'lbplanner');
+ }
+ if ($oldversion < 202509020001) {
+ $table = new xmldb_table('local_lbplanner_kanbanentries');
+
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('cmid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('column', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null);
+
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+ $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'local_lbplanner_users', ['userid']);
+
+ $table->add_index('uniqueentry', XMLDB_INDEX_UNIQUE, ['userid', 'cmid']);
+
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ upgrade_plugin_savepoint(true, 202509020001, 'local', 'lbplanner');
+ }
return true;
}
diff --git a/lbplanner/services/kanban/get_board.php b/lbplanner/services/kanban/get_board.php
new file mode 100644
index 00000000..ae275e82
--- /dev/null
+++ b/lbplanner/services/kanban/get_board.php
@@ -0,0 +1,91 @@
+.
+
+namespace local_lbplanner_services;
+
+use core_external\{
+ external_api,
+ external_function_parameters,
+ external_multiple_structure,
+ external_single_structure,
+ external_value,
+};
+use local_lbplanner\enums\{KANBANCOL_TYPE, KANBANCOL_TYPE_NUMERIC, MODULE_TYPE};
+use local_lbplanner\helpers\kanban_helper;
+use local_lbplanner\model\{module, user};
+
+/**
+ * Returns all entries in the kanban board for the current user.
+ *
+ * @package local_lbplanner
+ * @subpackage services_kanban
+ * @copyright 2025 necodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+class kanban_get_board extends external_api {
+ /**
+ * Parameters for kanban_get_board.
+ * @return external_function_parameters
+ */
+ public static function get_board_parameters(): external_function_parameters {
+ return new external_function_parameters([]);
+ }
+
+ /**
+ * Gets all the entries on this user's board.
+ */
+ public static function get_board(): array {
+ global $USER;
+
+ $entries = kanban_helper::get_all_entries_by_user($USER->id);
+
+ $sorted = [
+ KANBANCOL_TYPE_NUMERIC::TODO => [],
+ KANBANCOL_TYPE_NUMERIC::INPROGRESS => [],
+ KANBANCOL_TYPE_NUMERIC::DONE => [],
+ ];
+
+ $ekenabled = user::from_mdlobj($USER)->ekenabled;
+
+ foreach ($entries as $entry) {
+ if (!$ekenabled) {
+ $module = module::from_cmid($entry->cmid);
+ if ($module->get_type() === MODULE_TYPE::EK) {
+ continue;
+ }
+ }
+ array_push($sorted[$entry->column], $entry->cmid);
+ }
+
+ return [
+ KANBANCOL_TYPE::TODO => $sorted[KANBANCOL_TYPE_NUMERIC::TODO],
+ KANBANCOL_TYPE::INPROGRESS => $sorted[KANBANCOL_TYPE_NUMERIC::INPROGRESS],
+ KANBANCOL_TYPE::DONE => $sorted[KANBANCOL_TYPE_NUMERIC::DONE],
+ ];
+ }
+
+ /**
+ * Return structure of kanban_get_board
+ * @return external_multiple_structure
+ */
+ public static function get_board_returns() {
+ return new external_single_structure([
+ KANBANCOL_TYPE::TODO => new external_multiple_structure(new external_value(PARAM_INT, 'course-module ID')),
+ KANBANCOL_TYPE::INPROGRESS => new external_multiple_structure(new external_value(PARAM_INT, 'course-module ID')),
+ KANBANCOL_TYPE::DONE => new external_multiple_structure(new external_value(PARAM_INT, 'course-module ID')),
+ ]);
+ }
+}
diff --git a/lbplanner/services/kanban/move_module.php b/lbplanner/services/kanban/move_module.php
new file mode 100644
index 00000000..f33b9c29
--- /dev/null
+++ b/lbplanner/services/kanban/move_module.php
@@ -0,0 +1,82 @@
+.
+
+namespace local_lbplanner_services;
+
+use core_external\{external_api, external_function_parameters, external_value};
+use local_lbplanner\enums\KANBANCOL_TYPE;
+use local_lbplanner\helpers\kanban_helper;
+use local_lbplanner\model\kanbanentry;
+
+/**
+ * Moves a module to a different column on the kanban board.
+ *
+ * @package local_lbplanner
+ * @subpackage services_kanban
+ * @copyright 2025 necodeIT
+ * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
+ */
+class kanban_move_module extends external_api {
+ /**
+ * Parameters for move_module.
+ * @return external_function_parameters
+ */
+ public static function move_module_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'cmid' => new external_value(
+ PARAM_INT,
+ 'ID of the module to move',
+ VALUE_REQUIRED,
+ null,
+ NULL_NOT_ALLOWED
+ ),
+ 'column' => new external_value(
+ PARAM_TEXT,
+ 'name of the target column',
+ VALUE_REQUIRED,
+ null,
+ NULL_NOT_ALLOWED
+ ),
+ ]);
+ }
+
+ /**
+ * Moves a module to a different column on the kanban board.
+ * @param int $cmid content-module ID
+ * @param string $column name of the target column
+ */
+ public static function move_module(int $cmid, string $column): void {
+ global $USER;
+ self::validate_parameters(
+ self::move_module_parameters(),
+ [
+ 'cmid' => $cmid,
+ 'column' => $column,
+ ]
+ );
+
+ $colnr = KANBANCOL_TYPE::to_numeric($column);
+ kanban_helper::set_entry(new kanbanentry(0, $USER->id, $cmid, $colnr));
+ }
+
+ /**
+ * Return structure of move_module
+ * @return null
+ */
+ public static function move_module_returns() {
+ return null;
+ }
+}
diff --git a/lbplanner/services/user/update_user.php b/lbplanner/services/user/update_user.php
index 81dbd286..89eb5750 100644
--- a/lbplanner/services/user/update_user.php
+++ b/lbplanner/services/user/update_user.php
@@ -23,13 +23,14 @@
use local_lbplanner\helpers\user_helper;
use local_lbplanner\model\user;
+use local_lbplanner\enums\KANBANCOL_TYPE_ORNONE;
/**
- * Update the data for a user.
+ * Update the data for a user. null values or unset parameters are left unmodified.
*
* @package local_lbplanner
* @subpackage services_user
- * @copyright 2024 necodeIT
+ * @copyright 2025 necodeIT
* @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later
*/
class user_update_user extends external_api {
@@ -55,6 +56,26 @@ public static function update_user_parameters(): external_function_parameters {
'Whether the user wants to see EK modules',
VALUE_DEFAULT,
null),
+ 'showcolumncolors' => new external_value(
+ PARAM_BOOL,
+ 'Whether column colors should show in kanban board',
+ VALUE_DEFAULT,
+ null),
+ 'automovecompletedtasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if completed '.KANBANCOL_TYPE_ORNONE::format(),
+ VALUE_DEFAULT,
+ null),
+ 'automovesubmittedtasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if submitted '.KANBANCOL_TYPE_ORNONE::format(),
+ VALUE_DEFAULT,
+ null),
+ 'automoveoverduetasks' => new external_value(
+ PARAM_TEXT,
+ 'The kanban column to move a task to if overdue '.KANBANCOL_TYPE_ORNONE::format(),
+ VALUE_DEFAULT,
+ null),
]);
}
@@ -64,12 +85,25 @@ public static function update_user_parameters(): external_function_parameters {
* @param ?string $colorblindness The colorblindness the user has selected
* @param ?bool $displaytaskcount The displaytaskcount the user has selected
* @param ?bool $ekenabled whether the user wants to see EK modules
+ * @param ?bool $showcolumncolors whether column colors should show in kanban board
+ * @param ?string $automovecompletedtasks what kanban column to move completed tasks to ("" → don't move)
+ * @param ?string $automovesubmittedtasks what kanban column to move submitted tasks to ("" → don't move)
+ * @param ?string $automoveoverduetasks what kanban column to move overdue tasks to ("" → don't move)
* @return array The updated user
* @throws moodle_exception
* @throws dml_exception
* @throws invalid_parameter_exception
*/
- public static function update_user(?string $theme, ?string $colorblindness, ?bool $displaytaskcount, ?bool $ekenabled): array {
+ public static function update_user(
+ ?string $theme,
+ ?string $colorblindness,
+ ?bool $displaytaskcount,
+ ?bool $ekenabled,
+ ?bool $showcolumncolors,
+ ?string $automovecompletedtasks,
+ ?string $automovesubmittedtasks,
+ ?string $automoveoverduetasks,
+ ): array {
global $DB, $USER;
self::validate_parameters(
@@ -79,6 +113,10 @@ public static function update_user(?string $theme, ?string $colorblindness, ?boo
'colorblindness' => $colorblindness,
'displaytaskcount' => $displaytaskcount,
'ekenabled' => $ekenabled,
+ 'showcolumncolors' => $showcolumncolors,
+ 'automovecompletedtasks' => $automovecompletedtasks,
+ 'automovesubmittedtasks' => $automovesubmittedtasks,
+ 'automoveoverduetasks' => $automoveoverduetasks,
]
);
@@ -99,6 +137,18 @@ public static function update_user(?string $theme, ?string $colorblindness, ?boo
if ($ekenabled !== null) {
$user->ekenabled = $ekenabled;
}
+ if ($showcolumncolors !== null) {
+ $user->showcolumncolors = $showcolumncolors;
+ }
+ foreach (['automovecompletedtasks', 'automovesubmittedtasks', 'automoveoverduetasks'] as $propname) {
+ if ($$propname !== null) {
+ if ($$propname === KANBANCOL_TYPE_ORNONE::NONE) {
+ $user->$propname = null;
+ } else {
+ $user->$propname = $$propname;
+ }
+ }
+ }
$DB->update_record(user_helper::EDUPLANNER_USER_TABLE, $user->prepare_for_db());
diff --git a/lbplanner/version.php b/lbplanner/version.php
index ee33c580..88cb9940 100644
--- a/lbplanner/version.php
+++ b/lbplanner/version.php
@@ -28,7 +28,7 @@
$plugin->maturity = MATURITY_BETA;
$plugin->component = 'local_lbplanner';
$plugin->release = '1.0.2';
-$plugin->version = 202502200000;
+$plugin->version = 202509020004;
$plugin->dependencies = [
// Depend upon version 2023110600 of local_modcustomfields.
'local_modcustomfields' => 2023110600,