Skip to content

Commit

Permalink
MDL-55982 mod_forum: Add time-based discussion locking
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
andrewnicols committed Sep 21, 2016
1 parent 1f27448 commit 0f3bbfd
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 6 deletions.
2 changes: 1 addition & 1 deletion mod/forum/backup/moodle2/backup_forum_stepslib.php
Expand Up @@ -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');

Expand Down
9 changes: 9 additions & 0 deletions mod/forum/db/access.php
Expand Up @@ -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
)
),
);

3 changes: 2 additions & 1 deletion mod/forum/db/install.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/forum/db" VERSION="20160113" COMMENT="XMLDB file for Moodle mod/forum"
<XMLDB PATH="mod/forum/db" VERSION="20160912" COMMENT="XMLDB file for Moodle mod/forum"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -30,6 +30,7 @@
<FIELD NAME="completionreplies" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if a certain number of replies are required to mark this forum complete for a user."/>
<FIELD NAME="completionposts" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if a certain number of posts or replies (total) are required to mark this forum complete for a user."/>
<FIELD NAME="displaywordcount" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="lockdiscussionafter" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
Expand Down
15 changes: 15 additions & 0 deletions mod/forum/db/upgrade.php
Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions mod/forum/discuss.php
Expand Up @@ -372,6 +372,10 @@

echo "</div></div>";

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;
Expand Down
10 changes: 8 additions & 2 deletions mod/forum/externallib.php
Expand Up @@ -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(
Expand Down Expand Up @@ -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'
)
);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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'
)
),
Expand Down
8 changes: 8 additions & 0 deletions mod/forum/lang/en/forum.php
Expand Up @@ -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 <a href="{$a->discusshref}">here</a> in the forum <a href="{$a->forumhref}">{$a->forumname}</a>';
$string['discussionname'] = 'Discussion name';
Expand Down Expand Up @@ -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.';
Expand Down Expand Up @@ -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';
Expand Down
31 changes: 31 additions & 0 deletions mod/forum/lib.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
16 changes: 16 additions & 0 deletions mod/forum/mod_form.php
Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion mod/forum/tests/externallib_test.php
Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions mod/forum/tests/lib_test.php
Expand Up @@ -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
],
];
}
}
2 changes: 1 addition & 1 deletion mod/forum/version.php
Expand Up @@ -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)

0 comments on commit 0f3bbfd

Please sign in to comment.