MDL-60058 assign: allow update of assign calendar action events
authorRyan Wyllie <ryan@moodle.com>
Mon, 18 Sep 2017 03:53:36 +0000 (03:53 +0000)
committerRyan Wyllie <ryan@moodle.com>
Fri, 13 Oct 2017 06:18:43 +0000 (06:18 +0000)
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/lib_test.php

index 7572645..34accd3 100644 (file)
@@ -1920,3 +1920,147 @@ function mod_assign_core_calendar_event_action_shows_item_count(calendar_event $
     // For mod_assign, item count should be shown if the event type is 'gradingdue' and there is one or more item count.
     return in_array($event->eventtype, $eventtypesshowingitemcount) && $itemcount > 0;
 }
+
+/**
+ * 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 due date must be after the sbumission start date'],
+ *     [1506741172, 'The due date must be before the cutoff date']
+ * ]
+ *
+ * @param calendar_event $event The calendar event to get the time range for
+ * @param stdClass|null $instance The module instance to get the range from
+ */
+function mod_assign_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance = null) {
+    global $DB;
+
+    if (!$instance) {
+        $instance = $DB->get_record('assign', ['id' => $event->instance]);
+    }
+
+    $coursemodule = get_coursemodule_from_instance('assign',
+                                         $event->instance,
+                                         $event->courseid,
+                                         false,
+                                         MUST_EXIST);
+
+    if (empty($coursemodule)) {
+        // If we don't have a course module yet then it likely means
+        // the activity is still being set up. In this case there is
+        // nothing for us to do anyway.
+        return;
+    }
+
+    $context = context_module::instance($coursemodule->id);
+    $assign = new assign($context, null, null);
+    $assign->set_instance($instance);
+
+    return $assign->get_valid_calendar_event_timestart_range($event);
+}
+
+/**
+ * This function will check that the given event is valid for it's
+ * corresponding assign module instance.
+ *
+ * An exception is thrown if the event fails validation.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @return bool
+ */
+function mod_assign_core_calendar_validate_event_timestart(\calendar_event $event) {
+    global $DB;
+
+    if (!isset($event->instance)) {
+        return;
+    }
+
+    // We need to read from the DB directly because course module may
+    // currently be getting created so it won't be in mod info yet.
+    $instance = $DB->get_record('assign', ['id' => $event->instance], '*', MUST_EXIST);
+    $timestart = $event->timestart;
+    list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance);
+
+    if ($min && $timestart < $min[0]) {
+        throw new \moodle_exception($min[1]);
+    }
+
+    if ($max && $timestart > $max[0]) {
+        throw new \moodle_exception($max[1]);
+    }
+}
+
+/**
+ * This function will update the assign module according to the
+ * event that has been modified.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ */
+function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event) {
+    global $DB;
+
+    if (empty($event->instance) || $event->modulename != 'assign') {
+        return;
+    }
+
+    $coursemodule = get_coursemodule_from_instance('assign',
+                                         $event->instance,
+                                         $event->courseid,
+                                         false,
+                                         MUST_EXIST);
+
+    if (empty($coursemodule)) {
+        // If we don't have a course module yet then it likely means
+        // the activity is still being set up. In this case there is
+        // nothing for us to do anyway.
+        return;
+    }
+
+    $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;
+    }
+
+    $assign = new assign($context, $coursemodule, null);
+    $modified = false;
+
+    if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
+        // This check is in here because due date events are currently
+        // the only events that can be overridden, so we can save a DB
+        // query if we don't bother checking other events.
+        if ($assign->is_override_calendar_event($event)) {
+            // This is an override event so we should ignore it.
+            return;
+        }
+
+        $instance = $assign->get_instance();
+        $newduedate = $event->timestart;
+
+        if ($newduedate != $instance->duedate) {
+            $instance->duedate = $newduedate;
+            $instance->timemodified = time();
+            $modified = true;
+        }
+    }
+
+    if ($modified) {
+        // Persist the assign instance changes.
+        $DB->update_record('assign', $instance);
+        $assign->update_calendar($coursemodule->id);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
+}
index 0ae808e..4f84e77 100644 (file)
@@ -943,6 +943,127 @@ class assign {
         );
     }
 
