diff --git a/backup/moodle2/backup_kanban_stepslib.php b/backup/moodle2/backup_kanban_stepslib.php
index cc9b951f..a7a1cd89 100644
--- a/backup/moodle2/backup_kanban_stepslib.php
+++ b/backup/moodle2/backup_kanban_stepslib.php
@@ -34,7 +34,20 @@ protected function define_structure(): backup_nested_element {
$kanban = new backup_nested_element(
'kanban',
['id'],
- ['course', 'name', 'intro', 'introformat', 'userboards', 'history', 'completioncreate', 'completioncomplete']
+ [
+ 'course',
+ 'name',
+ 'intro',
+ 'introformat',
+ 'userboards',
+ 'history',
+ 'completioncreate',
+ 'completioncomplete',
+ 'repeat_enable',
+ 'repeat_interval',
+ 'repeat_interval_type',
+ 'repeat_newduedate',
+ ]
);
$kanban->set_source_table('kanban', ['id' => backup::VAR_ACTIVITYID]);
$kanban->annotate_files('mod_kanban', 'intro', null);
diff --git a/classes/boardmanager.php b/classes/boardmanager.php
index 20635f51..4449ef08 100644
--- a/classes/boardmanager.php
+++ b/classes/boardmanager.php
@@ -18,7 +18,7 @@
* Class to handle updating the board
*
* @package mod_kanban
- * @copyright 2023-2024 ISB Bayern
+ * @copyright 2023-2024 ISB Bayern
* @author Stefan Hanauska
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -34,7 +34,7 @@
* Class to handle updating the board. It also sends notifications, but does not check permissions.
*
* @package mod_kanban
- * @copyright 2023-2024 ISB Bayern
+ * @copyright 2023-2024 ISB Bayern
* @author Stefan Hanauska
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -477,7 +477,6 @@ public function add_card(int $columnid, int $aftercard = 0, array $data = []): i
// Users can always edit cards they created.
$data['canedit'] = $this->can_user_manage_specific_card($data['id']);
- ;
$data['columnname'] = clean_param($column->title, PARAM_TEXT);
$this->formatter->put('cards', $data);
@@ -547,7 +546,9 @@ public function move_card(int $cardid, int $aftercard, int $columnid = 0): void
// If target column has autoclose option set, update card to be completed.
$options = json_decode($targetcolumn->options);
if (!empty($options->autoclose)) {
- $updatecard['completed'] = 1;
+ if ($card['completed']) {
+ self::set_card_complete($cardid, 1);
+ }
}
$DB->update_record('kanban_card', $updatecard);
// When inplace editing the title and moving the card happens quite fast in a row,
@@ -581,11 +582,7 @@ public function move_card(int $cardid, int $aftercard, int $columnid = 0): void
$assignees = $this->get_card_assignees($cardid);
helper::send_notification($this->cminfo, 'moved', $assignees, (object) $data);
if (!empty($options->autoclose) && $card->completed == 0) {
- $data['title'] = clean_param($card->title, PARAM_TEXT);
- helper::send_notification($this->cminfo, 'closed', $assignees, (object) $data);
- helper::remove_calendar_event($this->kanban, $card);
- $this->write_history('completed', constants::MOD_KANBAN_CARD, [], $columnid, $cardid);
- $this->update_completion($assignees);
+ self::set_card_complete($cardid, 1);
}
$this->write_history(
'moved',
@@ -688,6 +685,30 @@ public function set_card_complete(int $cardid, int $state): void {
$assignees = $this->get_card_assignees($cardid);
if ($state) {
helper::remove_calendar_event($this->kanban, $card, $assignees);
+ if (!empty($card->repeat_enable)) {
+ $newcard = clone $card;
+ if ($card->repeat_newduedate == constants::MOD_KANBAN_REPEAT_NONEWDUEDATE) {
+ $newcard->duedate = 0;
+ $newcard->reminder = 0;
+ } else {
+ $timedifference = $newcard->duedate - $newcard->reminder;
+ $timebase = (
+ $card->repeat_newduedate == constants::MOD_KANBAN_REPEAT_NEWDUEDATE_AFTERDUE && !empty($newcard->duedate) ?
+ $newcard->duedate :
+ time()
+ );
+ $newcard->duedate = strtotime(
+ '+' .
+ $card->repeat_interval .
+ ' ' .
+ constants::MOD_KANBAN_REPEAT_INTERVAL_TYPE[$card->repeat_interval_type],
+ $timebase
+ );
+ $newcard->reminder = $newcard->duedate - $timedifference;
+ }
+ $newcard->isrepeated = 1;
+ $this->add_card($this->get_leftmost_column($card->kanban_board), 0, (array)$newcard);
+ }
} else {
helper::add_or_update_calendar_event($this->kanban, $card, $assignees);
}
@@ -813,6 +834,10 @@ public function update_card(int $cardid, array $data): void {
'kanban_column',
'kanban_board',
'completed',
+ 'repeat_enable',
+ 'repeat_interval',
+ 'repeat_interval_type',
+ 'repeat_newduedate',
];
// Do some extra sanitizing.
if (isset($data['title'])) {
@@ -1230,4 +1255,24 @@ public function can_user_manage_specific_card(int $cardid, int $userid = 0): boo
return false;
}
+
+ /**
+ * Returns the leftmost column of a board, 0 if none is found.
+ *
+ * @param int $boardid Id of the board, defaults to 0 (current board)
+ * @return int
+ */
+ public function get_leftmost_column(int $boardid = 0): int {
+ global $DB;
+ if (empty($boardid) || $this->board->id == $boardid) {
+ $sequence = $this->board->sequence;
+ } else {
+ $sequence = $DB->get_field('kanban_board', 'sequence', ['id' => $boardid]);
+ }
+ if (empty($sequence)) {
+ return 0;
+ }
+ $columnids = explode(',', $sequence, 2);
+ return $columnids[0];
+ }
}
diff --git a/classes/constants.php b/classes/constants.php
index 635863cc..d9fb199e 100644
--- a/classes/constants.php
+++ b/classes/constants.php
@@ -91,4 +91,46 @@ class constants {
self::MOD_KANBAN_DISCUSSION => 'discussion',
self::MOD_KANBAN_HISTORY => 'history',
];
+ /**
+ * Repeat interval type: hours
+ */
+ public const MOD_KANBAN_REPEAT_HOURS = 2;
+ /**
+ * Repeat interval type: days
+ */
+ public const MOD_KANBAN_REPEAT_DAYS = 3;
+ /**
+ * Repeat interval type: weeks
+ */
+ public const MOD_KANBAN_REPEAT_WEEKS = 4;
+ /**
+ * Repeat interval type: months
+ */
+ public const MOD_KANBAN_REPEAT_MONTHS = 5;
+ /**
+ * Repeat interval type: years
+ */
+ public const MOD_KANBAN_REPEAT_YEARS = 6;
+ /**
+ * Mapping of repeat interval types to strings
+ */
+ public const MOD_KANBAN_REPEAT_INTERVAL_TYPE = [
+ self::MOD_KANBAN_REPEAT_HOURS => 'hour',
+ self::MOD_KANBAN_REPEAT_DAYS => 'day',
+ self::MOD_KANBAN_REPEAT_WEEKS => 'week',
+ self::MOD_KANBAN_REPEAT_MONTHS => 'month',
+ self::MOD_KANBAN_REPEAT_YEARS => 'year',
+ ];
+ /**
+ * Repeat new due date: no new due date
+ */
+ public const MOD_KANBAN_REPEAT_NONEWDUEDATE = 0;
+ /**
+ * Repeat new due date: after due date
+ */
+ public const MOD_KANBAN_REPEAT_NEWDUEDATE_AFTERDUE = 1;
+ /**
+ * Repeat new due date: after completion
+ */
+ public const MOD_KANBAN_REPEAT_NEWDUEDATE_AFTERCOMPLETION = 2;
}
diff --git a/classes/external/change_kanban_content.php b/classes/external/change_kanban_content.php
index 6d7eca81..7b9c193d 100644
--- a/classes/external/change_kanban_content.php
+++ b/classes/external/change_kanban_content.php
@@ -299,7 +299,6 @@ public static function move_card_returns(): external_single_structure {
* @throws moodle_exception
*/
public static function move_card(int $cmid, int $boardid, array $data): array {
- global $USER;
$params = self::validate_parameters(self::move_card_parameters(), [
'cmid' => $cmid,
'boardid' => $boardid,
diff --git a/classes/form/edit_card_form.php b/classes/form/edit_card_form.php
index df28ac5f..151b7d35 100644
--- a/classes/form/edit_card_form.php
+++ b/classes/form/edit_card_form.php
@@ -21,13 +21,14 @@
use core_form\dynamic_form;
use mod_kanban\boardmanager;
use mod_kanban\helper;
+use mod_kanban\constants;
use moodle_url;
/**
* From for editing a card.
*
* @package mod_kanban
- * @copyright 2023-2024 ISB Bayern
+ * @copyright 2023-2024 ISB Bayern
* @author Stefan Hanauska
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -80,6 +81,32 @@ public function definition() {
$mform->addElement('date_time_selector', 'reminderdate', get_string('reminderdate', 'kanban'), ['optional' => true]);
+ $repeatgroup = [];
+ $repeatgroup[] = $mform->createElement('advcheckbox', 'repeat_enable', get_string('enable'));
+ $repeatgroup[] = $mform->createElement('text', 'repeat_interval', get_string('repeat_interval', 'kanban'), ['size' => 3]);
+ $repeatgroup[] = $mform->createElement('select', 'repeat_interval_type', get_string('repeat_interval_type', 'kanban'), [
+ constants::MOD_KANBAN_REPEAT_HOURS => get_string('hours'),
+ constants::MOD_KANBAN_REPEAT_DAYS => get_string('days'),
+ constants::MOD_KANBAN_REPEAT_WEEKS => get_string('weeks'),
+ constants::MOD_KANBAN_REPEAT_MONTHS => get_string('months'),
+ constants::MOD_KANBAN_REPEAT_YEARS => get_string('years'),
+ ]);
+ $repeatgroup[] = $mform->createElement('select', 'repeat_newduedate', get_string('repeat_newduedate', 'kanban'), [
+ constants::MOD_KANBAN_REPEAT_NONEWDUEDATE => get_string('nonewduedate', 'kanban'),
+ constants::MOD_KANBAN_REPEAT_NEWDUEDATE_AFTERDUE => get_string('afterdue', 'kanban'),
+ constants::MOD_KANBAN_REPEAT_NEWDUEDATE_AFTERCOMPLETION => get_string('aftercompletion', 'kanban'),
+ ]);
+
+ $mform->addElement('group', 'repeatgroup', get_string('repeat', 'kanban'), $repeatgroup, ' ', false);
+
+ $mform->setType('repeat_interval', PARAM_INT);
+ $mform->setType('repeat_interval_type', PARAM_INT);
+ $mform->setDefault('repeat_interval', 1);
+ $mform->disabledIf('repeatgroup', 'repeat_enable', 'notchecked');
+ $mform->disabledIf('repeat_interval', 'repeat_newduedate', 'eq', constants::MOD_KANBAN_REPEAT_NONEWDUEDATE);
+ $mform->disabledIf('repeat_interval_type', 'repeat_newduedate', 'eq', constants::MOD_KANBAN_REPEAT_NONEWDUEDATE);
+ $mform->addHelpButton('repeatgroup', 'repeat', 'kanban');
+
$mform->addElement('filemanager', 'attachments', get_string('attachments', 'kanban'));
$mform->addElement('color', 'color', get_string('color', 'mod_kanban'), ['size' => 5]);
diff --git a/db/install.xml b/db/install.xml
index 6227c5a1..861fcbfb 100755
--- a/db/install.xml
+++ b/db/install.xml
@@ -78,6 +78,10 @@
+
+
+
+
diff --git a/db/upgrade.php b/db/upgrade.php
index 5a15e923..274e60c4 100755
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -18,7 +18,7 @@
* mod_kanban db upgrades.
*
* @package mod_kanban
- * @copyright 2023-2024 ISB Bayern
+ * @copyright 2023-2024 ISB Bayern
* @author Stefan Hanauska
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -29,6 +29,60 @@
* @param int $oldversion Version number the plugin is being upgraded from.
*/
function xmldb_kanban_upgrade($oldversion) {
- // No upgrade steps until now.
+ global $DB;
+ $dbman = $DB->get_manager();
+
+ if ($oldversion < 2024121602) {
+ // Define field repeat_enable to be added to kanban_card.
+ $table = new xmldb_table('kanban_card');
+ $field = new xmldb_field('repeat_enable', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'timemodified');
+
+ // Conditionally launch add field repeat_enable.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ $field = new xmldb_field('repeat_interval', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, '0', 'repeat_enable');
+
+ // Conditionally launch add field repeat_interval.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ $field = new xmldb_field(
+ 'repeat_interval_type',
+ XMLDB_TYPE_INTEGER,
+ '11',
+ null,
+ XMLDB_NOTNULL,
+ null,
+ '0',
+ 'repeat_interval'
+ );
+
+ // Conditionally launch add field repeat_interval_type.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ $field = new xmldb_field(
+ 'repeat_newduedate',
+ XMLDB_TYPE_INTEGER,
+ '5',
+ null,
+ XMLDB_NOTNULL,
+ null,
+ '0',
+ 'repeat_interval_type'
+ );
+
+ // Conditionally launch add field repeat_newduedate.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Kanban savepoint reached.
+ upgrade_mod_savepoint(true, 2024121602, 'kanban');
+ }
return true;
}
diff --git a/lang/en/kanban.php b/lang/en/kanban.php
index c06f3cfb..2ffa4a53 100644
--- a/lang/en/kanban.php
+++ b/lang/en/kanban.php
@@ -27,6 +27,8 @@
$string['addcard'] = 'Add a card to this column';
$string['addcolumn'] = 'Add a column to this board';
+$string['aftercompletion'] = 'after card is closed';
+$string['afterdue'] = 'after card is due';
$string['assignee'] = 'Assignee';
$string['assignees'] = 'Assignees';
$string['assignme'] = 'Assign me';
@@ -143,6 +145,7 @@
$string['newcolumn'] = 'New column';
$string['nogroupavailable'] = 'No group available';
$string['nokanbaninstances'] = 'There are no kanban boards in this course or you are not allowed to access them';
+$string['nonewduedate'] = 'No new due date';
$string['nouser'] = 'No user';
$string['nouserboards'] = 'No personal boards';
$string['pluginadministration'] = 'Kanban administration';
@@ -167,6 +170,12 @@
$string['pushcardconfirm'] = 'This will send a copy of this card to all boards inside this kanban activity including templates. Existing copies will be replaced.';
$string['reminderdate'] = 'Reminder date';
$string['remindertask'] = 'Send reminder notifications';
+$string['repeat'] = 'Repeat card';
+$string['repeat_help'] = "If selected, a new copy of this card will be created in the leftmost column as soon as this instance is completed. Discussion, history and assignees are not copied.
+You can choose how to calculate the new due date, if needed. This will also be applied to the new reminder date.";
+$string['repeat_interval'] = 'Interval';
+$string['repeat_interval_type'] = 'Frequency';
+$string['repeat_newduedate'] = 'New due date';
$string['reset_group'] = 'Reset group boards';
$string['reset_kanban'] = 'Reset shared boards';
$string['reset_personal'] = 'Reset personal boards';
diff --git a/tests/change_kanban_content_test.php b/tests/change_kanban_content_test.php
index c08c4571..9e5b0fd2 100644
--- a/tests/change_kanban_content_test.php
+++ b/tests/change_kanban_content_test.php
@@ -263,10 +263,12 @@ public function test_move_card(): void {
$update = json_decode($returnvalue['update'], true);
- $this->assertCount(3, $update);
+ // As the target column has autoclose enabled by default, we get two updates for cards.
+ $this->assertCount(4, $update);
$this->assertEquals('cards', $update[0]['name']);
$this->assertEquals('columns', $update[1]['name']);
$this->assertEquals('columns', $update[2]['name']);
+ $this->assertEquals('cards', $update[3]['name']);
$this->assertEquals(join(',', [$cards[0]->id, $cards[2]->id]), $update[2]['fields']['sequence']);
$this->assertEquals('', $update[1]['fields']['sequence']);
@@ -301,10 +303,12 @@ public function test_move_card(): void {
$update = json_decode($returnvalue['update'], true);
- $this->assertCount(3, $update);
+ // As the target column has autoclose enabled by default, we get two updates for cards.
+ $this->assertCount(4, $update);
$this->assertEquals('cards', $update[0]['name']);
$this->assertEquals('columns', $update[1]['name']);
$this->assertEquals('columns', $update[2]['name']);
+ $this->assertEquals('cards', $update[3]['name']);
$this->assertEquals(join(',', [$cards[2]->id, $cards[1]->id, $cards[0]->id]), $update[2]['fields']['sequence']);
$this->assertEquals('', $update[1]['fields']['sequence']);
diff --git a/version.php b/version.php
index a788d7f0..43c2b633 100644
--- a/version.php
+++ b/version.php
@@ -26,7 +26,7 @@
$plugin->component = 'mod_kanban';
$plugin->release = '0.2.6';
-$plugin->version = 2024121601;
+$plugin->version = 2024121602;
$plugin->requires = 2022112800;
$plugin->supported = [401, 405];
$plugin->maturity = MATURITY_STABLE;