diff --git a/mod/forum/classes/local/data_mappers/legacy/forum.php b/mod/forum/classes/local/data_mappers/legacy/forum.php index bc89b2c31f6..97aa7a9b2fd 100644 --- a/mod/forum/classes/local/data_mappers/legacy/forum.php +++ b/mod/forum/classes/local/data_mappers/legacy/forum.php @@ -69,7 +69,9 @@ class forum { 'completionreplies' => $forum->get_completion_replies(), 'completionposts' => $forum->get_completion_posts(), 'displaywordcount' => $forum->should_display_word_count(), - 'lockdiscussionafter' => $forum->get_lock_discussions_after() + 'lockdiscussionafter' => $forum->get_lock_discussions_after(), + 'duedate' => $forum->get_due_date(), + 'cutoffdate' => $forum->get_cutoff_date() ]; }, $forums); } diff --git a/mod/forum/classes/local/entities/forum.php b/mod/forum/classes/local/entities/forum.php index fe9e9395b30..34459634be5 100644 --- a/mod/forum/classes/local/entities/forum.php +++ b/mod/forum/classes/local/entities/forum.php @@ -99,6 +99,10 @@ class forum { private $displaywordcounts; /** @var bool $lockdiscussionafter Timestamp after which discussions should be locked */ private $lockdiscussionafter; + /** @var int $duedate Timestamp that represents the due date for forum posts */ + private $duedate; + /** @var int $cutoffdate Timestamp after which forum posts will no longer be accepted */ + private $cutoffdate; /** * Constructor @@ -132,6 +136,8 @@ class forum { * @param int $completionposts Completion posts * @param bool $displaywordcount Should display word counts in posts * @param int $lockdiscussionafter Timestamp after which discussions should be locked + * @param int $duedate Timestamp that represents the due date for forum posts + * @param int $cutoffdate Timestamp after which forum posts will no longer be accepted */ public function __construct( context $context, @@ -162,7 +168,9 @@ class forum { int $completionreplies, int $completionposts, bool $displaywordcount, - int $lockdiscussionafter + int $lockdiscussionafter, + int $duedate, + int $cutoffdate ) { $this->context = $context; $this->coursemodule = $coursemodule; @@ -193,6 +201,8 @@ class forum { $this->completionposts = $completionposts; $this->displaywordcount = $displaywordcount; $this->lockdiscussionafter = $lockdiscussionafter; + $this->duedate = $duedate; + $this->cutoffdate = $cutoffdate; } /** @@ -546,4 +556,66 @@ class forum { return (($discussion->get_time_modified() + $this->get_lock_discussions_after()) < time()); } + + /** + * Get the cutoff date. + * + * @return int + */ + public function get_cutoff_date() : int { + return $this->cutoffdate; + } + + /** + * Does the forum have a cutoff date? + * + * @return bool + */ + public function has_cutoff_date() : bool { + return !empty($this->get_cutoff_date()); + } + + /** + * Is the cutoff date for the forum reached? + * + * @return bool + */ + public function is_cutoff_date_reached() : bool { + if ($this->has_cutoff_date() && ($this->get_cutoff_date() < time())) { + return true; + } + + return false; + } + + /** + * Get the due date. + * + * @return int + */ + public function get_due_date() : int { + return $this->duedate; + } + + /** + * Does the forum have a due date? + * + * @return bool + */ + public function has_due_date() : bool { + return !empty($this->get_due_date()); + } + + /** + * Is the due date for the forum reached? + * + * @return bool + */ + public function is_due_date_reached() : bool { + if ($this->has_due_date() && ($this->get_due_date() < time())) { + return true; + } + + return false; + } } diff --git a/mod/forum/classes/local/factories/entity.php b/mod/forum/classes/local/factories/entity.php index b7f18379605..e3a322a672d 100644 --- a/mod/forum/classes/local/factories/entity.php +++ b/mod/forum/classes/local/factories/entity.php @@ -100,7 +100,9 @@ class entity { $record->completionreplies, $record->completionposts, $record->displaywordcount, - $record->lockdiscussionafter + $record->lockdiscussionafter, + $record->duedate, + $record->cutoffdate ); } diff --git a/mod/forum/classes/local/managers/capability.php b/mod/forum/classes/local/managers/capability.php index 23293e93473..8bc2046c7f1 100644 --- a/mod/forum/classes/local/managers/capability.php +++ b/mod/forum/classes/local/managers/capability.php @@ -110,6 +110,12 @@ class capability { return false; } + if ($this->forum->is_cutoff_date_reached()) { + if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) { + return false; + } + } + switch ($this->forum->get_type()) { case 'news': $capability = 'mod/forum:addnews'; diff --git a/mod/forum/classes/local/renderers/discussion.php b/mod/forum/classes/local/renderers/discussion.php index ca3e7db90d6..dcc59caffb0 100644 --- a/mod/forum/classes/local/renderers/discussion.php +++ b/mod/forum/classes/local/renderers/discussion.php @@ -377,6 +377,18 @@ class discussion { $forum = $this->forum; $renderer = $this->renderer; + if ($forum->is_cutoff_date_reached()) { + $notifications[] = (new notification( + get_string('cutoffdatereached', 'forum'), + notification::NOTIFY_INFO + ))->set_show_closebutton(); + } else if ($forum->has_due_date()) { + $notifications[] = (new notification( + get_string('thisforumhasduedate', 'forum', userdate($forum->get_due_date())), + notification::NOTIFY_INFO + ))->set_show_closebutton(); + } + if ($forum->is_discussion_locked($discussion)) { $notifications[] = (new notification( get_string('discussionlocked', 'forum'), diff --git a/mod/forum/classes/local/renderers/discussion_list.php b/mod/forum/classes/local/renderers/discussion_list.php index 7b7f0e02bae..f62a76986b4 100644 --- a/mod/forum/classes/local/renderers/discussion_list.php +++ b/mod/forum/classes/local/renderers/discussion_list.php @@ -327,6 +327,18 @@ class discussion_list { $renderer = $this->renderer; $capabilitymanager = $this->capabilitymanager; + if ($forum->is_cutoff_date_reached()) { + $notifications[] = (new notification( + get_string('cutoffdatereached', 'forum'), + notification::NOTIFY_INFO + ))->set_show_closebutton(); + } else if ($forum->has_due_date()) { + $notifications[] = (new notification( + get_string('thisforumhasduedate', 'forum', userdate($forum->get_due_date())), + notification::NOTIFY_INFO + ))->set_show_closebutton(); + } + if ($forum->has_blocking_enabled()) { $notifications[] = (new notification( get_string('thisforumisthrottled', 'forum', [ diff --git a/mod/forum/db/access.php b/mod/forum/db/access.php index 5e5244dbd6c..b9d201542cd 100644 --- a/mod/forum/db/access.php +++ b/mod/forum/db/access.php @@ -395,5 +395,15 @@ $capabilities = array( 'manager' => CAP_ALLOW ) ), + 'mod/forum:canoverridecutoff' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'mod/forum:canoverridediscussionlock' + ), ); diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index 61494bf0dea..c898e519ff2 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -126,6 +126,7 @@ $string['couldnotupdate'] = 'Could not update your post due to an unknown error' $string['crontask'] = 'Forum mailings and maintenance jobs'; $string['cutoffdate'] = 'Cut-off date'; $string['cutoffdate_help'] = 'If set, the forum will not accept posts after this date.'; +$string['cutoffdatereached'] = 'The cut-off date for posting to this forum is reached so you can no longer post to it.'; $string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.'; $string['delete'] = 'Delete'; $string['deleteddiscussion'] = 'The discussion topic has been deleted'; @@ -235,6 +236,7 @@ $string['forum:addinstance'] = 'Add a new forum'; $string['forum:addnews'] = 'Add announcements'; $string['forum:addquestion'] = 'Add question'; $string['forum:allowforcesubscribe'] = 'Allow force subscribe'; +$string['forum:canoverridecutoff'] = 'Post to forums after their cut-off date'; $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.'; @@ -587,6 +589,7 @@ $string['subscriptions'] = 'Subscriptions'; $string['tagarea_forum_posts'] = 'Forum posts'; $string['tagsdeleted'] = 'Forum tags have been deleted'; $string['thisforumisthrottled'] = 'This forum has a limit to the number of forum postings you can make in a given time period - this is currently set at {$a->blockafter} posting(s) in {$a->blockperiod}'; +$string['thisforumhasduedate'] = 'The due date for posting to this forum is {$a}.'; $string['timedhidden'] = 'Timed status: Hidden from students'; $string['timedposts'] = 'Timed posts'; $string['timedvisible'] = 'Timed status: Visible to all users'; diff --git a/mod/forum/lib.php b/mod/forum/lib.php index cdecf8ae582..d7c8851b2ab 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -3703,6 +3703,12 @@ function forum_user_can_post_discussion($forum, $currentgroup=null, $unused=-1, $context = context_module::instance($cm->id); } + if (forum_is_cutoff_date_reached($forum)) { + if (!has_capability('mod/forum:canoverridecutoff', $context)) { + return false; + } + } + if ($currentgroup === null) { $currentgroup = groups_get_activity_group($cm); } @@ -3796,6 +3802,12 @@ function forum_user_can_post($forum, $discussion, $user=NULL, $cm=NULL, $course= $context = context_module::instance($cm->id); } + if (forum_is_cutoff_date_reached($forum)) { + if (!has_capability('mod/forum:canoverridecutoff', $context)) { + return false; + } + } + // Check whether the discussion is locked. if (forum_discussion_is_locked($forum, $discussion)) { if (!has_capability('mod/forum:canoverridediscussionlock', $context)) { @@ -6305,6 +6317,26 @@ function mod_forum_inplace_editable($itemtype, $itemid, $newvalue) { } } +/** + * Determine whether the specified forum's cutoff date is reached. + * + * @param stdClass $forum The forum + * @return bool + */ +function forum_is_cutoff_date_reached($forum) { + $entityfactory = \mod_forum\local\container::get_entity_factory(); + $coursemoduleinfo = get_fast_modinfo($forum->course); + $cminfo = $coursemoduleinfo->instances['forum'][$forum->id]; + $forumentity = $entityfactory->get_forum_from_stdclass( + $forum, + context_module::instance($cminfo->id), + $cminfo->get_course_module_record(), + $cminfo->get_course() + ); + + return $forumentity->is_cutoff_date_reached(); +} + /** * Determine whether the specified discussion is time-locked. * diff --git a/mod/forum/tests/entities_forum_test.php b/mod/forum/tests/entities_forum_test.php index 43071f911dd..0bbfb62efaf 100644 --- a/mod/forum/tests/entities_forum_test.php +++ b/mod/forum/tests/entities_forum_test.php @@ -92,6 +92,8 @@ class mod_forum_entities_forum_testcase extends advanced_testcase { $completionposts = 0; $displaywordcount = false; $lockdiscussionafter = 0; + $duedate = 0; + $cutoffdate = 0; $forum = new forum_entity( $context, @@ -122,7 +124,9 @@ class mod_forum_entities_forum_testcase extends advanced_testcase { $completionreplies, $completionposts, $displaywordcount, - $lockdiscussionafter + $lockdiscussionafter, + $duedate, + $cutoffdate ); $this->assertEquals($context, $forum->get_context()); @@ -160,5 +164,9 @@ class mod_forum_entities_forum_testcase extends advanced_testcase { $this->assertEquals($lockdiscussionafter, $forum->get_lock_discussions_after()); $this->assertEquals(false, $forum->has_lock_discussions_after()); $this->assertEquals(false, $forum->is_discussion_locked($discussion)); + $this->assertEquals(false, $forum->has_due_date()); + $this->assertEquals(false, $forum->is_due_date_reached()); + $this->assertEquals(false, $forum->has_cutoff_date()); + $this->assertEquals(false, $forum->is_cutoff_date_reached()); } } diff --git a/mod/forum/tests/externallib_test.php b/mod/forum/tests/externallib_test.php index 8fa9dceb49e..7017f17d5b8 100644 --- a/mod/forum/tests/externallib_test.php +++ b/mod/forum/tests/externallib_test.php @@ -1458,7 +1458,34 @@ class mod_forum_external_testcase extends externallib_advanced_testcase { $this->assertTrue($result['status']); $this->assertTrue($result['canpindiscussions']); $this->assertTrue($result['cancreateattachment']); + } + /* + * A basic test to make sure users cannot post to forum after the cutoff date. + */ + public function test_can_add_discussion_after_cutoff() { + $this->resetAfterTest(true); + + // Create courses to add the modules. + $course = self::getDataGenerator()->create_course(); + + $user = self::getDataGenerator()->create_user(); + + // Create a forum with cutoff date set to a past date. + $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]); + + // User with no mod/forum:canoverridecutoff capability. + self::setUser($user); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $result = mod_forum_external::can_add_discussion($forum->id); + $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); + $this->assertFalse($result['status']); + + self::setAdminUser(); + $result = mod_forum_external::can_add_discussion($forum->id); + $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result); + $this->assertTrue($result['status']); } /** diff --git a/mod/forum/tests/lib_test.php b/mod/forum/tests/lib_test.php index a6a8c8a4f9a..88f77327298 100644 --- a/mod/forum/tests/lib_test.php +++ b/mod/forum/tests/lib_test.php @@ -2200,7 +2200,7 @@ class mod_forum_lib_testcase extends advanced_testcase { * Test forum_user_can_post_discussion */ public function test_forum_user_can_post_discussion() { - global $CFG, $DB; + global $DB; $this->resetAfterTest(true); @@ -2297,6 +2297,40 @@ class mod_forum_lib_testcase extends advanced_testcase { $this->assertTrue($can); } + /** + * Test forum_user_can_post_discussion_after_cutoff + */ + public function test_forum_user_can_post_discussion_after_cutoff() { + $this->resetAfterTest(true); + + // Create course to add the module. + $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); + $student = self::getDataGenerator()->create_user(); + $teacher = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($student->id, $course->id); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + + // Forum forcing separate gropus. + $record = new stdClass(); + $record->course = $course->id; + $record->cutoffdate = time() - 1; + $forum = self::getDataGenerator()->create_module('forum', $record); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = context_module::instance($cm->id); + + self::setUser($student); + + // Students usually don't have the mod/forum:canoverridecutoff capability. + $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); + $this->assertFalse($can); + + self::setUser($teacher); + + // Teachers usually have the mod/forum:canoverridecutoff capability. + $can = forum_user_can_post_discussion($forum, null, -1, $cm, $context); + $this->assertTrue($can); + } + /** * Test forum_user_has_posted_discussion with no groups. */ @@ -3138,6 +3172,52 @@ class mod_forum_lib_testcase extends advanced_testcase { ]; } + /** + * Test the forum_is_cutoff_date_reached function. + * + * @dataProvider forum_is_cutoff_date_reached_provider + * @param array $forum + * @param bool $expect + */ + public function test_forum_is_cutoff_date_reached($forum, $expect) { + $this->resetAfterTest(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + $forum = $datagenerator->create_module('forum', (object) array_merge([ + 'course' => $course->id + ], $forum)); + + $this->assertEquals($expect, forum_is_cutoff_date_reached($forum)); + } + + /** + * Dataprovider for forum_is_cutoff_date_reached tests. + * + * @return array + */ + public function forum_is_cutoff_date_reached_provider() { + $now = time(); + return [ + 'cutoffdate is unset' => [ + [], + false + ], + 'cutoffdate is 0' => [ + ['cutoffdate' => 0], + false + ], + 'cutoffdate is set and is in future' => [ + ['cutoffdate' => $now + 86400], + false + ], + 'cutoffdate is set and is in past' => [ + ['cutoffdate' => $now - 86400], + true + ], + ]; + } + /** * Test that {@link forum_update_post()} keeps correct forum_discussions usermodified. */