From 0f3bbfd4f8e85ea6e938766de79e4d25a09dedc5 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 13 Sep 2016 08:16:11 +0800 Subject: [PATCH] MDL-55982 mod_forum: Add time-based discussion locking This patch adds support for time-based locking of discussions. Discussions are automatically locked after a user-definable period of inactivity. After this time, only those with the the relevant capability are able to add replies. This has been designed to add support for other types of discussion locking at a later date with relative ease. --- .../backup/moodle2/backup_forum_stepslib.php | 2 +- mod/forum/db/access.php | 9 +++ mod/forum/db/install.xml | 3 +- mod/forum/db/upgrade.php | 15 +++++ mod/forum/discuss.php | 4 ++ mod/forum/externallib.php | 10 +++- mod/forum/lang/en/forum.php | 8 +++ mod/forum/lib.php | 31 ++++++++++ mod/forum/mod_form.php | 16 ++++++ mod/forum/tests/externallib_test.php | 4 +- mod/forum/tests/lib_test.php | 57 +++++++++++++++++++ mod/forum/version.php | 2 +- 12 files changed, 155 insertions(+), 6 deletions(-) diff --git a/mod/forum/backup/moodle2/backup_forum_stepslib.php b/mod/forum/backup/moodle2/backup_forum_stepslib.php index ed374a637a709..c01e33a22a31d 100644 --- a/mod/forum/backup/moodle2/backup_forum_stepslib.php +++ b/mod/forum/backup/moodle2/backup_forum_stepslib.php @@ -44,7 +44,7 @@ protected function define_structure() { 'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype', 'rsstype', 'rssarticles', 'timemodified', 'warnafter', 'blockafter', 'blockperiod', 'completiondiscussions', 'completionreplies', - 'completionposts', 'displaywordcount')); + 'completionposts', 'displaywordcount', 'lockdiscussionafter')); $discussions = new backup_nested_element('discussions'); diff --git a/mod/forum/db/access.php b/mod/forum/db/access.php index e3eae61a83abd..1dce7d453b3f6 100644 --- a/mod/forum/db/access.php +++ b/mod/forum/db/access.php @@ -366,5 +366,14 @@ 'manager' => CAP_ALLOW ) ), + 'mod/forum:canoverridediscussionlock' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), ); diff --git a/mod/forum/db/install.xml b/mod/forum/db/install.xml index 4e44b1bdbde23..e1c33097e0d63 100644 --- a/mod/forum/db/install.xml +++ b/mod/forum/db/install.xml @@ -1,5 +1,5 @@ - @@ -30,6 +30,7 @@ + diff --git a/mod/forum/db/upgrade.php b/mod/forum/db/upgrade.php index 942b7608f9d76..eb131b79d3935 100644 --- a/mod/forum/db/upgrade.php +++ b/mod/forum/db/upgrade.php @@ -177,5 +177,20 @@ function xmldb_forum_upgrade($oldversion) { // Moodle v3.1.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2016091200) { + + // Define field lockdiscussionafter to be added to forum. + $table = new xmldb_table('forum'); + $field = new xmldb_field('lockdiscussionafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'displaywordcount'); + + // Conditionally launch add field lockdiscussionafter. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Forum savepoint reached. + upgrade_mod_savepoint(true, 2016091200, 'forum'); + } + return true; } diff --git a/mod/forum/discuss.php b/mod/forum/discuss.php index 29bfbb376250c..2bb4a41570836 100644 --- a/mod/forum/discuss.php +++ b/mod/forum/discuss.php @@ -372,6 +372,10 @@ echo ""; +if (forum_discussion_is_locked($forum, $discussion)) { + echo html_writer::div(get_string('discussionlocked', 'forum'), 'discussionlocked'); +} + if (!empty($forum->blockafter) && !empty($forum->blockperiod)) { $a = new stdClass(); $a->blockafter = $forum->blockafter; diff --git a/mod/forum/externallib.php b/mod/forum/externallib.php index c0db8fed07ed6..005e7fbd1d83e 100644 --- a/mod/forum/externallib.php +++ b/mod/forum/externallib.php @@ -112,7 +112,7 @@ public static function get_forums_by_courses($courseids = array()) { * @return external_single_structure * @since Moodle 2.5 */ - public static function get_forums_by_courses_returns() { + public static function get_forums_by_courses_returns() { return new external_multiple_structure( new external_single_structure( array( @@ -143,6 +143,7 @@ public static function get_forums_by_courses_returns() { 'cmid' => new external_value(PARAM_INT, 'Course module id'), 'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL), 'cancreatediscussions' => new external_value(PARAM_BOOL, 'If the user can create discussions', VALUE_OPTIONAL), + 'lockdiscussionafter' => new external_value(PARAM_INT, 'After what period a discussion is locked', VALUE_OPTIONAL), ), 'forum' ) ); @@ -499,6 +500,9 @@ public static function get_forum_discussions_paginated($forumid, $sortby = 'time $discussion->id); } + $discussion->locked = forum_discussion_is_locked($forum, $discussion); + $discussion->canreply = forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext); + $discussions[] = $discussion; } } @@ -549,7 +553,9 @@ public static function get_forum_discussions_paginated_returns() { 'usermodifiedpictureurl' => new external_value(PARAM_URL, 'Post modifier picture.'), 'numreplies' => new external_value(PARAM_TEXT, 'The number of replies in the discussion'), 'numunread' => new external_value(PARAM_INT, 'The number of unread discussions.'), - 'pinned' => new external_value(PARAM_BOOL, 'Is the discussion pinned') + 'pinned' => new external_value(PARAM_BOOL, 'Is the discussion pinned'), + 'locked' => new external_value(PARAM_BOOL, 'Is the discussion locked'), + 'canreply' => new external_value(PARAM_BOOL, 'Can the user reply to the discussion'), ), 'post' ) ), diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index e5c7459568356..dea33f2ca29a3 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -133,6 +133,9 @@ $string['disallowsubscription_help'] = 'This forum has been configured so that you cannot subscribe to discussions.'; $string['disallowsubscribeteacher'] = 'Subscriptions not allowed (except for teachers)'; $string['discussion'] = 'Discussion'; +$string['discussionlocked'] = 'This discussion has been locked so you can no longer reply to it.'; +$string['discussionlockingheader'] = 'Discussion locking'; +$string['discussionlockingdisabled'] = 'Do not lock discussions'; $string['discussionmoved'] = 'This discussion has been moved to \'{$a}\'.'; $string['discussionmovedpost'] = 'This discussion has been moved to here in the forum {$a->forumname}'; $string['discussionname'] = 'Discussion name'; @@ -216,6 +219,7 @@ $string['forum:addnews'] = 'Add news'; $string['forum:addquestion'] = 'Add question'; $string['forum:allowforcesubscribe'] = 'Allow force subscribe'; +$string['forum:canoverridediscussionlock'] = 'Reply to locked discussions'; $string['forumauthorhidden'] = 'Author (hidden)'; $string['forumblockingalmosttoomanyposts'] = 'You are approaching the posting threshold. You have posted {$a->numposts} times in the last {$a->blockperiod} and the limit is {$a->blockafter} posts.'; $string['forumbodyhidden'] = 'This post cannot be viewed by you, probably because you have not posted in the discussion, the maximum editing time hasn\'t passed yet, the discussion has not started or the discussion has expired.'; @@ -275,6 +279,10 @@ $string['invalidpostid'] = 'Invalid post ID - {$a}'; $string['lastpost'] = 'Last post'; $string['learningforums'] = 'Learning forums'; +$string['lockdiscussionafter'] = 'Lock discussions after period of inactivity'; +$string['lockdiscussionafter_help'] = 'Discussions may be automatically locked after a specified time has elapsed since the last reply. + +Users with the capability to reply to locked discussions can unlock a discussion by replying to it.'; $string['longpost'] = 'Long post'; $string['mailnow'] = 'Send forum post notifications with no editing-time delay'; $string['manydiscussions'] = 'Discussions per page'; diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 5b08c41b4e87d..265a002e92ea9 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -5043,6 +5043,13 @@ function forum_user_can_post($forum, $discussion, $user=NULL, $cm=NULL, $course= $context = context_module::instance($cm->id); } + // Check whether the discussion is locked. + if (forum_discussion_is_locked($forum, $discussion)) { + if (!has_capability('mod/forum:canoverridediscussionlock', $context)) { + return false; + } + } + // normal users with temporary guest access can not post, suspended users can not post either if (!is_viewing($context, $user->id) and !is_enrolled($context, $user->id, '', true)) { return false; @@ -8013,3 +8020,27 @@ function mod_forum_inplace_editable($itemtype, $itemid, $newvalue) { return $renderer->render_digest_options($forum, $newvalue); } } + +/** + * Determine whether the specified discussion is time-locked. + * + * @param stdClass $forum The forum that the discussion belongs to + * @param stdClass $discussion The discussion to test + * @return bool + */ +function forum_discussion_is_locked($forum, $discussion) { + if (empty($forum->lockdiscussionafter)) { + return false; + } + + if ($forum->type === 'single') { + // It does not make sense to lock a single discussion forum. + return false; + } + + if (($discussion->timemodified + $forum->lockdiscussionafter) < time()) { + return true; + } + + return false; +} diff --git a/mod/forum/mod_form.php b/mod/forum/mod_form.php index 9f9cc0a5c2927..34465e3a8b23a 100644 --- a/mod/forum/mod_form.php +++ b/mod/forum/mod_form.php @@ -147,6 +147,22 @@ function definition() { } } + $mform->addElement('header', 'discussionlocking', get_string('discussionlockingheader', 'forum')); + $options = [ + 0 => get_string('discussionlockingdisabled', 'forum'), + 1 * DAYSECS => get_string('numday', 'core', 1), + 1 * WEEKSECS => get_string('numweek', 'core', 1), + 2 * WEEKSECS => get_string('numweeks', 'core', 2), + 30 * DAYSECS => get_string('nummonth', 'core', 1), + 60 * DAYSECS => get_string('nummonths', 'core', 2), + 90 * DAYSECS => get_string('nummonths', 'core', 3), + 180 * DAYSECS => get_string('nummonths', 'core', 6), + 1 * YEARSECS => get_string('numyear', 'core', 1), + ]; + $mform->addElement('select', 'lockdiscussionafter', get_string('lockdiscussionafter', 'forum'), $options); + $mform->addHelpButton('lockdiscussionafter', 'lockdiscussionafter', 'forum'); + $mform->disabledIf('lockdiscussionafter', 'type', 'eq', 'single'); + //------------------------------------------------------------------------------- $mform->addElement('header', 'blockafterheader', get_string('blockafter', 'forum')); $options = array(); diff --git a/mod/forum/tests/externallib_test.php b/mod/forum/tests/externallib_test.php index f9f08ca3e8a40..9f7612ae9cb5e 100644 --- a/mod/forum/tests/externallib_test.php +++ b/mod/forum/tests/externallib_test.php @@ -491,7 +491,9 @@ public function test_mod_forum_get_forum_discussions_paginated() { 'usermodifiedpictureurl' => '', 'numreplies' => 3, 'numunread' => 0, - 'pinned' => FORUM_DISCUSSION_UNPINNED + 'pinned' => FORUM_DISCUSSION_UNPINNED, + 'locked' => false, + 'canreply' => false, ); // Call the external function passing forum id. diff --git a/mod/forum/tests/lib_test.php b/mod/forum/tests/lib_test.php index 3453534017d38..ef135e5eac33a 100644 --- a/mod/forum/tests/lib_test.php +++ b/mod/forum/tests/lib_test.php @@ -3203,4 +3203,61 @@ public function forum_get_unmailed_posts_provider() { ], ]; } + + /** + * Test the forum_discussion_is_locked function. + * + * @dataProvider forum_discussion_is_locked_provider + * @param stdClass $forum + * @param stdClass $discussion + * @param bool $expect + */ + public function test_forum_discussion_is_locked($forum, $discussion, $expect) { + $this->assertEquals($expect, forum_discussion_is_locked($forum, $discussion)); + } + + /** + * Dataprovider for forum_discussion_is_locked tests. + * + * @return array + */ + public function forum_discussion_is_locked_provider() { + return [ + 'Unlocked: lockdiscussionafter is unset' => [ + (object) [], + (object) [], + false + ], + 'Unlocked: lockdiscussionafter is false' => [ + (object) ['lockdiscussionafter' => false], + (object) [], + false + ], + 'Unlocked: lockdiscussionafter is null' => [ + (object) ['lockdiscussionafter' => null], + (object) [], + false + ], + 'Unlocked: lockdiscussionafter is set; forum is of type single; post is recent' => [ + (object) ['lockdiscussionafter' => DAYSECS, 'type' => 'single'], + (object) ['timemodified' => time()], + false + ], + 'Unlocked: lockdiscussionafter is set; forum is of type single; post is old' => [ + (object) ['lockdiscussionafter' => MINSECS, 'type' => 'single'], + (object) ['timemodified' => time() - DAYSECS], + false + ], + 'Unlocked: lockdiscussionafter is set; forum is of type eachuser; post is recent' => [ + (object) ['lockdiscussionafter' => DAYSECS, 'type' => 'eachuser'], + (object) ['timemodified' => time()], + false + ], + 'Locked: lockdiscussionafter is set; forum is of type eachuser; post is old' => [ + (object) ['lockdiscussionafter' => MINSECS, 'type' => 'eachuser'], + (object) ['timemodified' => time() - DAYSECS], + true + ], + ]; + } } diff --git a/mod/forum/version.php b/mod/forum/version.php index 8c7190859fae0..09f78b36b62ff 100644 --- a/mod/forum/version.php +++ b/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016052300; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2016091201; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2016051900; // Requires this Moodle version $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)