Merge branch 'MDL-71410-311' of git://github.com/rezaies/moodle into MOODLE_311_STABLE

This commit is contained in:
Jun Pataleta
2021-04-27 12:36:29 +08:00
32 changed files with 1542 additions and 30 deletions
+3 -1
View File
@@ -1121,7 +1121,9 @@ class completion_info {
// Custom activity module completion data.
// Cast custom data to array before checking for custom completion rules.
$customdata = (array)$cm->customdata;
// We call ->get_custom_data() instead of ->customdata here because there is the chance of recursive calling,
// and we cannot call a getter from a getter in PHP.
$customdata = (array) $cm->get_custom_data();
// Return early if the plugin does not define custom completion rules.
if (empty($customdata['customcompletionrules'])) {
return $data;
+24 -3
View File
@@ -701,7 +701,7 @@ class course_modinfo {
* {@link cached_cm_info}
*
* <b>Stage 2 - dynamic data.</b>
* Dynamic data is user-dependend, it is stored in request-level cache. To reset this cache
* Dynamic data is user-dependent, it is stored in request-level cache. To reset this cache
* {@link get_fast_modinfo()} with $reset argument may be called.
*
* Dynamic data is obtained when any of the following properties/methods is requested:
@@ -722,6 +722,7 @@ class course_modinfo {
* - {@link cm_info::set_user_visible()}
* - {@link cm_info::set_on_click()}
* - {@link cm_info::set_icon_url()}
* - {@link cm_info::override_customdata()}
* Any methods affecting view elements can also be set in this callback.
*
* <b>Stage 3 (view data).</b>
@@ -1419,9 +1420,15 @@ class cm_info implements IteratorAggregate {
return $this->onclick;
}
/**
* Getter method for property $customdata, ensures that dynamic data is retrieved.
*
* This method is normally called by the property ->customdata, but can be called directly if there
* is a case when it might be called recursively (you can't call property values recursively).
*
* @return mixed Optional custom data stored in modinfo cache for this activity, or null if none
*/
private function get_custom_data() {
public function get_custom_data() {
$this->obtain_dynamic_data();
return $this->customdata;
}
@@ -1679,6 +1686,19 @@ class cm_info implements IteratorAggregate {
$this->onclick = $onclick;
}
/**
* Overrides the value of an element in the customdata array.
*
* @param string $name The key in the customdata array
* @param mixed $value The value
*/
public function override_customdata($name, $value) {
if (!is_array($this->customdata)) {
$this->customdata = [];
}
$this->customdata[$name] = $value;
}
/**
* Sets HTML that displays after link on course view page.
* @param string $afterlink HTML string (empty string if none)
@@ -1881,7 +1901,8 @@ class cm_info implements IteratorAggregate {
* the module or not.
*
* As part of this function, the module's _cm_info_dynamic function from its lib.php will
* be called (if it exists).
* be called (if it exists). Make sure that the functions that are called here do not use
* any getter magic method from cm_info.
* @return void
*/
private function obtain_dynamic_data() {
+110
View File
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache data source for the assign overrides.
*
* @package mod_assign
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_assign\cache;
use cache_definition;
/**
* Class assign_overrides
*
* @package mod_assign
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overrides implements \cache_data_source {
/** @var overrides the singleton instance of this class. */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return object
*/
public static function get_instance_for_cache(cache_definition $definition): overrides {
if (is_null(self::$instance)) {
self::$instance = new overrides();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return mixed What ever data should be returned, or false if it can't be loaded.
* @throws \coding_exception
*/
public function load_for_cache($key) {
global $DB;
[$assignid, $ug, $ugid] = explode('_', $key);
$assignid = (int) $assignid;
switch ($ug) {
case 'u':
$userid = (int) $ugid;
$override = $DB->get_record(
'assign_overrides',
['assignid' => $assignid, 'userid' => $userid],
'duedate, cutoffdate, allowsubmissionsfromdate'
);
break;
case 'g':
$groupid = (int) $ugid;
$override = $DB->get_record(
'assign_overrides',
['assignid' => $assignid, 'groupid' => $groupid],
'sortorder, duedate, cutoffdate, allowsubmissionsfromdate'
);
break;
default:
throw new \coding_exception('Invalid cache key');
}
// Return null instead of false, because false will not be cached.
return $override ?: null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys) {
$results = [];
foreach ($keys as $key) {
$results[] = $this->load_for_cache($key);
}
return $results;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for fetching the important dates in mod_assign for a given module instance and a user.
*
* @package mod_assign
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_assign;
use core\activity_dates;
/**
* Class for fetching the important dates in mod_assign for a given module instance and a user.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates extends activity_dates {
/**
* Returns a list of important dates in mod_assign
*
* @return array
*/
protected function get_dates(): array {
$timeopen = $this->cm->customdata['allowsubmissionsfromdate'] ?? null;
$timedue = $this->cm->customdata['duedate'] ?? null;
$now = time();
$dates = [];
if ($timeopen) {
$openlabelid = $timeopen > $now ? 'activitydate:submissionsopen' : 'activitydate:submissionsopened';
$dates[] = [
'label' => get_string($openlabelid, 'mod_assign'),
'timestamp' => (int) $timeopen,
];
}
if ($timedue) {
$dates[] = [
'label' => get_string('activitydate:submissionsdue', 'mod_assign'),
'timestamp' => (int) $timedue,
];
}
return $dates;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defined caches used internally by the plugin.
*
* @package mod_assign
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
$definitions = [
'overrides' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'datasource' => '\mod_assign\cache\overrides',
],
];
+4
View File
@@ -22,6 +22,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['activitydate:submissionsdue'] = 'Submissions due:';
$string['activitydate:submissionsopen'] = 'Submissions open:';
$string['activitydate:submissionsopened'] = 'Submissions opened:';
$string['activityoverview'] = 'You have assignments that need attention';
$string['addsubmission'] = 'Add submission';
$string['addsubmission_help'] = 'You have not made a submission yet.';
@@ -112,6 +115,7 @@ $string['batchsetmarkingworkflowstateforusers'] = 'Set marking workflow state fo
$string['blindmarking'] = 'Anonymous submissions';
$string['blindmarkingenabledwarning'] = 'Anonymous submissions are enabled for this activity. Grades will not be added to the gradebook until student identities are revealed via the grading action menu.';
$string['blindmarking_help'] = 'Anonymous submissions hide the identity of students from markers. Anonymous submission settings will be locked once a submission or grade has been made in relation to this assignment.';
$string['cachedef_overrides'] = 'User and group override information';
$string['calendardue'] = '{$a} is due';
$string['calendargradingdue'] = '{$a} is due to be graded';
$string['changeuser'] = 'Change user';
+76 -2
View File
@@ -485,10 +485,11 @@ function assign_extend_settings_navigation(settings_navigation $settings, naviga
* will know about (most noticeably, an icon).
*/
function assign_get_coursemodule_info($coursemodule) {
global $CFG, $DB;
global $DB;
$dbparams = array('id'=>$coursemodule->instance);
$fields = 'id, name, alwaysshowdescription, allowsubmissionsfromdate, intro, introformat, completionsubmit';
$fields = 'id, name, alwaysshowdescription, allowsubmissionsfromdate, intro, introformat, completionsubmit,
duedate, cutoffdate, allowsubmissionsfromdate';
if (! $assignment = $DB->get_record('assign', $dbparams, $fields)) {
return false;
}
@@ -507,9 +508,82 @@ function assign_get_coursemodule_info($coursemodule) {
$result->customdata['customcompletionrules']['completionsubmit'] = $assignment->completionsubmit;
}
// Populate some other values that can be used in calendar or on dashboard.
if ($assignment->duedate) {
$result->customdata['duedate'] = $assignment->duedate;
}
if ($assignment->cutoffdate) {
$result->customdata['cutoffdate'] = $assignment->cutoffdate;
}
if ($assignment->allowsubmissionsfromdate) {
$result->customdata['allowsubmissionsfromdate'] = $assignment->allowsubmissionsfromdate;
}
return $result;
}
/**
* Sets dynamic information about a course module
*
* This function is called from cm_info when displaying the module
*
* @param cm_info $cm
*/
function mod_assign_cm_info_dynamic(cm_info $cm) {
global $USER;
$cache = cache::make('mod_assign', 'overrides');
$override = $cache->get("{$cm->instance}_u_{$USER->id}");
if (!$override) {
$override = (object) [
'allowsubmissionsfromdate' => null,
'duedate' => null,
'cutoffdate' => null,
];
}
// No need to look for group overrides if there are user overrides for all allowsubmissionsfromdate, duedate and cutoffdate.
if (is_null($override->allowsubmissionsfromdate) || is_null($override->duedate) || is_null($override->cutoffdate)) {
$selectedgroupoverride = (object) [
'allowsubmissionsfromdate' => null,
'duedate' => null,
'cutoffdate' => null,
'sortorder' => PHP_INT_MAX, // So that every sortorder read from DB is less than this.
];
$groupings = groups_get_user_groups($cm->course, $USER->id);
foreach ($groupings[0] as $groupid) {
$groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
if ($groupoverride) {
if ($groupoverride->sortorder < $selectedgroupoverride->sortorder) {
$selectedgroupoverride = $groupoverride;
}
}
}
// If there is a user override for a setting, ignore the group override.
if (is_null($override->allowsubmissionsfromdate)) {
$override->allowsubmissionsfromdate = $selectedgroupoverride->allowsubmissionsfromdate;
}
if (is_null($override->duedate)) {
$override->duedate = $selectedgroupoverride->duedate;
}
if (is_null($override->cutoffdate)) {
$override->cutoffdate = $selectedgroupoverride->cutoffdate;
}
}
// Populate some other values that can be used in calendar or on dashboard.
if (!is_null($override->allowsubmissionsfromdate)) {
$cm->override_customdata('allowsubmissionsfromdate', $override->allowsubmissionsfromdate);
}
if (!is_null($override->duedate)) {
$cm->override_customdata('duedate', $override->duedate);
}
if (!is_null($override->cutoffdate)) {
$cm->override_customdata('cutoffdate', $override->cutoffdate);
}
}
/**
* Callback which returns human-readable strings describing the active completion custom rules for the module instance.
*
+30 -6
View File
@@ -874,8 +874,10 @@ class assign {
$conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
if (isset($override->userid)) {
$conds['userid'] = $override->userid;
$cachekey = "{$cm->instance}_u_{$override->userid}";
} else {
$conds['groupid'] = $override->groupid;
$cachekey = "{$cm->instance}_g_{$override->groupid}";
}
$events = $DB->get_records('event', $conds);
foreach ($events as $event) {
@@ -884,6 +886,7 @@ class assign {
}
$DB->delete_records('assign_overrides', array('id' => $overrideid));
cache::make('mod_assign', 'overrides')->delete($cachekey);
// Set the common parameters for one of the events we will be triggering.
$params = array(
@@ -1203,6 +1206,8 @@ class assign {
}
}
$purgeoverrides = false;
// Remove user overrides.
if (!empty($data->reset_assign_user_overrides)) {
$DB->delete_records_select('assign_overrides',
@@ -1211,6 +1216,7 @@ class assign {
'component' => $componentstr,
'item' => get_string('useroverridesdeleted', 'assign'),
'error' => false);
$purgeoverrides = true;
}
// Remove group overrides.
if (!empty($data->reset_assign_group_overrides)) {
@@ -1220,6 +1226,7 @@ class assign {
'component' => $componentstr,
'item' => get_string('groupoverridesdeleted', 'assign'),
'error' => false);
$purgeoverrides = true;
}
// Updating dates - shift may be negative too.
@@ -1237,6 +1244,8 @@ class assign {
WHERE assignid =? AND cutoffdate <> 0",
array($data->timeshift, $this->get_instance()->id));
$purgeoverrides = true;
// Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
// See MDL-9367.
shift_course_mod_dates('assign',
@@ -1248,6 +1257,10 @@ class assign {
'error'=>false);
}
if ($purgeoverrides) {
cache::make('mod_assign', 'overrides')->purge();
}
return $status;
}
@@ -9790,14 +9803,14 @@ function assign_process_group_deleted_in_course($courseid, $groupid = null) {
if ($groupid) {
$params['groupid'] = $groupid;
// We just update the group that was deleted.
$sql = "SELECT o.id, o.assignid
$sql = "SELECT o.id, o.assignid, o.groupid
FROM {assign_overrides} o
JOIN {assign} assign ON assign.id = o.assignid
WHERE assign.course = :courseid
AND o.groupid = :groupid";
} else {
// No groupid, we update all orphaned group overrides for all assign in course.
$sql = "SELECT o.id, o.assignid
$sql = "SELECT o.id, o.assignid, o.groupid
FROM {assign_overrides} o
JOIN {assign} assign ON assign.id = o.assignid
LEFT JOIN {groups} grp ON grp.id = o.groupid
@@ -9805,11 +9818,15 @@ function assign_process_group_deleted_in_course($courseid, $groupid = null) {
AND o.groupid IS NOT NULL
AND grp.id IS NULL";
}
$records = $DB->get_records_sql_menu($sql, $params);
$records = $DB->get_records_sql($sql, $params);
if (!$records) {
return; // Nothing to do.
}
$DB->delete_records_list('assign_overrides', 'id', array_keys($records));
$cache = cache::make('mod_assign', 'overrides');
foreach ($records as $record) {
$cache->delete("{$record->assignid}_g_{$record->groupid}");
}
}
/**
@@ -9824,7 +9841,7 @@ function move_group_override($id, $move, $assignid) {
global $DB;
// Get the override object.
if (!$override = $DB->get_record('assign_overrides', array('id' => $id), 'id, sortorder')) {
if (!$override = $DB->get_record('assign_overrides', ['id' => $id], 'id, sortorder, groupid')) {
return false;
}
// Count the number of group overrides.
@@ -9840,8 +9857,8 @@ function move_group_override($id, $move, $assignid) {
}
// Retrieve the override object that is currently residing in the new position.
$params = array('sortorder' => $neworder, 'assignid' => $assignid);
if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder')) {
$params = ['sortorder' => $neworder, 'assignid' => $assignid];
if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
// Swap the sortorders.
$swapoverride->sortorder = $override->sortorder;
@@ -9850,6 +9867,11 @@ function move_group_override($id, $move, $assignid) {
// Update the override records.
$DB->update_record('assign_overrides', $override);
$DB->update_record('assign_overrides', $swapoverride);
// Delete cache for the 2 records we updated above.
$cache = cache::make('mod_assign', 'overrides');
$cache->delete("{$override->assignid}_g_{$override->groupid}");
$cache->delete("{$swapoverride->assignid}_g_{$swapoverride->groupid}");
}
reorder_group_overrides($assignid);
@@ -9866,11 +9888,13 @@ function reorder_group_overrides($assignid) {
$i = 1;
if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
$cache = cache::make('mod_assign', 'overrides');
foreach ($overrides as $override) {
$f = new stdClass();
$f->id = $override->id;
$f->sortorder = $i++;
$DB->update_record('assign_overrides', $f);
$cache->delete("{$assignid}_g_{$override->groupid}");
// Update priorities of group overrides.
$params = [
+4
View File
@@ -187,6 +187,8 @@ if ($mform->is_cancelled()) {
if (!empty($override->id)) {
$fromform->id = $override->id;
$DB->update_record('assign_overrides', $fromform);
$cachekey = $groupmode ? "{$fromform->assignid}_g_{$fromform->groupid}" : "{$fromform->assignid}_u_{$fromform->userid}";
cache::make('mod_assign', 'overrides')->delete($cachekey);
// Determine which override updated event to fire.
$params['objectid'] = $override->id;
@@ -219,6 +221,8 @@ if ($mform->is_cancelled()) {
$DB->update_record('assign_overrides', $fromform);
reorder_group_overrides($assigninstance->id);
}
$cachekey = $groupmode ? "{$fromform->assignid}_g_{$fromform->groupid}" : "{$fromform->assignid}_u_{$fromform->userid}";
cache::make('mod_assign', 'overrides')->delete($cachekey);
// Determine which override created event to fire.
$params['objectid'] = $fromform->id;
+175
View File
@@ -0,0 +1,175 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains unit tests for mod_assign\dates.
*
* @package mod_assign
* @category test
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_assign;
use advanced_testcase;
use cm_info;
use core\activity_dates;
/**
* Class for unit testing mod_assign\dates.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_test extends advanced_testcase {
/**
* Data provider for get_dates_for_module().
* @return array[]
*/
public function get_dates_for_module_provider(): array {
$now = time();
$before = $now - DAYSECS;
$earlier = $before - DAYSECS;
$after = $now + DAYSECS;
$later = $after + DAYSECS;
return [
'without any dates' => [
null, null, null, null, null, null, []
],
'only with opening time' => [
$after, null, null, null, null, null, [
['label' => get_string('activitydate:submissionsopen', 'mod_assign'), 'timestamp' => $after],
]
],
'only with closing time' => [
null, $after, null, null, null, null, [
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $after],
]
],
'with both times' => [
$after, $later, null, null, null, null, [
['label' => get_string('activitydate:submissionsopen', 'mod_assign'), 'timestamp' => $after],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $later],
]
],
'between the dates' => [
$before, $after, null, null, null, null, [
['label' => get_string('activitydate:submissionsopened', 'mod_assign'), 'timestamp' => $before],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $after],
]
],
'dates are past' => [
$earlier, $before, null, null, null, null, [
['label' => get_string('activitydate:submissionsopened', 'mod_assign'), 'timestamp' => $earlier],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $before],
]
],
'with user override' => [
$before, $after, $earlier, $later, null, null, [
['label' => get_string('activitydate:submissionsopened', 'mod_assign'), 'timestamp' => $earlier],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $later],
]
],
'with group override' => [
$before, $after, null, null, $earlier, $later, [
['label' => get_string('activitydate:submissionsopened', 'mod_assign'), 'timestamp' => $earlier],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $later],
]
],
'with both user and group overrides' => [
$before, $after, $earlier, $later, $earlier - DAYSECS, $later + DAYSECS, [
['label' => get_string('activitydate:submissionsopened', 'mod_assign'), 'timestamp' => $earlier],
['label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $later],
]
],
];
}
/**
* Test for get_dates_for_module().
*
* @dataProvider get_dates_for_module_provider
* @param int|null $from Time of opening submissions in the assignment.
* @param int|null $due Assignment's due date.
* @param int|null $userfrom The user override for opening submissions.
* @param int|null $userdue The user override for due date.
* @param int|null $groupfrom The group override for opening submissions.
* @param int|null $groupdue The group override for due date.
* @param array $expected The expected value of calling get_dates_for_module()
*/
public function test_get_dates_for_module(?int $from, ?int $due,
?int $userfrom, ?int $userdue,
?int $groupfrom, ?int $groupdue,
array $expected) {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $generator->get_plugin_generator('mod_assign');
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$data = ['course' => $course->id];
if ($from) {
$data['allowsubmissionsfromdate'] = $from;
}
if ($due) {
$data['duedate'] = $due;
}
$assign = $assigngenerator->create_instance($data);
if ($userfrom || $userdue || $groupfrom || $groupdue) {
$generator->enrol_user($user->id, $course->id);
$group = $generator->create_group(['courseid' => $course->id]);
$generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
if ($userfrom || $userdue) {
$assigngenerator->create_override([
'assignid' => $assign->id,
'userid' => $user->id,
'allowsubmissionsfromdate' => $userfrom,
'duedate' => $userdue,
]);
}
if ($groupfrom || $groupdue) {
$assigngenerator->create_override([
'assignid' => $assign->id,
'groupid' => $group->id,
'allowsubmissionsfromdate' => $groupfrom,
'duedate' => $groupdue,
]);
}
}
$this->setUser($user);
$cm = get_coursemodule_from_instance('assign', $assign->id);
// Make sure we're using a cm_info object.
$cm = cm_info::create($cm);
$dates = activity_dates::get_dates_for_module($cm, (int) $user->id);
$this->assertEquals($expected, $dates);
}
}
+24
View File
@@ -59,4 +59,28 @@ class mod_assign_generator extends testing_module_generator {
return parent::create_instance($record, (array)$options);
}
/**
* Create an assign override (either user or group).
*
* @param array $data must specify assignid, and one of userid or groupid.
* @throws coding_exception
*/
public function create_override(array $data): void {
global $DB;
if (!isset($data['assignid'])) {
throw new coding_exception('Must specify assignid when creating an assign override.');
}
if (!isset($data['userid']) && !isset($data['groupid'])) {
throw new coding_exception('Must specify one of userid or groupid when creating an assign override.');
}
if (isset($data['userid']) && isset($data['groupid'])) {
throw new coding_exception('Cannot specify both userid and groupid when creating an assign override.');
}
$DB->insert_record('assign_overrides', (object) $data);
}
}
+1 -1
View File
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
$plugin->version = 2020110900; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2021042700; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2020110300; // Requires this Moodle version.
+1 -1
View File
@@ -439,7 +439,7 @@ function folder_get_coursemodule_info($cm) {
* @param cm_info $cm
*/
function folder_cm_info_dynamic(cm_info $cm) {
if ($cm->customdata) {
if ($cm->get_custom_data()) {
// the field 'customdata' is not empty IF AND ONLY IF we display contens inline
$cm->set_no_view_link();
}
+110
View File
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache data source for the lesson overrides.
*
* @package mod_lesson
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_lesson\cache;
use cache_definition;
/**
* Class lesson_overrides
*
* @package mod_lesson
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overrides implements \cache_data_source {
/** @var overrides the singleton instance of this class. */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return object
*/
public static function get_instance_for_cache(cache_definition $definition): overrides {
if (is_null(self::$instance)) {
self::$instance = new overrides();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return mixed What ever data should be returned, or false if it can't be loaded.
* @throws \coding_exception
*/
public function load_for_cache($key) {
global $DB;
[$lessonid, $ug, $ugid] = explode('_', $key);
$lessonid = (int) $lessonid;
switch ($ug) {
case 'u':
$userid = (int) $ugid;
$override = $DB->get_record(
'lesson_overrides',
['lessonid' => $lessonid, 'userid' => $userid],
'available, deadline, timelimit, review, maxattempts, retake, password'
);
break;
case 'g':
$groupid = (int) $ugid;
$override = $DB->get_record(
'lesson_overrides',
['lessonid' => $lessonid, 'groupid' => $groupid],
'available, deadline, timelimit, review, maxattempts, retake, password'
);
break;
default:
throw new \coding_exception('Invalid cache key');
}
// Return null instead of false, because false will not be cached.
return $override ?: null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys) {
$results = [];
foreach ($keys as $key) {
$results[] = $this->load_for_cache($key);
}
return $results;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for fetching the important dates in mod_lesson for a given module instance and a user.
*
* @package mod_lesson
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_lesson;
use core\activity_dates;
/**
* Class for fetching the important dates in mod_lesson for a given module instance and a user.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates extends activity_dates {
/**
* Returns a list of important dates in mod_lesson
*
* @return array
*/
protected function get_dates(): array {
$timeopen = $this->cm->customdata['available'] ?? null;
$timeclose = $this->cm->customdata['deadline'] ?? null;
$now = time();
$dates = [];
if ($timeopen) {
$openlabelid = $timeopen > $now ? 'activitydate:opens' : 'activitydate:opened';
$dates[] = [
'label' => get_string($openlabelid, 'course'),
'timestamp' => (int) $timeopen,
];
}
if ($timeclose) {
$closelabelid = $timeclose > $now ? 'activitydate:closes' : 'activitydate:closed';
$dates[] = [
'label' => get_string($closelabelid, 'course'),
'timestamp' => (int) $timeclose,
];
}
return $dates;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defined caches used internally by the plugin.
*
* @package mod_lesson
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
$definitions = [
'overrides' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'datasource' => '\mod_lesson\cache\overrides',
],
];
+1
View File
@@ -83,6 +83,7 @@ $string['averagescore'] = 'Average score';
$string['averagetime'] = 'Average time';
$string['branch'] = 'Content';
$string['branchtable'] = 'Content';
$string['cachedef_overrides'] = 'User and group override information';
$string['cancel'] = 'Cancel';
$string['cannotfindanswer'] = 'Error: could not find answer';
$string['cannotfindattempt'] = 'Error: could not find attempt';
+81 -7
View File
@@ -925,23 +925,27 @@ function lesson_reset_userdata($data) {
$status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false);
}
$purgeoverrides = false;
// Remove user overrides.
if (!empty($data->reset_lesson_user_overrides)) {
$DB->delete_records_select('lesson_overrides',
'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
$status[] = array(
'component' => $componentstr,
'item' => get_string('useroverridesdeleted', 'lesson'),
'error' => false);
'component' => $componentstr,
'item' => get_string('useroverridesdeleted', 'lesson'),
'error' => false);
$purgeoverrides = true;
}
// Remove group overrides.
if (!empty($data->reset_lesson_group_overrides)) {
$DB->delete_records_select('lesson_overrides',
'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
$status[] = array(
'component' => $componentstr,
'item' => get_string('groupoverridesdeleted', 'lesson'),
'error' => false);
'component' => $componentstr,
'item' => get_string('groupoverridesdeleted', 'lesson'),
'error' => false);
$purgeoverrides = true;
}
/// updating dates - shift may be negative too
if ($data->timeshift) {
@@ -954,12 +958,18 @@ function lesson_reset_userdata($data) {
WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?)
AND deadline <> 0", array($data->timeshift, $data->courseid));
$purgeoverrides = true;
// Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
// See MDL-9367.
shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid);
$status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
}
if ($purgeoverrides) {
cache::make('mod_lesson', 'overrides')->purge();
}
return $status;
}
@@ -1497,7 +1507,7 @@ function lesson_get_coursemodule_info($coursemodule) {
global $DB;
$dbparams = ['id' => $coursemodule->instance];
$fields = 'id, name, intro, introformat, completionendreached, completiontimespent';
$fields = 'id, name, intro, introformat, completionendreached, completiontimespent, available, deadline';
if (!$lesson = $DB->get_record('lesson', $dbparams, $fields)) {
return false;
}
@@ -1516,9 +1526,73 @@ function lesson_get_coursemodule_info($coursemodule) {
$result->customdata['customcompletionrules']['completiontimespent'] = $lesson->completiontimespent;
}
// Populate some other values that can be used in calendar or on dashboard.
if ($lesson->available) {
$result->customdata['available'] = $lesson->available;
}
if ($lesson->deadline) {
$result->customdata['deadline'] = $lesson->deadline;
}
return $result;
}
/**
* Sets dynamic information about a course module
*
* This function is called from cm_info when displaying the module
*
* @param cm_info $cm
*/
function mod_lesson_cm_info_dynamic(cm_info $cm) {
global $USER;
$cache = cache::make('mod_lesson', 'overrides');
$override = $cache->get("{$cm->instance}_u_{$USER->id}");
if (!$override) {
$override = (object) [
'available' => null,
'deadline' => null,
];
}
// No need to look for group overrides if there are user overrides for both available and deadline.
if (is_null($override->available) || is_null($override->deadline)) {
$availables = [];
$deadlines = [];
$groupings = groups_get_user_groups($cm->course, $USER->id);
foreach ($groupings[0] as $groupid) {
$groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
if (isset($groupoverride->available)) {
$availables[] = $groupoverride->available;
}
if (isset($groupoverride->deadline)) {
$deadlines[] = $groupoverride->deadline;
}
}
// If there is a user override for a setting, ignore the group override.
if (is_null($override->available) && count($availables)) {
$override->available = min($availables);
}
if (is_null($override->deadline) && count($deadlines)) {
if (in_array(0, $deadlines)) {
$override->deadline = 0;
} else {
$override->deadline = max($deadlines);
}
}
}
// Populate some other values that can be used in calendar or on dashboard.
if (!is_null($override->available)) {
$cm->override_customdata('available', $override->available);
}
if (!is_null($override->deadline)) {
$cm->override_customdata('deadline', $override->deadline);
}
}
/**
* Callback which returns human-readable strings describing the active completion custom rules for the module instance.
*
+10 -3
View File
@@ -657,14 +657,14 @@ function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
if ($groupid) {
$params['groupid'] = $groupid;
// We just update the group that was deleted.
$sql = "SELECT o.id, o.lessonid
$sql = "SELECT o.id, o.lessonid, o.groupid
FROM {lesson_overrides} o
JOIN {lesson} lesson ON lesson.id = o.lessonid
WHERE lesson.course = :courseid
AND o.groupid = :groupid";
} else {
// No groupid, we update all orphaned group overrides for all lessons in course.
$sql = "SELECT o.id, o.lessonid
$sql = "SELECT o.id, o.lessonid, o.groupid
FROM {lesson_overrides} o
JOIN {lesson} lesson ON lesson.id = o.lessonid
LEFT JOIN {groups} grp ON grp.id = o.groupid
@@ -672,11 +672,15 @@ function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
AND o.groupid IS NOT NULL
AND grp.id IS NULL";
}
$records = $DB->get_records_sql_menu($sql, $params);
$records = $DB->get_records_sql($sql, $params);
if (!$records) {
return; // Nothing to do.
}
$DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
$cache = cache::make('mod_lesson', 'overrides');
foreach ($records as $record) {
$cache->delete("{$record->lessonid}_g_{$record->groupid}");
}
}
/**
@@ -1740,8 +1744,10 @@ class lesson extends lesson_base {
'instance' => $this->properties->id);
if (isset($override->userid)) {
$conds['userid'] = $override->userid;
$cachekey = "{$cm->instance}_u_{$override->userid}";
} else {
$conds['groupid'] = $override->groupid;
$cachekey = "{$cm->instance}_g_{$override->groupid}";
}
$events = $DB->get_records('event', $conds);
foreach ($events as $event) {
@@ -1750,6 +1756,7 @@ class lesson extends lesson_base {
}
$DB->delete_records('lesson_overrides', array('id' => $overrideid));
cache::make('mod_lesson', 'overrides')->delete($cachekey);
// Set the common parameters for one of the events we will be triggering.
$params = array(
+4
View File
@@ -175,6 +175,8 @@ if ($mform->is_cancelled()) {
if (!empty($override->id)) {
$fromform->id = $override->id;
$DB->update_record('lesson_overrides', $fromform);
$cachekey = $groupmode ? "{$fromform->lessonid}_g_{$fromform->groupid}" : "{$fromform->lessonid}_u_{$fromform->userid}";
cache::make('mod_lesson', 'overrides')->delete($cachekey);
// Determine which override updated event to fire.
$params['objectid'] = $override->id;
@@ -191,6 +193,8 @@ if ($mform->is_cancelled()) {
} else {
unset($fromform->id);
$fromform->id = $DB->insert_record('lesson_overrides', $fromform);
$cachekey = $groupmode ? "{$fromform->lessonid}_g_{$fromform->groupid}" : "{$fromform->lessonid}_u_{$fromform->userid}";
cache::make('mod_lesson', 'overrides')->delete($cachekey);
// Determine which override created event to fire.
$params['objectid'] = $fromform->id;
+175
View File
@@ -0,0 +1,175 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains unit tests for mod_lesson\dates.
*
* @package mod_lesson
* @category test
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_lesson;
use advanced_testcase;
use cm_info;
use core\activity_dates;
/**
* Class for unit testing mod_lesson\dates.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_test extends advanced_testcase {
/**
* Data provider for get_dates_for_module().
* @return array[]
*/
public function get_dates_for_module_provider(): array {
$now = time();
$before = $now - DAYSECS;
$earlier = $before - DAYSECS;
$after = $now + DAYSECS;
$later = $after + DAYSECS;
return [
'without any dates' => [
null, null, null, null, null, null, []
],
'only with opening time' => [
$after, null, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after],
]
],
'only with closing time' => [
null, $after, null, null, null, null, [
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after],
]
],
'with both times' => [
$after, $later, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'between the dates' => [
$before, $after, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $before],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after],
]
],
'dates are past' => [
$earlier, $before, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closed', 'course'), 'timestamp' => $before],
]
],
'with user override' => [
$before, $after, $earlier, $later, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'with group override' => [
$before, $after, null, null, $earlier, $later, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'with both user and group overrides' => [
$before, $after, $earlier, $later, $earlier - DAYSECS, $later + DAYSECS, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
];
}
/**
* Test for get_dates_for_module().
*
* @dataProvider get_dates_for_module_provider
* @param int|null $available The 'available from' value of the lesson.
* @param int|null $deadline The lesson's deadline.
* @param int|null $useravailable The user override for opening the lesson.
* @param int|null $userdeadline The user override for deadline of the lesson.
* @param int|null $groupavailable The group override for opening the lesson.
* @param int|null $groupuserdeadline The group override for deadline of the lesson.
* @param array $expected The expected value of calling get_dates_for_module()
*/
public function test_get_dates_for_module(?int $available, ?int $deadline,
?int $useravailable, ?int $userdeadline,
?int $groupavailable, ?int $groupuserdeadline,
array $expected) {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var \mod_lesson_generator $lessongenerator */
$lessongenerator = $generator->get_plugin_generator('mod_lesson');
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$data = ['course' => $course->id];
if ($available) {
$data['available'] = $available;
}
if ($deadline) {
$data['deadline'] = $deadline;
}
$this->setAdminUser();
$lesson = $lessongenerator->create_instance($data);
if ($useravailable || $userdeadline || $groupavailable || $groupuserdeadline) {
$generator->enrol_user($user->id, $course->id);
$group = $generator->create_group(['courseid' => $course->id]);
$generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
if ($useravailable || $userdeadline) {
$lessongenerator->create_override([
'lessonid' => $lesson->id,
'userid' => $user->id,
'available' => $useravailable,
'deadline' => $userdeadline,
]);
}
if ($groupavailable || $groupuserdeadline) {
$lessongenerator->create_override([
'lessonid' => $lesson->id,
'groupid' => $group->id,
'available' => $groupavailable,
'deadline' => $groupuserdeadline,
]);
}
}
$this->setUser($user);
$cm = get_coursemodule_from_instance('lesson', $lesson->id);
// Make sure we're using a cm_info object.
$cm = cm_info::create($cm);
$dates = activity_dates::get_dates_for_module($cm, (int) $user->id);
$this->assertEquals($expected, $dates);
}
}
+23
View File
@@ -426,4 +426,27 @@ class mod_lesson_generator extends testing_module_generator {
$page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
}
/**
* Create a lesson override (either user or group).
*
* @param array $data must specify lessonid, and one of userid or groupid.
*/
public function create_override(array $data): void {
global $DB;
if (!isset($data['lessonid'])) {
throw new coding_exception('Must specify lessonid when creating a lesson override.');
}
if (!isset($data['userid']) && !isset($data['groupid'])) {
throw new coding_exception('Must specify one of userid or groupid when creating a lesson override.');
}
if (isset($data['userid']) && isset($data['groupid'])) {
throw new coding_exception('Cannot specify both userid and groupid when creating a lesson override.');
}
$DB->insert_record('lesson_overrides', (object) $data);
}
}
+1 -1
View File
@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2020110900; // The current module version (Date: YYYYMMDDXX)
$plugin->version = 2021042700; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2020110300; // Requires this Moodle version
$plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
+110
View File
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache data source for the quiz overrides.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz\cache;
use cache_definition;
/**
* Class quiz_overrides
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overrides implements \cache_data_source {
/** @var overrides the singleton instance of this class. */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return object
*/
public static function get_instance_for_cache(cache_definition $definition): overrides {
if (is_null(self::$instance)) {
self::$instance = new overrides();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return mixed What ever data should be returned, or false if it can't be loaded.
* @throws \coding_exception
*/
public function load_for_cache($key) {
global $DB;
[$quizid, $ug, $ugid] = explode('_', $key);
$quizid = (int) $quizid;
switch ($ug) {
case 'u':
$userid = (int) $ugid;
$override = $DB->get_record(
'quiz_overrides',
['quiz' => $quizid, 'userid' => $userid],
'timeopen, timeclose, timelimit, attempts, password'
);
break;
case 'g':
$groupid = (int) $ugid;
$override = $DB->get_record(
'quiz_overrides',
['quiz' => $quizid, 'groupid' => $groupid],
'timeopen, timeclose, timelimit, attempts, password'
);
break;
default:
throw new \coding_exception('Invalid cache key');
}
// Return null instead of false, because false will not be cached.
return $override ?: null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys) {
$results = [];
foreach ($keys as $key) {
$results[] = $this->load_for_cache($key);
}
return $results;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for fetching the important dates in mod_quiz for a given module instance and a user.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz;
use core\activity_dates;
/**
* Class for fetching the important dates in mod_quiz for a given module instance and a user.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates extends activity_dates {
/**
* Returns a list of important dates in mod_quiz
*
* @return array
*/
protected function get_dates(): array {
$timeopen = $this->cm->customdata['timeopen'] ?? null;
$timeclose = $this->cm->customdata['timeclose'] ?? null;
$now = time();
$dates = [];
if ($timeopen) {
$openlabelid = $timeopen > $now ? 'activitydate:opens' : 'activitydate:opened';
$dates[] = [
'label' => get_string($openlabelid, 'core_course'),
'timestamp' => (int) $timeopen,
];
}
if ($timeclose) {
$closelabelid = $timeclose > $now ? 'activitydate:closes' : 'activitydate:closed';
$dates[] = [
'label' => get_string($closelabelid, 'core_course'),
'timestamp' => (int) $timeclose,
];
}
return $dates;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defined caches used internally by the plugin.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
$definitions = [
'overrides' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'datasource' => '\mod_quiz\cache\overrides',
],
];
+1
View File
@@ -131,6 +131,7 @@ $string['browsersecurity_help'] = 'If "Full screen pop-up with some JavaScript s
* The quiz will only start if the student has a JavaScript-enabled web-browser
* The quiz appears in a full screen popup window that covers all the other windows and has no navigation controls
* Students are prevented, as far as is possible, from using facilities like copy and paste';
$string['cachedef_overrides'] = 'User and group override information';
$string['calculated'] = 'Calculated';
$string['calculatedquestion'] = 'Calculated question not supported at line {$a}. The question will be ignored';
$string['cannotcreatepath'] = 'Path cannot be created ({$a})';
+79 -1
View File
@@ -234,10 +234,12 @@ function quiz_delete_override($quiz, $overrideid, $log = true) {
// Create the search array for a group override.
$eventsearcharray = array('modulename' => 'quiz',
'instance' => $quiz->id, 'groupid' => (int)$override->groupid);
$cachekey = "{$quiz->id}_g_{$override->groupid}";
} else {
// Create the search array for a user override.
$eventsearcharray = array('modulename' => 'quiz',
'instance' => $quiz->id, 'userid' => (int)$override->userid);
$cachekey = "{$quiz->id}_u_{$override->userid}";
}
$events = $DB->get_records('event', $eventsearcharray);
foreach ($events as $event) {
@@ -246,6 +248,7 @@ function quiz_delete_override($quiz, $overrideid, $log = true) {
}
$DB->delete_records('quiz_overrides', array('id' => $overrideid));
cache::make('mod_quiz', 'overrides')->delete($cachekey);
if ($log) {
// Set the common parameters for one of the events we will be triggering.
@@ -1538,6 +1541,8 @@ function quiz_reset_userdata($data) {
'error' => false);
}
$purgeoverrides = false;
// Remove user overrides.
if (!empty($data->reset_quiz_user_overrides)) {
$DB->delete_records_select('quiz_overrides',
@@ -1546,6 +1551,7 @@ function quiz_reset_userdata($data) {
'component' => $componentstr,
'item' => get_string('useroverridesdeleted', 'quiz'),
'error' => false);
$purgeoverrides = true;
}
// Remove group overrides.
if (!empty($data->reset_quiz_group_overrides)) {
@@ -1555,6 +1561,7 @@ function quiz_reset_userdata($data) {
'component' => $componentstr,
'item' => get_string('groupoverridesdeleted', 'quiz'),
'error' => false);
$purgeoverrides = true;
}
// Updating dates - shift may be negative too.
@@ -1568,6 +1575,8 @@ function quiz_reset_userdata($data) {
WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
AND timeclose <> 0", array($data->timeshift, $data->courseid));
$purgeoverrides = true;
// Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
// See MDL-9367.
shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
@@ -1579,6 +1588,10 @@ function quiz_reset_userdata($data) {
'error' => false);
}
if ($purgeoverrides) {
cache::make('mod_quiz', 'overrides')->purge();
}
return $status;
}
@@ -2079,7 +2092,8 @@ function quiz_get_coursemodule_info($coursemodule) {
global $DB;
$dbparams = ['id' => $coursemodule->instance];
$fields = 'id, name, intro, introformat, completionattemptsexhausted, completionpass, completionminattempts';
$fields = 'id, name, intro, introformat, completionattemptsexhausted, completionpass, completionminattempts,
timeopen, timeclose';
if (!$quiz = $DB->get_record('quiz', $dbparams, $fields)) {
return false;
}
@@ -2106,9 +2120,73 @@ function quiz_get_coursemodule_info($coursemodule) {
$result->customdata['customcompletionrules']['completionminattempts'] = $quiz->completionminattempts;
}
// Populate some other values that can be used in calendar or on dashboard.
if ($quiz->timeopen) {
$result->customdata['timeopen'] = $quiz->timeopen;
}
if ($quiz->timeclose) {
$result->customdata['timeclose'] = $quiz->timeclose;
}
return $result;
}
/**
* Sets dynamic information about a course module
*
* This function is called from cm_info when displaying the module
*
* @param cm_info $cm
*/
function mod_quiz_cm_info_dynamic(cm_info $cm) {
global $USER;
$cache = cache::make('mod_quiz', 'overrides');
$override = $cache->get("{$cm->instance}_u_{$USER->id}");
if (!$override) {
$override = (object) [
'timeopen' => null,
'timeclose' => null,
];
}
// No need to look for group overrides if there are user overrides for both timeopen and timeclose.
if (is_null($override->timeopen) || is_null($override->timeclose)) {
$opens = [];
$closes = [];
$groupings = groups_get_user_groups($cm->course, $USER->id);
foreach ($groupings[0] as $groupid) {
$groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
if (isset($groupoverride->timeopen)) {
$opens[] = $groupoverride->timeopen;
}
if (isset($groupoverride->timeclose)) {
$closes[] = $groupoverride->timeclose;
}
}
// If there is a user override for a setting, ignore the group override.
if (is_null($override->timeopen) && count($opens)) {
$override->timeopen = min($opens);
}
if (is_null($override->timeclose) && count($closes)) {
if (in_array(0, $closes)) {
$override->timeclose = 0;
} else {
$override->timeclose = max($closes);
}
}
}
// Populate some other values that can be used in calendar or on dashboard.
if (!is_null($override->timeopen)) {
$cm->override_customdata('timeopen', $override->timeopen);
}
if (!is_null($override->timeclose)) {
$cm->override_customdata('timeclose', $override->timeclose);
}
}
/**
* Callback which returns human-readable strings describing the active completion custom rules for the module instance.
*
+7 -3
View File
@@ -1923,7 +1923,7 @@ function quiz_process_group_deleted_in_course($courseid) {
// It would be nice if we got the groupid that was deleted.
// Instead, we just update all quizzes with orphaned group overrides.
$sql = "SELECT o.id, o.quiz
$sql = "SELECT o.id, o.quiz, o.groupid
FROM {quiz_overrides} o
JOIN {quiz} quiz ON quiz.id = o.quiz
LEFT JOIN {groups} grp ON grp.id = o.groupid
@@ -1931,12 +1931,16 @@ function quiz_process_group_deleted_in_course($courseid) {
AND o.groupid IS NOT NULL
AND grp.id IS NULL";
$params = array('courseid' => $courseid);
$records = $DB->get_records_sql_menu($sql, $params);
$records = $DB->get_records_sql($sql, $params);
if (!$records) {
return; // Nothing to do.
}
$DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
quiz_update_open_attempts(array('quizid' => array_unique(array_values($records))));
$cache = cache::make('mod_quiz', 'overrides');
foreach ($records as $record) {
$cache->delete("{$record->quiz}_g_{$record->groupid}");
}
quiz_update_open_attempts(['quizid' => array_unique(array_column($records, 'quiz'))]);
}
/**
+4
View File
@@ -176,6 +176,8 @@ if ($mform->is_cancelled()) {
if (!empty($override->id)) {
$fromform->id = $override->id;
$DB->update_record('quiz_overrides', $fromform);
$cachekey = $groupmode ? "{$fromform->quiz}_g_{$fromform->groupid}" : "{$fromform->quiz}_u_{$fromform->userid}";
cache::make('mod_quiz', 'overrides')->delete($cachekey);
// Determine which override updated event to fire.
$params['objectid'] = $override->id;
@@ -192,6 +194,8 @@ if ($mform->is_cancelled()) {
} else {
unset($fromform->id);
$fromform->id = $DB->insert_record('quiz_overrides', $fromform);
$cachekey = $groupmode ? "{$fromform->quiz}_g_{$fromform->groupid}" : "{$fromform->quiz}_u_{$fromform->userid}";
cache::make('mod_quiz', 'overrides')->delete($cachekey);
// Determine which override created event to fire.
$params['objectid'] = $fromform->id;
+175
View File
@@ -0,0 +1,175 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains unit tests for mod_quiz\dates.
*
* @package mod_quiz
* @category test
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz;
use advanced_testcase;
use cm_info;
use core\activity_dates;
/**
* Class for unit testing mod_quiz\dates.
*
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_test extends advanced_testcase {
/**
* Data provider for get_dates_for_module().
* @return array[]
*/
public function get_dates_for_module_provider(): array {
$now = time();
$before = $now - DAYSECS;
$earlier = $before - DAYSECS;
$after = $now + DAYSECS;
$later = $after + DAYSECS;
return [
'without any dates' => [
null, null, null, null, null, null, []
],
'only with opening time' => [
$after, null, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after],
]
],
'only with closing time' => [
null, $after, null, null, null, null, [
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after],
]
],
'with both times' => [
$after, $later, null, null, null, null, [
['label' => get_string('activitydate:opens', 'course'), 'timestamp' => $after],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'between the dates' => [
$before, $after, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $before],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $after],
]
],
'dates are past' => [
$earlier, $before, null, null, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closed', 'course'), 'timestamp' => $before],
]
],
'with user override' => [
$before, $after, $earlier, $later, null, null, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'with group override' => [
$before, $after, null, null, $earlier, $later, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
'with both user and group overrides' => [
$before, $after, $earlier, $later, $earlier - DAYSECS, $later + DAYSECS, [
['label' => get_string('activitydate:opened', 'course'), 'timestamp' => $earlier],
['label' => get_string('activitydate:closes', 'course'), 'timestamp' => $later],
]
],
];
}
/**
* Test for get_dates_for_module().
*
* @dataProvider get_dates_for_module_provider
* @param int|null $timeopen Time of opening the quiz.
* @param int|null $timeclose Time of closing the quiz.
* @param int|null $usertimeopen The user override for opening the quiz.
* @param int|null $usertimeclose The user override for closing the quiz.
* @param int|null $grouptimeopen The group override for opening the quiz.
* @param int|null $grouptimeclose The group override for closing the quiz.
* @param array $expected The expected value of calling get_dates_for_module()
*/
public function test_get_dates_for_module(?int $timeopen, ?int $timeclose,
?int $usertimeopen, ?int $usertimeclose,
?int $grouptimeopen, ?int $grouptimeclose,
array $expected) {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$data = ['course' => $course->id];
if ($timeopen) {
$data['timeopen'] = $timeopen;
}
if ($timeclose) {
$data['timeclose'] = $timeclose;
}
$quiz = $quizgenerator->create_instance($data);
if ($usertimeopen || $usertimeclose || $grouptimeopen || $grouptimeclose) {
$generator->enrol_user($user->id, $course->id);
$group = $generator->create_group(['courseid' => $course->id]);
$generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
if ($usertimeopen || $usertimeclose) {
$quizgenerator->create_override([
'quiz' => $quiz->id,
'userid' => $user->id,
'timeopen' => $usertimeopen,
'timeclose' => $usertimeclose,
]);
}
if ($grouptimeopen || $grouptimeclose) {
$quizgenerator->create_override([
'quiz' => $quiz->id,
'groupid' => $group->id,
'timeopen' => $grouptimeopen,
'timeclose' => $grouptimeclose,
]);
}
}
$this->setUser($user);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Make sure we're using a cm_info object.
$cm = cm_info::create($cm);
$dates = activity_dates::get_dates_for_module($cm, (int) $user->id);
$this->assertEquals($expected, $dates);
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2020121100;
$plugin->version = 2021042700;
$plugin->requires = 2020110300;
$plugin->component = 'mod_quiz';