diff --git a/availability/classes/info.php b/availability/classes/info.php index fc00ad8d5cc..fe0735be511 100644 --- a/availability/classes/info.php +++ b/availability/classes/info.php @@ -49,6 +49,9 @@ abstract class info { /** @var tree Availability configuration, decoded from JSON; null if unset */ protected $availabilitytree; + /** @var array|null Array of information about current restore if any */ + protected static $restoreinfo = null; + /** * Constructs with item details. * @@ -307,9 +310,12 @@ abstract class info { * @param string $restoreid Restore identifier * @param int $courseid Target course id * @param \base_logger $logger Logger for any warnings + * @param int $dateoffset Date offset to be added to any dates (0 = none) */ - public function update_after_restore($restoreid, $courseid, \base_logger $logger) { + public function update_after_restore($restoreid, $courseid, \base_logger $logger, $dateoffset) { $tree = $this->get_availability_tree(); + // Set static data for use by get_restore_date_offset function. + self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset); $changed = $tree->update_after_restore($restoreid, $courseid, $logger, $this->get_thing_name()); if ($changed) { @@ -319,6 +325,24 @@ abstract class info { } } + /** + * Gets the date offset (amount by which any date values should be + * adjusted) for the current restore. + * + * @param string $restoreid Restore identifier + * @return int Date offset (0 if none) + * @throws coding_exception If not in a restore (or not in that restore) + */ + public static function get_restore_date_offset($restoreid) { + if (!self::$restoreinfo) { + throw new coding_exception('Only valid during restore'); + } + if (self::$restoreinfo['restoreid'] !== $restoreid) { + throw new coding_exception('Data not available for that restore id'); + } + return self::$restoreinfo['dateoffset']; + } + /** * Obtains the name of the item (cm_info or section_info, at present) that * this is controlling availability of. Name should be formatted ready diff --git a/availability/classes/tree.php b/availability/classes/tree.php index 218748c1813..a7308c083ae 100644 --- a/availability/classes/tree.php +++ b/availability/classes/tree.php @@ -606,10 +606,13 @@ class tree extends tree_node { public function save() { $result = new \stdClass(); $result->op = $this->op; - if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) { - $result->showc = $this->showchildren; - } else { - $result->show = $this->show; + // Only root tree has the 'show' options. + if ($this->root) { + if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) { + $result->showc = $this->showchildren; + } else { + $result->show = $this->show; + } } $result->c = array(); foreach ($this->children as $child) { diff --git a/availability/classes/tree_node.php b/availability/classes/tree_node.php index 54b42cd5676..a8471a01900 100644 --- a/availability/classes/tree_node.php +++ b/availability/classes/tree_node.php @@ -87,6 +87,9 @@ abstract class tree_node { * The default behaviour is simply to return false. If there is a problem * with the update, $logger can be used to output a warning. * + * Note: If you need information about the date offset, call + * \core_availability\info::get_restore_date_offset($restoreid). + * * @param string $restoreid Restore ID * @param int $courseid ID of target course * @param \base_logger $logger Logger for any warnings diff --git a/availability/condition/date/classes/condition.php b/availability/condition/date/classes/condition.php index 36191f94798..be3be9b4c42 100644 --- a/availability/condition/date/classes/condition.php +++ b/availability/condition/date/classes/condition.php @@ -209,4 +209,15 @@ class condition extends \core_availability\condition { protected static function is_midnight($time) { return usergetmidnight($time) == $time; } + + public function update_after_restore( + $restoreid, $courseid, \base_logger $logger, $name) { + // Update the date, if restoring with changed date. + $dateoffset = \core_availability\info::get_restore_date_offset($restoreid); + if ($dateoffset) { + $this->time += $dateoffset; + return true; + } + return false; + } } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 6a9242337df..5492497ea8d 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -645,6 +645,9 @@ class restore_update_availability extends restore_execution_step { rebuild_course_cache($this->get_courseid(), true); $modinfo = get_fast_modinfo($this->get_courseid()); + // Get the date offset for this restore. + $dateoffset = $this->apply_date_offset(1) - 1; + // Update all sections that were restored. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section'); $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid'); @@ -667,7 +670,7 @@ class restore_update_availability extends restore_execution_step { if (!is_null($section->availability)) { $info = new \core_availability\info_section($section); $info->update_after_restore($this->get_restoreid(), - $this->get_courseid(), $this->get_logger()); + $this->get_courseid(), $this->get_logger(), $dateoffset); } } $rs->close(); @@ -687,7 +690,7 @@ class restore_update_availability extends restore_execution_step { if (!is_null($cm->availability)) { $info = new \core_availability\info_module($cm); $info->update_after_restore($this->get_restoreid(), - $this->get_courseid(), $this->get_logger()); + $this->get_courseid(), $this->get_logger(), $dateoffset); } } $rs->close(); diff --git a/backup/moodle2/tests/moodle2_test.php b/backup/moodle2/tests/moodle2_test.php index 23b25302455..bb937aa65d5 100644 --- a/backup/moodle2/tests/moodle2_test.php +++ b/backup/moodle2/tests/moodle2_test.php @@ -331,13 +331,67 @@ class core_backup_moodle2_testcase extends advanced_testcase { $this->assertEquals($expected, $actual); } + /** + * When restoring a course, you can change the start date, which shifts other + * dates. This test checks that certain dates are correctly modified. + */ + public function test_restore_dates() { + global $DB, $CFG; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $CFG->enableavailability = true; + + // Create a course with specific start date. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(array( + 'startdate' => strtotime('1 Jan 2014 00:00 GMT'))); + + // Add a forum with conditional availability date restriction, including + // one of them nested inside a tree. + $availability = '{"op":"&","showc":[true,true],"c":[' . + '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' . + '{"type":"date","d":"<","t":DATE2}]}'; + $before = str_replace( + array('DATE1', 'DATE2'), + array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')), + $availability); + $forum = $generator->create_module('forum', array('course' => $course->id, + 'availability' => $before)); + + // Add an assign with defined start date. + $assign = $generator->create_module('assign', array('course' => $course->id, + 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT'))); + + // Do backup and restore. + $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT')); + + $modinfo = get_fast_modinfo($newcourseid); + + // Check forum dates are modified by the same amount as the course start. + $newforums = $modinfo->get_instances_of('forum'); + $newforum = reset($newforums); + $after = str_replace( + array('DATE1', 'DATE2'), + array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')), + $availability); + $this->assertEquals($after, $newforum->availability); + + // Check assign date. + $newassigns = $modinfo->get_instances_of('assign'); + $newassign = reset($newassigns); + $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field( + 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance))); + } + /** * Backs a course up and restores it. * * @param stdClass $course Course object to backup + * @param int $newdate If non-zero, specifies custom date for new course * @return int ID of newly restored course */ - protected function backup_and_restore($course) { + protected function backup_and_restore($course, $newdate = 0) { global $USER, $CFG; // Turn off file logging, otherwise it can't delete the file (Windows). @@ -358,6 +412,9 @@ class core_backup_moodle2_testcase extends advanced_testcase { $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); + if ($newdate) { + $rc->get_plan()->get_setting('course_startdate')->set_value($newdate); + } $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); diff --git a/backup/util/plan/restore_step.class.php b/backup/util/plan/restore_step.class.php index 29b2734d985..92c840af58a 100644 --- a/backup/util/plan/restore_step.class.php +++ b/backup/util/plan/restore_step.class.php @@ -45,6 +45,55 @@ abstract class restore_step extends base_step { } return $this->task->get_restoreid(); } + + /** + * Apply course startdate offset based in original course startdate and course_offset_startdate setting + * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple + * executions in the same request + * + * @param int $value Time value (seconds since epoch), or empty for nothing + * @return int Time value after applying the date offset, or empty for nothing + */ + public function apply_date_offset($value) { + + // Empties don't offset - zeros (int and string), false and nulls return original value. + if (empty($value)) { + return $value; + } + + static $cache = array(); + // Lookup cache. + if (isset($cache[$this->get_restoreid()])) { + return $value + $cache[$this->get_restoreid()]; + } + // No cache, let's calculate the offset. + $original = $this->task->get_info()->original_course_startdate; + $setting = 0; + if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019). + $setting = $this->get_setting_value('course_startdate'); + } + + if (empty($original) || empty($setting)) { + // Original course has not startdate or setting doesn't exist, offset = 0. + $cache[$this->get_restoreid()] = 0; + + } else if (abs($setting - $original) < 24 * 60 * 60) { + // Less than 24h of difference, offset = 0 (this avoids some problems with timezones). + $cache[$this->get_restoreid()] = 0; + + } else if (!has_capability('moodle/restore:rolldates', + context_course::instance($this->get_courseid()), $this->task->get_userid())) { + // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case. + $cache[$this->get_restoreid()] = 0; + + } else { + // Arrived here, let's calculate the real offset. + $cache[$this->get_restoreid()] = $setting - $original; + } + + // Return the passed value with cached offset applied. + return $value + $cache[$this->get_restoreid()]; + } } /* diff --git a/backup/util/plan/restore_structure_step.class.php b/backup/util/plan/restore_structure_step.class.php index e4685b09615..94e5ad66132 100644 --- a/backup/util/plan/restore_structure_step.class.php +++ b/backup/util/plan/restore_structure_step.class.php @@ -245,53 +245,6 @@ abstract class restore_structure_step extends restore_step { $this->task->add_result($resultstoadd); } - /** - * Apply course startdate offset based in original course startdate and course_offset_startdate setting - * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple - * executions in the same request - */ - public function apply_date_offset($value) { - - // empties don't offset - zeros (int and string), false and nulls return original value - if (empty($value)) { - return $value; - } - - static $cache = array(); - // Lookup cache - if (isset($cache[$this->get_restoreid()])) { - return $value + $cache[$this->get_restoreid()]; - } - // No cache, let's calculate the offset - $original = $this->task->get_info()->original_course_startdate; - $setting = 0; - if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019) - $setting = $this->get_setting_value('course_startdate'); - } - - // Original course has not startdate or setting doesn't exist, offset = 0 - if (empty($original) || empty($setting)) { - $cache[$this->get_restoreid()] = 0; - - // Less than 24h of difference, offset = 0 (this avoids some problems with timezones) - } else if (abs($setting - $original) < 24 * 60 * 60) { - $cache[$this->get_restoreid()] = 0; - - // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case - } else if (!has_capability('moodle/restore:rolldates', - context_course::instance($this->get_courseid()), - $this->task->get_userid())) { - $cache[$this->get_restoreid()] = 0; - - // Arrived here, let's calculate the real offset - } else { - $cache[$this->get_restoreid()] = $setting - $original; - } - - // Return the passed value with cached offset applied - return $value + $cache[$this->get_restoreid()]; - } - /** * As far as restore structure steps are implementing restore_plugin stuff, they need to * have the parent task available for wrapping purposes (get course/context....)