+    /**
+     * Check if the given calendar_event is either a user or group override
+     * event.
+     *
+     * @return bool
+     */
+    public function is_override_calendar_event(\calendar_event $event) {
+        global $DB;
+
+        if (!isset($event->modulename)) {
+            return false;
+        }
+
+        if ($event->modulename != 'assign') {
+            return false;
+        }
+
+        if (!isset($event->instance)) {
+            return false;
+        }
+
+        if (!isset($event->userid) && !isset($event->groupid)) {
+            return false;
+        }
+
+        $overrideparams = [
+            'assignid' => $event->instance
+        ];
+
+        if (isset($event->groupid)) {
+            $overrideparams['groupid'] = $event->groupid;
+        } else if (isset($event->userid)) {
+            $overrideparams['userid'] = $event->userid;
+        }
+
+        if ($DB->get_record('assign_overrides', $overrideparams)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 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 due date must be after the sbumission start date'],
+     *     [1506741172, 'The due date must be before the cutoff date']
+     * ]
+     *
+     * @param calendar_event $event The calendar event to get the time range for
+     */
+    function get_valid_calendar_event_timestart_range(\calendar_event $event) {
+        $instance = $this->get_instance();
+        $submissionsfromdate = $instance->allowsubmissionsfromdate;
+        $cutoffdate = $instance->cutoffdate;
+        $duedate = $instance->duedate;
+        $gradingduedate = $instance->gradingduedate;
+        $mindate = null;
+        $maxdate = null;
+
+        if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
+            // This check is in here because due date events are currently
+            // the only events that can be overridden, so we can save a DB
+            // query if we don't bother checking other events.
+            if ($this->is_override_calendar_event($event)) {
+                // This is an override event so we should ignore it.
+                return [null, null];
+            }
+
+            if ($submissionsfromdate) {
+                $mindate = [
+                    $submissionsfromdate,
+                    get_string('duedatevalidation', 'assign'),
+                ];
+            }
+
+            if ($cutoffdate) {
+                $maxdate = [
+                    $cutoffdate,
+                    get_string('cutoffdatevalidation', 'assign'),
+                ];
+            }
+
+            if ($gradingduedate) {
+                // If we don't have a cutoff date or we've got a grading due date
+                // that is earlier than the cutoff then we should use that as the
+                // upper limit for the due date.
+                if (!$cutoffdate || $gradingduedate < $cutoffdate) {
+                    $maxdate = [
+                        $gradingduedate,
+                        get_string('gradingdueduedatevalidation', 'assign'),
+                    ];
+                }
+            }
+        } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
+            if ($duedate) {
+                $mindate = [
+                    $duedate,
+                    get_string('gradingdueduedatevalidation', 'assign'),
+                ];
+            } else if ($submissionsfromdate) {
+                $mindate = [
+                    $submissionsfromdate,
+                    get_string('gradingduefromdatevalidation', 'assign'),
+                ];
+            }
+        }
+
+        return [$mindate, $maxdate];
+    }
+
     /**
      * Actual implementation of the reset course functionality, delete all the
      * assignment submissions for course $data->courseid.
index c59504f..976d182 100644 (file)
@@ -31,6 +31,9 @@ require_once($CFG->dirroot . '/mod/assign/lib.php');
 require_once($CFG->dirroot . '/mod/assign/locallib.php');
 require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
 
+use \core_calendar\local\api as calendar_local_api;
+use \core_calendar\local\event\container as calendar_event_container;
+
 /**
  * Unit tests for (some of) mod/assign/lib.php.
  *
@@ -684,4 +687,866 @@ class mod_assign_lib_testcase extends mod_assign_base_testcase {
         $this->assertEquals($student0grade->grade, 5);
         $this->assertEquals($student1grade->grade, ASSIGN_GRADE_NOT_SET);
     }
+
+    /**
+     * Return false when there are not overrides for this assign instance.
+     */
+    public function test_assign_is_override_calendar_event_no_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $userid = 1234;
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+
+        $instance = $assign->get_instance();
+        $event = new \calendar_event((object)[
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid
+        ]);
+
+        $this->assertFalse($assign->is_override_calendar_event($event));
+    }
+
+    /**
+     * Return false if the given event isn't an assign module event.
+     */
+    public function test_assign_is_override_calendar_event_no_nodule_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $userid = $this->students[0]->id;
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+
+        $instance = $assign->get_instance();
+        $event = new \calendar_event((object)[
+            'userid' => $userid
+        ]);
+
+        $this->assertFalse($assign->is_override_calendar_event($event));
+    }
+
+    /**
+     * Return false if there is overrides for this use but they belong to another assign
+     * instance.
+     */
+    public function test_assign_is_override_calendar_event_different_assign_instance() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $userid = 1234;
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+        $assign2 = $this->create_instance(['duedate' => $duedate]);
+
+        $instance = $assign->get_instance();
+        $event = new \calendar_event((object) [
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid
+        ]);
+
+        $record = (object) [
+            'assignid' => $assign2->get_instance()->id,
+            'userid' => $userid
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        $this->assertFalse($assign->is_override_calendar_event($event));
+    }
+
+    /**
+     * Return true if there is a user override for this event and assign instance.
+     */
+    public function test_assign_is_override_calendar_event_user_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $userid = 1234;
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+
+        $instance = $assign->get_instance();
+        $event = new \calendar_event((object) [
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid
+        ]);
+
+        $record = (object) [
+            'assignid' => $instance->id,
+            'userid' => $userid
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        $this->assertTrue($assign->is_override_calendar_event($event));
+    }
+
+    /**
+     * Return true if there is a group override for the event and assign instance.
+     */
+    public function test_assign_is_override_calendar_event_group_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+        $instance = $assign->get_instance();
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $instance->course));
+        $groupid = $group->id;
+
+        $event = new \calendar_event((object) [
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'groupid' => $groupid
+        ]);
+
+        $record = (object) [
+            'assignid' => $instance->id,
+            'groupid' => $groupid
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        $this->assertTrue($assign->is_override_calendar_event($event));
+    }
+
+    /**
+     * Unknown event types should not have any limit restrictions returned.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => 'SOME RANDOM EVENT'
+        ]);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * Override events should not have any limit restrictions returned.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $assign = $this->create_instance(['duedate' => $duedate]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        $record = (object) [
+            'assignid' => $instance->id,
+            'userid' => $userid
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * Assignments configured without a submissions from and cutoff date should not have
+     * any limits applied.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => 0,
+            'cutoffdate' => 0,
+        ]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * Assignments should be bottom and top bound by the submissions from date and cutoff date
+     * respectively.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_with_limits() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertEquals($submissionsfromdate, $min[0]);
+        $this->assertNotEmpty($min[1]);
+        $this->assertEquals($cutoffdate, $max[0]);
+        $this->assertNotEmpty($max[1]);
+    }
+
+    /**
+     * Assignment grading due date should not have any limits of no due date and cutoff date is set.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $assign = $this->create_instance([
+            'duedate' => 0,
+            'allowsubmissionsfromdate' => 0,
+            'cutoffdate' => 0,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
+        ]);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * Assignment grading due event is minimum bound by the due date, if it is set.
+     */
+    public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $assign = $this->create_instance([
+            'duedate' => $duedate
+        ]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE
+        ]);
+
+        list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertEquals($duedate, $min[0]);
+        $this->assertNotEmpty($min[1]);
+        $this->assertNull($max);
+    }
+
+    /**
+     * Calendar events without and instance id should be ignored by the validate
+     * event function.
+     */
+    public function test_mod_assign_core_calendar_validate_event_timestart_no_instance_id() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $event = new \calendar_event((object) [
+            'modulename' => 'assign',
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        mod_assign_core_calendar_validate_event_timestart($event);
+        // The function above throws an exception so all we need to do is make sure
+        // it gets here and that is considered success.
+        $this->assertTrue(true);
+    }
+
+    /**
+     * Calendar events for an unknown instance should throw an exception.
+     */
+    public function test_mod_assign_core_calendar_validate_event_timestart_no_instance_found() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $event = new \calendar_event((object) [
+            'modulename' => 'assign',
+            'instance' => 1234,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_assign_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * Assignments configured without any limits on the due date should not
+     * throw an exception.
+     */
+    public function test_mod_assign_core_calendar_validate_event_timestart_no_limit() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $assign = $this->create_instance([
+            'duedate' => 0,
+            'allowsubmissionsfromdate' => 0,
+            'cutoffdate' => 0,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => time()
+        ]);
+
+        mod_assign_core_calendar_validate_event_timestart($event);
+        // The function above throws an exception so all we need to do is make sure
+        // it gets here and that is considered success.
+        $this->assertTrue(true);
+    }
+
+    /**
+     * Due date events with a timestart equal to or greater than the minimum limit
+     * should not throw an exception. Timestart values below the minimum limit should
+     * throw an exception.
+     */
+    public function test_mod_assign_core_calendar_validate_due_event_min_limit() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => $submissionsfromdate + 1,
+        ]);
+
+        // No exception when new time is above minimum cutoff.
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // No exception when new time is equal to minimum cutoff.
+        $event->timestart = $submissionsfromdate;
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // Exception when new time is earlier than minimum cutoff.
+        $event->timestart = $submissionsfromdate - 1;
+        $this->expectException('moodle_exception');
+        mod_assign_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * A due date event with a timestart less than or equal to the max limit should
+     * not throw an exception. A timestart greater than the max limit should throw
+     * an exception.
+     */
+    public function test_mod_assign_core_calendar_validate_due_event_max_limit() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => $cutoffdate - 1,
+        ]);
+
+        // No exception when new time is below maximum cutoff.
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // No exception when new time is equal to maximum cutoff.
+        $event->timestart = $cutoffdate;
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // Exception when new time is later than maximum cutoff.
+        $event->timestart = $submissionsfromdate - 1;
+        $this->expectException('moodle_exception');
+        mod_assign_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * Due date override events should not throw an exception.
+     */
+    public function test_mod_assign_core_calendar_validate_due_event_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => $duedate + (2 * DAYSECS)
+        ]);
+
+        $record = (object) [
+            'assignid' => $instance->id,
+            'userid' => $userid,
+            'duedate' => $duedate + (2 * DAYSECS)
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        // No exception when dealing with an override.
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+    }
+
+    /**
+     * Grading due date event should throw an exception if it's timestart is less than the
+     * assignment due date.
+     */
+    public function test_mod_assign_core_calendar_validate_gradingdue_event_min_limit_duedate() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
+            'timestart' => $duedate + 1,
+        ]);
+
+        // No exception when new time is above minimum cutoff.
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // No exception when new time is equal to minimum cutoff.
+        $event->timestart = $duedate;
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // Exception when new time is earlier than minimum cutoff.
+        $event->timestart = $duedate - 1;
+        $this->expectException('moodle_exception');
+        mod_assign_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * Grading due date event should throw an exception if it's timestart is less than the
+     * submissions allowed from date if there is no due date set.
+     */
+    public function test_mod_assign_core_calendar_validate_gradingdue_event_min_limit_submissionsfromdate() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = 0;
+        $submissionsfromdate = time() - DAYSECS;
+        $cutoffdate = time() + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
+            'timestart' => $submissionsfromdate + 1,
+        ]);
+
+        // No exception when new time is above minimum cutoff.
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // No exception when new time is equal to minimum cutoff.
+        $event->timestart = $submissionsfromdate;
+        mod_assign_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+
+        // Exception when new time is earlier than minimum cutoff.
+        $event->timestart = $submissionsfromdate - 1;
+        $this->expectException('moodle_exception');
+        mod_assign_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * Non due date events should not update the assignment due date.
+     */
+    public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE,
+            'timestart' => $duedate + 1
+        ]);
+
+        mod_assign_core_calendar_event_timestart_updated($event);
+
+        $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
+        $this->assertEquals($duedate, $newinstance->duedate);
+    }
+
+    /**
+     * Due date override events should not change the assignment due date.
+     */
+    public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+        $userid = $this->students[0]->id;
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'userid' => $userid,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => $duedate + 1
+        ]);
+
+        $record = (object) [
+            'assignid' => $instance->id,
+            'userid' => $userid,
+            'duedate' => $duedate + 1
+        ];
+
+        $DB->insert_record('assign_overrides', $record);
+
+        mod_assign_core_calendar_event_timestart_updated($event);
+
+        $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
+        $this->assertEquals($duedate, $newinstance->duedate);
+    }
+
+    /**
+     * Due date events should update the assignment due date.
+     */
+    public function test_mod_assign_core_calendar_event_timestart_updated_due_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $duedate = time();
+        $newduedate = $duedate + 1;
+        $submissionsfromdate = $duedate - DAYSECS;
+        $cutoffdate = $duedate + DAYSECS;
+        $assign = $this->create_instance([
+            'duedate' => $duedate,
+            'allowsubmissionsfromdate' => $submissionsfromdate,
+            'cutoffdate' => $cutoffdate,
+        ]);
+        $instance = $assign->get_instance();
+
+        $event = new \calendar_event((object) [
+            'courseid' => $instance->course,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE,
+            'timestart' => $newduedate
+        ]);
+
+        mod_assign_core_calendar_event_timestart_updated($event);
+
+        $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
+        $this->assertEquals($newduedate, $newinstance->duedate);
+    }
+
+    /**
+     * If a student somehow finds a way to update the due date calendar event
+     * then the callback should not be executed to update the assignment due
+     * date as well otherwise that would be a security issue.
+     */
+    public function test_student_role_cant_update_due_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $mapper = calendar_event_container::get_event_mapper();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $now = time();
+        $duedate = (new DateTime())->setTimestamp($now);
+        $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
+        $assign = $this->create_instance([
+            'course' => $course->id,
+            'duedate' => $duedate->getTimestamp(),
+        ]);
+        $instance = $assign->get_instance();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $record = $DB->get_record('event', [
+            'courseid' => $course->id,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        $event = new \calendar_event($record);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
+
+        $this->setUser($user);
+
+        calendar_local_api::update_event_start_day(
+            $mapper->from_legacy_event_to_event($event),
+            $newduedate
+        );
+
+        $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
+        $newevent = \calendar_event::load($event->id);
+        // The due date shouldn't have changed even though we updated the calendar
+        // event.
+        $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate);
+        $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
+    }
+
+    /**
+     * A teacher with the capability to modify an assignment module should be
+     * able to update the assignment due date by changing the due date calendar
+     * event.
+     */
+    public function test_teacher_role_can_update_due_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $mapper = calendar_event_container::get_event_mapper();
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $now = time();
+        $duedate = (new DateTime())->setTimestamp($now);
+        $newduedate = (new DateTime())->setTimestamp($now)->modify('+1 day');
+        $assign = $this->create_instance([
+            'course' => $course->id,
+            'duedate' => $duedate->getTimestamp(),
+        ]);
+        $instance = $assign->get_instance();
+
+        $generator->enrol_user($user->id, $course->id, 'teacher');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $record = $DB->get_record('event', [
+            'courseid' => $course->id,
+            'modulename' => 'assign',
+            'instance' => $instance->id,
+            'eventtype' => ASSIGN_EVENT_TYPE_DUE
+        ]);
+
+        $event = new \calendar_event($record);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
+
+        $this->setUser($user);
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+
+        calendar_local_api::update_event_start_day(
+            $mapper->from_legacy_event_to_event($event),
+            $newduedate
+        );
+
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+
+        $newinstance = $DB->get_record('assign', ['id' => $instance->id]);
+        $newevent = \calendar_event::load($event->id);
+        // The due date shouldn't have changed even though we updated the calendar
+        // event.
+        $this->assertEquals($newduedate->getTimestamp(), $newinstance->duedate);
+        $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart);
+        // Confirm that a module updated event is fired when the module
+        // is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
 }