MDL-60058 choice: implement timestart range callback for calendar UI
authorRyan Wyllie <ryan@moodle.com>
Thu, 28 Sep 2017 06:56:40 +0000 (06:56 +0000)
committerRyan Wyllie <ryan@moodle.com>
Fri, 13 Oct 2017 06:38:02 +0000 (06:38 +0000)
mod/choice/lib.php
mod/choice/tests/lib_test.php

index 85441c3..8f85132 100644 (file)
@@ -1240,6 +1240,55 @@ function mod_choice_core_calendar_provide_event_action(calendar_event $event,
     );
 }
 
+/**
+ * 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|null $instance The module instance to get the range from
+ */
+function mod_choice_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $choice = null) {
+    global $DB;
+
+    if (!$choice) {
+        $choice = $DB->get_record('choice', ['id' => $event->instance]);
+    }
+
+    $mindate = null;
+    $maxdate = null;
+
+    if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) {
+        if (!empty($choice->timeclose)) {
+            $maxdate = [
+                $choice->timeclose,
+                get_string('openafterclose', 'choice')
+            ];
+        }
+    } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) {
+        if (!empty($choice->timeopen)) {
+            $mindate = [
+                $choice->timeopen,
+                get_string('closebeforeopen', 'choice')
+            ];
+        }
+    }
+
+    return [$mindate, $maxdate];
+}
+
 /**
  * This function will check that the given event is valid for it's
  * corresponding choice module.
@@ -1253,23 +1302,23 @@ function mod_choice_core_calendar_provide_event_action(calendar_event $event,
 function mod_choice_core_calendar_validate_event_timestart(\calendar_event $event) {
     global $DB;
 
-    $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+    if (!isset($event->instance)) {
+        return;
+    }
 
-    if ($event->eventtype == CHOICE_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($record->timeclose) && $event->timestart > $record->timeclose) {
-            throw new \moodle_exception('openafterclose', 'choice');
-        }
-    } else if ($event->eventtype == CHOICE_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($record->timeopen) && $event->timestart < $record->timeopen) {
-            throw new \moodle_exception('closebeforeopen', 'choice');
-        }
+    // 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('choice', ['id' => $event->instance], '*', MUST_EXIST);
+    $timestart = $event->timestart;
+    list($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $instance);
+
+    if ($min && $timestart < $min[0]) {
+        throw new \moodle_exception($min[1]);
     }
 
-    return true;
+    if ($max && $timestart > $max[0]) {
+        throw new \moodle_exception($max[1]);
+    }
 }
 
 /**
@@ -1285,29 +1334,55 @@ function mod_choice_core_calendar_validate_event_timestart(\calendar_event $even
 function mod_choice_core_calendar_event_timestart_updated(\calendar_event $event) {
     global $DB;
 
+    $courseid = $event->courseid;
+    $modulename = $event->modulename;
+    $instanceid = $event->instance;
+    $modified = false;
+
+    // Something weird going on. The event is for a different module so
+    // we should ignore it.
+    if ($modulename != 'choice') {
+        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 == CHOICE_EVENT_TYPE_OPEN) {
         // If the event is for the choice activity opening then we should
         // set the start time of the choice activity to be the new start
         // time of the event.
-        $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+        $record = $DB->get_record('choice', ['id' => $instanceid], '*', MUST_EXIST);
 
         if ($record->timeopen != $event->timestart) {
             $record->timeopen = $event->timestart;
             $record->timemodified = time();
-            $DB->update_record('choice', $record);
+            $modified = true;
         }
     } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) {
         // If the event is for the choice activity closing then we should
         // set the end time of the choice activity to be the new start
         // time of the event.
-        $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+        $record = $DB->get_record('choice', ['id' => $instanceid], '*', MUST_EXIST);
 
         if ($record->timeclose != $event->timestart) {
             $record->timeclose = $event->timestart;
             $record->timemodified = time();
-            $DB->update_record('choice', $record);
+            $modified = true;
         }
     }
+
+    if ($modified) {
+        // Persist the instance changes.
+        $DB->update_record('choice', $record);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
 }
 
 /**
index 5d1c19c..92ee1bf 100644 (file)
@@ -554,7 +554,10 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
             'visible' => 1
         ]);
 
-        $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event));
+        mod_choice_core_calendar_validate_event_timestart($event);
+        // The function above will throw an exception if the event is
+        // invalid.
+        $this->assertTrue(true);
     }
 
     /**
@@ -630,7 +633,10 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
             'visible' => 1
         ]);
 
-        $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event));
+        mod_choice_core_calendar_validate_event_timestart($event);
+        // The function above will throw an exception if the event isn't
+        // valid.
+        $this->assertTrue(true);
     }
 
     /**
@@ -754,8 +760,16 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
             'visible' => 1
         ]);
 
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+
         mod_choice_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');
+        });
+
         $choice = $DB->get_record('choice', ['id' => $choice->id]);
         // Ensure the timeopen property matches the event timestart.
         $this->assertEquals($newtimeopen, $choice->timeopen);
@@ -763,6 +777,9 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($timeclose, $choice->timeclose);
         // Ensure the timemodified property has been changed.
         $this->assertNotEquals($timemodified, $choice->timemodified);
+        // Confirm that a module updated event is fired when the module
+        // is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
     }
 
     /**
@@ -804,8 +821,16 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
             'visible' => 1
         ]);
 
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+
         mod_choice_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');
+        });
+
         $choice = $DB->get_record('choice', ['id' => $choice->id]);
         // Ensure the timeclose property matches the event timestart.
         $this->assertEquals($newtimeclose, $choice->timeclose);
@@ -813,5 +838,140 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($timeopen, $choice->timeopen);
         // Ensure the timemodified property has been changed.
         $this->assertNotEquals($timemodified, $choice->timemodified);
+        // Confirm that a module updated event is fired when the module
+        // is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * An unkown event type should not have any limits
+     */
+    public function test_mod_choice_core_calendar_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();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = new \stdClass();
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => 1,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The open event should be limited by the choice's timeclose property, if it's set.
+     */
+    public function test_mod_choice_core_calendar_get_valid_event_timestart_range_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = new \stdClass();
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => 1,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
+
+        $this->assertNull($min);
+        $this->assertEquals($timeclose, $max[0]);
+
+        // No timeclose value should result in no upper limit.
+        $choice->timeclose = 0;
+        list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
+
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The close event should be limited by the choice's timeopen property, if it's set.
+     */
+    public function test_mod_choice_core_calendar_get_valid_event_timestart_range_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = new \stdClass();
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => 1,
+            'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
+
+        $this->assertEquals($timeopen, $min[0]);
+        $this->assertNull($max);
+
+        // No timeclose value should result in no upper limit.
+        $choice->timeopen = 0;
+        list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
+
+        $this->assertNull($min);
+        $this->assertNull($max);
     }
 }