MDL-60063 feedback: add drag and drop of open close events
authorRyan Wyllie <ryan@moodle.com>
Sun, 24 Sep 2017 04:26:33 +0000 (04:26 +0000)
committerRyan Wyllie <ryan@moodle.com>
Tue, 17 Oct 2017 03:54:24 +0000 (03:54 +0000)
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/tests/lib_test.php

index cd6de5a..34bfabd 100644 (file)
@@ -207,6 +207,7 @@ $string['of'] = 'of';
 $string['oldvaluespreserved'] = 'All old questions and the assigned values will be preserved';
 $string['oldvalueswillbedeleted'] = 'Current questions and all responses will be deleted.';
 $string['only_one_captcha_allowed'] = 'Only one captcha is allowed in a feedback';
+$string['openafterclose'] = 'You have specified an open date after the close date';
 $string['overview'] = 'Overview';
 $string['page'] = 'Page';
 $string['page-mod-feedback-x'] = 'Any feedback module page';
index 639cf92..c87bbca 100644 (file)
@@ -3556,3 +3556,153 @@ function mod_feedback_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * 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
+ * @return array
+ */
+function mod_feedback_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance = null) {
+    global $DB;
+
+    if (!$instance) {
+        $instance = $DB->get_record('feedback', ['id' => $event->instance], '*', MUST_EXIST);
+    }
+
+    $mindate = null;
+    $maxdate = null;
+
+    if ($event->eventtype == FEEDBACK_EVENT_TYPE_OPEN) {
+        // The start time of the open event can't be equal to or after the
+        // close time of the choice activity.
+        if (!empty($instance->timeclose)) {
+            $maxdate = [
+                $instance->timeclose,
+                get_string('openafterclose', 'feedback')
+            ];
+        }
+    } else if ($event->eventtype == FEEDBACK_EVENT_TYPE_CLOSE) {
+        // The start time of the close event can't be equal to or earlier than the
+        // open time of the choice activity.
+        if (!empty($instance->timeopen)) {
+            $mindate = [
+                $instance->timeopen,
+                get_string('closebeforeopen', 'feedback')
+            ];
+        }
+    }
+
+    return [$mindate, $maxdate];
+}
+
+/**
+ * This function will check that the given event is valid for it's
+ * corresponding feedback module.
+ *
+ * An exception is thrown if the event fails validation.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ */
+function mod_feedback_core_calendar_validate_event_timestart(\calendar_event $event) {
+    global $DB;
+
+    $record = $DB->get_record('feedback', ['id' => $event->instance], '*', MUST_EXIST);
+    $timestart = $event->timestart;
+
+    list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $record);
+
+    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 feedback module according to the
+ * event that has been modified.
+ *
+ * It will set the timeopen or timeclose value of the feedback instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ */
+function mod_feedback_core_calendar_event_timestart_updated(\calendar_event $event) {
+    global $CFG, $DB;
+
+    if (empty($event->instance) || $event->modulename != 'feedback') {
+        return;
+    }
+
+    $coursemodule = get_coursemodule_from_instance('feedback',
+                                         $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;
+    }
+
+    $modified = false;
+
+    if ($event->eventtype == FEEDBACK_EVENT_TYPE_OPEN) {
+        // If the event is for the feedback activity opening then we should
+        // set the start time of the feedback activity to be the new start
+        // time of the event.
+        $record = $DB->get_record('feedback', ['id' => $event->instance], '*', MUST_EXIST);
+
+        if ($record->timeopen != $event->timestart) {
+            $record->timeopen = $event->timestart;
+            $record->timemodified = time();
+            $modified = true;
+        }
+    } else if ($event->eventtype == FEEDBACK_EVENT_TYPE_CLOSE) {
+        // If the event is for the feedback activity closing then we should
+        // set the end time of the feedback activity to be the new start
+        // time of the event.
+        $record = $DB->get_record('feedback', ['id' => $event->instance], '*', MUST_EXIST);
+
+        if ($record->timeclose != $event->timestart) {
+            $record->timeclose = $event->timestart;
+            $record->timemodified = time();
+            $modified = true;
+        }
+    }
+
+    if ($modified) {
+        $DB->update_record('feedback', $record);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
+}
index 22c01e3..b0b7c84 100644 (file)
@@ -380,4 +380,664 @@ class mod_feedback_lib_testcase extends advanced_testcase {
         $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * An unknown event should not have min or max restrictions.
+     */
+    public function test_get_valid_event_timestart_range_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => 'SOME UNKNOWN EVENT',
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_OPEN should have a max timestart equal to the activity
+     * close time.
+     */
+    public function test_get_valid_event_timestart_range_event_type_open() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertEquals($timeclose, $max[0]);
+        $this->assertNotEmpty($max[1]);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_OPEN should not have a max timestamp if the activity
+     * doesn't have a close date.
+     */
+    public function test_get_valid_event_timestart_range_event_type_open_no_close() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = 0;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_CLOSE should have a min timestart equal to the activity
+     * open time.
+     */
+    public function test_get_valid_event_timestart_range_event_type_close() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertEquals($timeopen, $min[0]);
+        $this->assertNotEmpty($min[1]);
+        $this->assertNull($max);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_CLOSE should not have a minimum timestamp if the activity
+     * doesn't have an open date.
+     */
+    public function test_get_valid_event_timestart_range_event_type_close_no_open() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = 0;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * You can't create a feedback module event when the module doesn't exist.
+     */
+    public function test_mod_feedback_core_calendar_validate_event_timestart_no_activity() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => 1234,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => time(),
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_feedback_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_OPEN must be before the close time of the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_validate_event_timestart_valid_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // This will throw an exception if the event is invald.
+        mod_feedback_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_OPEN can not have a start time set after the close time
+     * of the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_validate_event_timestart_invalid_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => $timeclose + 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_feedback_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_CLOSE must be after the open time of the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_validate_event_timestart_valid_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $timeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // This will throw an exception if the event is invald.
+        mod_feedback_core_calendar_validate_event_timestart($event);
+        $this->assertTrue(true);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_CLOSE can not have a start time set before the open time
+     * of the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_validate_event_timestart_invalid_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $timeopen - 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_feedback_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * An unkown event type should not change the feedback instance.
+     */
+    public function test_mod_feedback_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();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $DB->update_record('feedback', $feedback);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_feedback_core_calendar_event_timestart_updated($event);
+
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+        $this->assertEquals($timeopen, $feedback->timeopen);
+        $this->assertEquals($timeclose, $feedback->timeclose);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_OPEN event should update the timeopen property of
+     * the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_event_timestart_updated_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeopen = $timeopen - DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $feedback->timemodified = $timemodified;
+        $DB->update_record('feedback', $feedback);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
+            'timestart' => $newtimeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_feedback_core_calendar_event_timestart_updated($event);
+
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+        // Ensure the timeopen property matches the event timestart.
+        $this->assertEquals($newtimeopen, $feedback->timeopen);
+        // Ensure the timeclose isn't changed.
+        $this->assertEquals($timeclose, $feedback->timeclose);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $feedback->timemodified);
+    }
+
+    /**
+     * A FEEDBACK_EVENT_TYPE_CLOSE event should update the timeclose property of
+     * the feedback activity.
+     */
+    public function test_mod_feedback_core_calendar_event_timestart_updated_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $feedback->timemodified = $timemodified;
+        $DB->update_record('feedback', $feedback);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_feedback_core_calendar_event_timestart_updated($event);
+
+        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+        // Ensure the timeclose property matches the event timestart.
+        $this->assertEquals($newtimeclose, $feedback->timeclose);
+        // Ensure the timeopen isn't changed.
+        $this->assertEquals($timeopen, $feedback->timeopen);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $feedback->timemodified);
+    }
+
+    /**
+     * If a student somehow finds a way to update the calendar event
+     * then the callback should not be executed to update the activity
+     * properties as well because that would be a security issue.
+     */
+    public function test_student_role_cant_update_time_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $feedback->timemodified = $timemodified;
+        $DB->update_record('feedback', $feedback);
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => $user->id,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
+
+        $this->setUser($user);
+
+        mod_feedback_core_calendar_event_timestart_updated($event);
+
+        $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+        // The activity shouldn't have been updated because the user
+        // doesn't have permissions to do it.
+        $this->assertEquals($timeclose, $newfeedback->timeclose);
+    }
+
+    /**
+     * The activity should update if a teacher modifies the calendar
+     * event.
+     */
+    public function test_teacher_role_can_update_time_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
+        $feedback->timeopen = $timeopen;
+        $feedback->timeclose = $timeclose;
+        $feedback->timemodified = $timemodified;
+        $DB->update_record('feedback', $feedback);
+
+        $generator->enrol_user($user->id, $course->id, 'teacher');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => $user->id,
+            'modulename' => 'feedback',
+            'instance' => $feedback->id,
+            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
+
+        $this->setUser($user);
+
+        $sink = $this->redirectEvents();
+
+        mod_feedback_core_calendar_event_timestart_updated($event);
+
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+
+        $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]);
+        // The activity should have been updated because the user
+        // has permissions to do it.
+        $this->assertEquals($newtimeclose, $newfeedback->timeclose);
+        // A course_module_updated event should be fired if the module
+        // was successfully modified.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
 }