diff --git a/mod/forum/backup/moodle2/backup_forum_stepslib.php b/mod/forum/backup/moodle2/backup_forum_stepslib.php index ed374a637a7..c01e33a22a3 100644 --- a/mod/forum/backup/moodle2/backup_forum_stepslib.php +++ b/mod/forum/backup/moodle2/backup_forum_stepslib.php @@ -44,7 +44,7 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste '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 e3eae61a83a..1dce7d453b3 100644 --- a/mod/forum/db/access.php +++ b/mod/forum/db/access.php @@ -366,5 +366,14 @@ $capabilities = array( '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 4e44b1bdbde..e1c33097e0d 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 942b7608f9d..eb131b79d39 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 29bfbb37625..2bb4a415708 100644 --- a/mod/forum/discuss.php +++ b/mod/forum/discuss.php @@ -372,6 +372,10 @@ if (has_capability('mod/forum:pindiscussions', $modcontext)) { 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 c0db8fed07e..005e7fbd1d8 100644 --- a/mod/forum/externallib.php +++ b/mod/forum/externallib.php @@ -112,7 +112,7 @@ class mod_forum_external extends external_api { * @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 @@ class mod_forum_external extends external_api { '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 @@ class mod_forum_external extends external_api { $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 @@ class mod_forum_external extends external_api { '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 e5c74595683..dea33f2ca29 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -133,6 +133,9 @@ $string['disallowsubscription'] = 'Subscription'; $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:addinstance'] = 'Add a new forum'; $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['invalidparentpostid'] = 'Parent post ID was incorrect'; $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 5b08c41b4e8..265a002e92e 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 9f9cc0a5c29..34465e3a8b2 100644 --- a/mod/forum/mod_form.php +++ b/mod/forum/mod_form.php @@ -147,6 +147,22 @@ class mod_forum_mod_form extends moodleform_mod { } } + $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 f9f08ca3e8a..9f7612ae9cb 100644 --- a/mod/forum/tests/externallib_test.php +++ b/mod/forum/tests/externallib_test.php @@ -491,7 +491,9 @@ class mod_forum_external_testcase extends externallib_advanced_testcase { '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 3453534017d..ef135e5eac3 100644 --- a/mod/forum/tests/lib_test.php +++ b/mod/forum/tests/lib_test.php @@ -3203,4 +3203,61 @@ class mod_forum_lib_testcase extends advanced_testcase { ], ]; } + + /** + * 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 8c7190859fa..09f78b36b62 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)