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,