diff --git a/lib/setuplib.php b/lib/setuplib.php index 98a8666ada1..dbfa05e5e59 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -1407,7 +1407,7 @@ function disable_output_buffering() { */ function is_major_upgrade_required() { global $CFG; - $lastmajordbchanges = 2019032900.00; + $lastmajordbchanges = 2019041000.03; $required = empty($CFG->version); $required = $required || (float)$CFG->version < $lastmajordbchanges; diff --git a/mod/forum/backup/moodle2/backup_forum_stepslib.php b/mod/forum/backup/moodle2/backup_forum_stepslib.php index 63f45d9c6eb..7bc891faeb6 100644 --- a/mod/forum/backup/moodle2/backup_forum_stepslib.php +++ b/mod/forum/backup/moodle2/backup_forum_stepslib.php @@ -39,7 +39,7 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste // Define each element separated $forum = new backup_nested_element('forum', array('id'), array( - 'type', 'name', 'intro', 'introformat', + 'type', 'name', 'intro', 'introformat', 'duedate', 'cutoffdate', 'assessed', 'assesstimestart', 'assesstimefinish', 'scale', 'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype', 'rsstype', 'rssarticles', 'timemodified', 'warnafter', diff --git a/mod/forum/backup/moodle2/restore_forum_stepslib.php b/mod/forum/backup/moodle2/restore_forum_stepslib.php index 771fd41af07..855e25c2c6f 100644 --- a/mod/forum/backup/moodle2/restore_forum_stepslib.php +++ b/mod/forum/backup/moodle2/restore_forum_stepslib.php @@ -62,6 +62,14 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. + if (!isset($data->duedate)) { + $data->duedate = 0; + } + $data->duedate = $this->apply_date_offset($data->duedate); + if (!isset($data->cutoffdate)) { + $data->cutoffdate = 0; + } + $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); if ($data->scale < 0) { // scale found, get mapping 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..500bc75e8f8 100644 --- a/mod/forum/classes/local/renderers/discussion.php +++ b/mod/forum/classes/local/renderers/discussion.php @@ -377,6 +377,23 @@ 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->is_due_date_reached()) { + $notifications[] = (new notification( + get_string('thisforumisdue', 'forum', userdate($forum->get_due_date())), + 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..cd4ce425ef4 100644 --- a/mod/forum/classes/local/renderers/discussion_list.php +++ b/mod/forum/classes/local/renderers/discussion_list.php @@ -327,6 +327,23 @@ 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->is_due_date_reached()) { + $notifications[] = (new notification( + get_string('thisforumisdue', 'forum', userdate($forum->get_due_date())), + 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/db/install.xml b/mod/forum/db/install.xml index 3c3cf624073..bf593194367 100644 --- a/mod/forum/db/install.xml +++ b/mod/forum/db/install.xml @@ -1,5 +1,5 @@ - @@ -12,6 +12,8 @@ + + diff --git a/mod/forum/db/upgrade.php b/mod/forum/db/upgrade.php index 978e8c95d74..9127de4fe96 100644 --- a/mod/forum/db/upgrade.php +++ b/mod/forum/db/upgrade.php @@ -43,7 +43,7 @@ defined('MOODLE_INTERNAL') || die(); function xmldb_forum_upgrade($oldversion) { - global $CFG, $DB; + global $DB; $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes. @@ -118,5 +118,29 @@ function xmldb_forum_upgrade($oldversion) { upgrade_mod_savepoint(true, 2019031200, 'forum'); } + if ($oldversion < 2019040400) { + + $table = new xmldb_table('forum'); + + // Define field duedate to be added to forum. + $field = new xmldb_field('duedate', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'introformat'); + + // Conditionally launch add field duedate. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field cutoffdate to be added to forum. + $field = new xmldb_field('cutoffdate', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'duedate'); + + // Conditionally launch add field cutoffdate. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Forum savepoint reached. + upgrade_mod_savepoint(true, 2019040400, 'forum'); + } + return true; } diff --git a/mod/forum/externallib.php b/mod/forum/externallib.php index ba39260253d..a53e2c41ad9 100644 --- a/mod/forum/externallib.php +++ b/mod/forum/externallib.php @@ -127,6 +127,8 @@ class mod_forum_external extends external_api { 'intro' => new external_value(PARAM_RAW, 'The forum intro'), 'introformat' => new external_format_value('intro'), 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL), + 'duedate' => new external_value(PARAM_INT, 'duedate for the user', VALUE_OPTIONAL), + 'cutoffdate' => new external_value(PARAM_INT, 'cutoffdate for the user', VALUE_OPTIONAL), 'assessed' => new external_value(PARAM_INT, 'Aggregate type'), 'assesstimestart' => new external_value(PARAM_INT, 'Assess start time'), 'assesstimefinish' => new external_value(PARAM_INT, 'Assess finish time'), diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index 05c95c52f7c..7ae8c1c2886 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -43,6 +43,7 @@ $string['attachment_help'] = 'You can optionally attach one or more files to a f $string['attachmentnopost'] = 'You cannot export attachments without a post id'; $string['attachments'] = 'Attachments'; $string['attachmentswordcount'] = 'Attachments and word count'; +$string['availability'] = 'Availability'; $string['blockafter'] = 'Post threshold for blocking'; $string['blockafter_help'] = 'This setting specifies the maximum number of posts which a user can post in the given time period. Users with the capability mod/forum:postwithoutthrottling are exempt from post limits.'; $string['blockperiod'] = 'Time period for blocking'; @@ -51,6 +52,7 @@ $string['blockperioddisabled'] = 'Don\'t block'; $string['blogforum'] = 'Standard forum displayed in a blog-like format'; $string['bynameondate'] = 'by {$a->name} - {$a->date}'; $string['cachedef_forum_is_tracked'] = 'Forum tracking status for user'; +$string['calendardue'] = '{$a} is due'; $string['cannotadd'] = 'Could not add the discussion for this forum'; $string['cannotadddiscussion'] = 'Adding discussions to this forum requires group membership.'; $string['cannotadddiscussionall'] = 'You do not have permission to add a new discussion topic for all participants.'; @@ -122,6 +124,10 @@ $string['couldnotadd'] = 'Could not add your post due to an unknown error'; $string['couldnotdeletereplies'] = 'Sorry, that cannot be deleted as people have already responded to it'; $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'; $string['deletedpost'] = 'The post has been deleted'; @@ -170,6 +176,9 @@ $string['displaystart'] = 'Display start'; $string['displaystart_help'] = 'This setting specifies whether a forum post should be displayed from a certain date. Note that administrators can always view forum posts.'; $string['displaywordcount'] = 'Display word count'; $string['displaywordcount_help'] = 'This setting specifies whether the word count of each post should be displayed or not.'; +$string['duedate'] = 'Due date'; +$string['duedate_help'] = 'This is when the forum is due. Although this date is displayed on the calendar as the due date for the forum, posting to the forum will still be allowed after this date. To prevent posting to the forum after a certain date - set the forum cut off date.'; +$string['duedatetodisplayincalendar'] = 'Due date to display in calendar'; $string['eachuserforum'] = 'Each person posts one discussion'; $string['edit'] = 'Edit'; $string['editedby'] = 'Edited by {$a->name} - original submission {$a->date}'; @@ -227,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.'; @@ -579,6 +589,8 @@ $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['thisforumisdue'] = 'The due date for posting to this forum was {$a}.'; +$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 361c6e70be4..e1b7a826d82 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -86,6 +86,8 @@ define('FORUM_DISCUSSION_UNPINNED', 0); function forum_add_instance($forum, $mform = null) { global $CFG, $DB; + require_once($CFG->dirroot.'/mod/forum/locallib.php'); + $forum->timemodified = time(); if (empty($forum->assessed)) { @@ -127,6 +129,7 @@ function forum_add_instance($forum, $mform = null) { } } + forum_update_calendar($forum, $forum->coursemodule); forum_grade_item_update($forum); $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null; @@ -162,7 +165,9 @@ function forum_instance_created($context, $forum) { * @return bool success */ function forum_update_instance($forum, $mform) { - global $DB, $OUTPUT, $USER; + global $CFG, $DB, $OUTPUT, $USER; + + require_once($CFG->dirroot.'/mod/forum/locallib.php'); $forum->timemodified = time(); $forum->id = $forum->instance; @@ -249,6 +254,7 @@ function forum_update_instance($forum, $mform) { } } + forum_update_calendar($forum, $forum->coursemodule); forum_grade_item_update($forum); $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null; @@ -3697,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); } @@ -3790,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)) { @@ -6299,6 +6317,46 @@ 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 forum's due date is reached. + * + * @param stdClass $forum The forum + * @return bool + */ +function forum_is_due_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_due_date_reached(); +} + /** * Determine whether the specified discussion is time-locked. * @@ -6591,3 +6649,98 @@ function forum_user_can_reply_privately(\context_module $context, \stdClass $par return has_capability('mod/forum:postprivatereply', $context); } + +/** + * This function calculates the minimum and maximum cutoff values for the timestart of + * the given event. + * + * It will return an array with two values, the first being the minimum cutoff value and + * the second being the maximum cutoff value. Either or both values can be null, which + * indicates there is no minimum or maximum, respectively. + * + * If a cutoff is required then the function must return an array containing the cutoff + * timestamp and error string to display to the user if the cutoff value is violated. + * + * A minimum and maximum cutoff return value will look like: + * [ + * [1505704373, 'The date must be after this date'], + * [1506741172, 'The date must be before this date'] + * ] + * + * @param calendar_event $event The calendar event to get the time range for + * @param stdClass $forum The module instance to get the range from + * @return array Returns an array with min and max date. + */ +function mod_forum_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $forum) { + global $CFG; + + require_once($CFG->dirroot . '/mod/forum/locallib.php'); + + $mindate = null; + $maxdate = null; + + if ($event->eventtype == FORUM_EVENT_TYPE_DUE) { + if (!empty($forum->cutoffdate)) { + $maxdate = [ + $forum->cutoffdate, + get_string('cutoffdatevalidation', 'forum'), + ]; + } + } + + return [$mindate, $maxdate]; +} + +/** + * This function will update the forum module according to the + * event that has been modified. + * + * It will set the timeclose value of the forum instance + * according to the type of event provided. + * + * @throws \moodle_exception + * @param \calendar_event $event + * @param stdClass $forum The module instance to get the range from + */ +function mod_forum_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $forum) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/mod/forum/locallib.php'); + + if ($event->eventtype != FORUM_EVENT_TYPE_DUE) { + return; + } + + $courseid = $event->courseid; + $modulename = $event->modulename; + $instanceid = $event->instance; + + // Something weird going on. The event is for a different module so + // we should ignore it. + if ($modulename != 'forum') { + return; + } + + if ($forum->id != $instanceid) { + return; + } + + $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; + $context = context_module::instance($coursemodule->id); + + // The user does not have the capability to modify this activity. + if (!has_capability('moodle/course:manageactivities', $context)) { + return; + } + + if ($event->eventtype == FORUM_EVENT_TYPE_DUE) { + if ($forum->duedate != $event->timestart) { + $forum->duedate = $event->timestart; + $forum->timemodified = time(); + // Persist the instance changes. + $DB->update_record('forum', $forum); + $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); + $event->trigger(); + } + } +} diff --git a/mod/forum/locallib.php b/mod/forum/locallib.php index 6f57004d28c..ab333a993ae 100644 --- a/mod/forum/locallib.php +++ b/mod/forum/locallib.php @@ -1,5 +1,4 @@ dirroot . '/mod/forum/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); @@ -700,3 +706,50 @@ function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } + +/** + * Update the calendar entries for this forum activity. + * + * @param stdClass $forum the row from the database table forum. + * @param int $cmid The coursemodule id + * @return bool + */ +function forum_update_calendar($forum, $cmid) { + global $DB, $CFG; + + require_once($CFG->dirroot.'/calendar/lib.php'); + + $event = new stdClass(); + + if (!empty($forum->duedate)) { + $event->name = get_string('calendardue', 'forum', $forum->name); + $event->description = format_module_intro('forum', $forum, $cmid); + $event->courseid = $forum->course; + $event->modulename = 'forum'; + $event->instance = $forum->id; + $event->type = CALENDAR_EVENT_TYPE_ACTION; + $event->eventtype = FORUM_EVENT_TYPE_DUE; + $event->timestart = $forum->duedate; + $event->timesort = $forum->duedate; + $event->visible = instance_is_visible('forum', $forum); + } + + $event->id = $DB->get_field('event', 'id', + array('modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE)); + + if ($event->id) { + $calendarevent = calendar_event::load($event->id); + if (!empty($forum->duedate)) { + // Calendar event exists so update it. + $calendarevent->update($event); + } else { + // Calendar event is no longer needed. + $calendarevent->delete(); + } + } else if (!empty($forum->duedate)) { + // Event doesn't exist so create one. + calendar_event::create($event); + } + + return true; +} \ No newline at end of file diff --git a/mod/forum/mod_form.php b/mod/forum/mod_form.php index decea410987..5a43c363eb6 100644 --- a/mod/forum/mod_form.php +++ b/mod/forum/mod_form.php @@ -54,6 +54,16 @@ class mod_forum_mod_form extends moodleform_mod { $mform->addHelpButton('type', 'forumtype', 'forum'); $mform->setDefault('type', 'general'); + $mform->addElement('header', 'availability', get_string('availability', 'forum')); + + $name = get_string('duedate', 'forum'); + $mform->addElement('date_time_selector', 'duedate', $name, array('optional' => true)); + $mform->addHelpButton('duedate', 'duedate', 'forum'); + + $name = get_string('cutoffdate', 'forum'); + $mform->addElement('date_time_selector', 'cutoffdate', $name, array('optional' => true)); + $mform->addHelpButton('cutoffdate', 'cutoffdate', 'forum'); + // Attachments and word count. $mform->addElement('header', 'attachmentswordcounthdr', get_string('attachmentswordcount', 'forum')); @@ -229,6 +239,18 @@ class mod_forum_mod_form extends moodleform_mod { } + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($data['duedate'] && $data['cutoffdate']) { + if ($data['duedate'] > $data['cutoffdate']) { + $errors['cutoffdate'] = get_string('cutoffdatevalidation', 'forum'); + } + } + + return $errors; + } + function data_preprocessing(&$default_values) { parent::data_preprocessing($default_values); 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..80e27732d16 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,100 @@ 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 the forum_is_due_date_reached function. + * + * @dataProvider forum_is_due_date_reached_provider + * @param stdClass $forum + * @param bool $expect + */ + public function test_forum_is_due_date_reached($forum, $expect) { + $this->resetAfterTest(); + + $this->setAdminUser(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + $forum = $datagenerator->create_module('forum', (object) array_merge([ + 'course' => $course->id + ], $forum)); + + $this->assertEquals($expect, forum_is_due_date_reached($forum)); + } + + /** + * Dataprovider for forum_is_due_date_reached tests. + * + * @return array + */ + public function forum_is_due_date_reached_provider() { + $now = time(); + return [ + 'duedate is unset' => [ + [], + false + ], + 'duedate is 0' => [ + ['duedate' => 0], + false + ], + 'duedate is set and is in future' => [ + ['duedate' => $now + 86400], + false + ], + 'duedate is set and is in past' => [ + ['duedate' => $now - 86400], + true + ], + ]; + } + /** * Test that {@link forum_update_post()} keeps correct forum_discussions usermodified. */ @@ -3639,4 +3767,200 @@ class mod_forum_lib_testcase extends advanced_testcase { $this->setUser($otheruser->id); $this->assertFalse(forum_post_is_visible_privately($post, $cm)); } + + /** + * An unkown event type should not have any limits + */ + public function test_mod_forum_core_calendar_get_valid_event_timestart_range_unknown_event() { + global $CFG; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $duedate = time() + DAYSECS; + $forum = new \stdClass(); + $forum->duedate = $duedate; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'forum', + 'instance' => 1, + 'eventtype' => FORUM_EVENT_TYPE_DUE . "SOMETHING ELSE", + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + list ($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * Forums configured without a cutoff date should not have any limits applied. + */ + public function test_mod_forum_core_calendar_get_valid_event_timestart_range_due_no_limit() { + global $CFG; + require_once($CFG->dirroot . '/calendar/lib.php'); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $duedate = time() + DAYSECS; + $forum = new \stdClass(); + $forum->duedate = $duedate; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'forum', + 'instance' => 1, + 'eventtype' => FORUM_EVENT_TYPE_DUE, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + list($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * Forums should be top bound by the cutoff date. + */ + public function test_mod_forum_core_calendar_get_valid_event_timestart_range_due_with_limits() { + global $CFG; + require_once($CFG->dirroot . '/calendar/lib.php'); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $duedate = time() + DAYSECS; + $cutoffdate = $duedate + DAYSECS; + $forum = new \stdClass(); + $forum->duedate = $duedate; + $forum->cutoffdate = $cutoffdate; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'forum', + 'instance' => 1, + 'eventtype' => FORUM_EVENT_TYPE_DUE, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + list($min, $max) = mod_forum_core_calendar_get_valid_event_timestart_range($event, $forum); + $this->assertNull($min); + $this->assertEquals($cutoffdate, $max[0]); + $this->assertNotEmpty($max[1]); + } + + /** + * An unknown event type should not change the forum instance. + */ + public function test_mod_forum_core_calendar_event_timestart_updated_unknown_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $forumgenerator = $generator->get_plugin_generator('mod_forum'); + $duedate = time() + DAYSECS; + $cutoffdate = $duedate + DAYSECS; + $forum = $forumgenerator->create_instance(['course' => $course->id]); + $forum->duedate = $duedate; + $forum->cutoffdate = $cutoffdate; + $DB->update_record('forum', $forum); + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'forum', + 'instance' => $forum->id, + 'eventtype' => FORUM_EVENT_TYPE_DUE . "SOMETHING ELSE", + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + mod_forum_core_calendar_event_timestart_updated($event, $forum); + + $forum = $DB->get_record('forum', ['id' => $forum->id]); + $this->assertEquals($duedate, $forum->duedate); + $this->assertEquals($cutoffdate, $forum->cutoffdate); + } + + /** + * Due date events should update the forum due date. + */ + public function test_mod_forum_core_calendar_event_timestart_updated_due_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $forumgenerator = $generator->get_plugin_generator('mod_forum'); + $duedate = time() + DAYSECS; + $cutoffdate = $duedate + DAYSECS; + $newduedate = $duedate + 1; + $forum = $forumgenerator->create_instance(['course' => $course->id]); + $forum->duedate = $duedate; + $forum->cutoffdate = $cutoffdate; + $DB->update_record('forum', $forum); + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'forum', + 'instance' => $forum->id, + 'eventtype' => FORUM_EVENT_TYPE_DUE, + 'timestart' => $newduedate, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + mod_forum_core_calendar_event_timestart_updated($event, $forum); + + $forum = $DB->get_record('forum', ['id' => $forum->id]); + $this->assertEquals($newduedate, $forum->duedate); + $this->assertEquals($cutoffdate, $forum->cutoffdate); + } } diff --git a/mod/forum/tests/locallib_test.php b/mod/forum/tests/locallib_test.php new file mode 100644 index 00000000000..4a03a0d1882 --- /dev/null +++ b/mod/forum/tests/locallib_test.php @@ -0,0 +1,73 @@ +. + +/** + * File containing the forum module local library function tests. + * + * @package mod_forum + * @category test + * @copyright 2018 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/mod/forum/lib.php'); + +/** + * Class mod_forum_locallib_testcase. + * + * @copyright 2018 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_forum_locallib_testcase extends advanced_testcase { + public function test_forum_update_calendar() { + global $DB; + + $this->resetAfterTest(); + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create a forum activity. + $time = time(); + $forum = $this->getDataGenerator()->create_module('forum', + array( + 'course' => $course->id, + 'duedate' => $time + ) + ); + + // Check that there is now an event in the database. + $events = $DB->get_records('event'); + $this->assertCount(1, $events); + + // Get the event. + $event = reset($events); + + // Confirm the event is correct. + $this->assertEquals('forum', $event->modulename); + $this->assertEquals($forum->id, $event->instance); + $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); + $this->assertEquals(FORUM_EVENT_TYPE_DUE, $event->eventtype); + $this->assertEquals($time, $event->timestart); + $this->assertEquals($time, $event->timesort); + } +} \ No newline at end of file diff --git a/mod/forum/upgrade.txt b/mod/forum/upgrade.txt index 0eba55f6a05..f275f7434f7 100644 --- a/mod/forum/upgrade.txt +++ b/mod/forum/upgrade.txt @@ -9,6 +9,8 @@ information provided here is intended especially for developers. * The get_forum_discussion_posts web service has been deprecated in favour of get_discussion_posts. * The forum_count_replies function has been deprecated in favour of get_reply_count_for_post_id_in_discussion_id in the Post vault. + * External function get_forums_by_courses now returns two additional fields "duedate" and "cutoffdate" containing the due date and the cutoff date + for posting to the forums respectively. === 3.6 === diff --git a/mod/forum/version.php b/mod/forum/version.php index 52f7fce43dd..b84b45c4259 100644 --- a/mod/forum/version.php +++ b/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019031300; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2019040400; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2018112800; // Requires this Moodle version $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics) diff --git a/version.php b/version.php index ac26ccf5c8d..03f5cad0a38 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2019041000.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2019041000.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.