diff --git a/mod/lesson/backup/moodle1/lib.php b/mod/lesson/backup/moodle1/lib.php index 5a9b0d2ce30..72796d5304f 100644 --- a/mod/lesson/backup/moodle1/lib.php +++ b/mod/lesson/backup/moodle1/lib.php @@ -233,6 +233,9 @@ class moodle1_mod_lesson_handler extends moodle1_mod_handler { * This is executed when we reach the closing tag of our 'lesson' path */ public function on_lesson_end() { + // Append empty subpath element. + $this->write_xml('overrides', array()); + // finish writing lesson.xml $this->xmlwriter->end_tag('lesson'); $this->xmlwriter->end_tag('activity'); diff --git a/mod/lesson/backup/moodle2/backup_lesson_stepslib.php b/mod/lesson/backup/moodle2/backup_lesson_stepslib.php index 037353d6e7c..78ca35981df 100644 --- a/mod/lesson/backup/moodle2/backup_lesson_stepslib.php +++ b/mod/lesson/backup/moodle2/backup_lesson_stepslib.php @@ -145,6 +145,11 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st 'userid', 'starttime', 'lessontime', 'completed' )); + $overrides = new backup_nested_element('overrides'); + $override = new backup_nested_element('override', array('id'), array( + 'groupid', 'userid', 'available', 'deadline', 'timelimit', + 'review', 'maxattempts', 'retake', 'password')); + // Now that we have all of the elements created we've got to put them // together correctly. $lesson->add_child($pages); @@ -161,6 +166,8 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st $highscores->add_child($highscore); $lesson->add_child($timers); $timers->add_child($timer); + $lesson->add_child($overrides); + $overrides->add_child($override); // Set the source table for the elements that aren't reliant on the user // at this point (lesson, lesson_pages, lesson_answers) @@ -171,6 +178,9 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st // We use SQL here as answers must be ordered by id so that the restore gets them in the right order $answer->set_source_table('lesson_answers', array('pageid' => backup::VAR_PARENTID), 'id ASC'); + // Lesson overrides to backup are different depending of user info. + $overrideparams = array('lessonid' => backup::VAR_PARENTID); + // Check if we are also backing up user information if ($this->get_setting_value('userinfo')) { // Set the source table for elements that are reliant on the user @@ -180,7 +190,9 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st $grade->set_source_table('lesson_grades', array('lessonid'=>backup::VAR_PARENTID)); $highscore->set_source_table('lesson_high_scores', array('lessonid' => backup::VAR_PARENTID)); $timer->set_source_table('lesson_timer', array('lessonid' => backup::VAR_PARENTID)); + $overrideparams['userid'] = backup_helper::is_sqlparam(null); // Without userinfo, skip user overrides. } + $override->set_source_table('lesson_overrides', $overrideparams); // Annotate the user id's where required. $attempt->annotate_ids('user', 'userid'); @@ -188,6 +200,8 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st $grade->annotate_ids('user', 'userid'); $highscore->annotate_ids('user', 'userid'); $timer->annotate_ids('user', 'userid'); + $override->annotate_ids('user', 'userid'); + $override->annotate_ids('group', 'groupid'); // Annotate the file areas in user by the lesson module. $lesson->annotate_files('mod_lesson', 'intro', null); diff --git a/mod/lesson/backup/moodle2/restore_lesson_stepslib.php b/mod/lesson/backup/moodle2/restore_lesson_stepslib.php index b79b5e671eb..5129183eba0 100644 --- a/mod/lesson/backup/moodle2/restore_lesson_stepslib.php +++ b/mod/lesson/backup/moodle2/restore_lesson_stepslib.php @@ -48,6 +48,7 @@ class restore_lesson_activity_structure_step extends restore_activity_structure_ $paths[] = new restore_path_element('lesson_branch', '/activity/lesson/pages/page/branches/branch'); $paths[] = new restore_path_element('lesson_highscore', '/activity/lesson/highscores/highscore'); $paths[] = new restore_path_element('lesson_timer', '/activity/lesson/timers/timer'); + $paths[] = new restore_path_element('lesson_override', '/activity/lesson/overrides/override'); } // Return the paths wrapped into standard activity structure @@ -204,6 +205,39 @@ class restore_lesson_activity_structure_step extends restore_activity_structure_ $newitemid = $DB->insert_record('lesson_timer', $data); } + /** + * Process a lesson override restore + * @param object $data The data in object form + * @return void + */ + protected function process_lesson_override($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Based on userinfo, we'll restore user overides or no. + $userinfo = $this->get_setting_value('userinfo'); + + // Skip user overrides if we are not restoring userinfo. + if (!$userinfo && !is_null($data->userid)) { + return; + } + + $data->lessonid = $this->get_new_parentid('lesson'); + + $data->userid = $this->get_mappingid('user', $data->userid); + $data->groupid = $this->get_mappingid('group', $data->groupid); + + $data->available = $this->apply_date_offset($data->available); + $data->deadline = $this->apply_date_offset($data->deadline); + + $newitemid = $DB->insert_record('lesson_overrides', $data); + + // Add mapping, restore of logs needs it. + $this->set_mapping('lesson_override', $oldid, $newitemid); + } + protected function after_execute() { global $DB; diff --git a/mod/lesson/classes/event/group_override_created.php b/mod/lesson/classes/event/group_override_created.php new file mode 100644 index 00000000000..1edc21bbb37 --- /dev/null +++ b/mod/lesson/classes/event/group_override_created.php @@ -0,0 +1,100 @@ +. + +/** + * The mod_lesson group override created event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson group override created event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * - int groupid: the id of the group. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_override_created extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverridecreated', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrideedit.php', array('id' => $this->objectid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + + if (!isset($this->other['groupid'])) { + throw new \coding_exception('The \'groupid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/event/group_override_deleted.php b/mod/lesson/classes/event/group_override_deleted.php new file mode 100644 index 00000000000..16700850afa --- /dev/null +++ b/mod/lesson/classes/event/group_override_deleted.php @@ -0,0 +1,99 @@ +. + +/** + * The mod_lesson group override deleted event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson group override deleted event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * - int groupid: the id of the group. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_override_deleted extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverridedeleted', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrides.php', array('cmid' => $this->contextinstanceid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + + if (!isset($this->other['groupid'])) { + throw new \coding_exception('The \'groupid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/event/group_override_updated.php b/mod/lesson/classes/event/group_override_updated.php new file mode 100644 index 00000000000..7096cd11b42 --- /dev/null +++ b/mod/lesson/classes/event/group_override_updated.php @@ -0,0 +1,99 @@ +. + +/** + * The mod_lesson group override updated event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson group override updated event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * - int groupid: the id of the group. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_override_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverrideupdated', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrideedit.php', array('id' => $this->objectid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + + if (!isset($this->other['groupid'])) { + throw new \coding_exception('The \'groupid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/event/user_override_created.php b/mod/lesson/classes/event/user_override_created.php new file mode 100644 index 00000000000..9e47d57cb92 --- /dev/null +++ b/mod/lesson/classes/event/user_override_created.php @@ -0,0 +1,98 @@ +. + +/** + * The mod_lesson user override created event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson user override created event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_override_created extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverridecreated', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrideedit.php', array('id' => $this->objectid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/event/user_override_deleted.php b/mod/lesson/classes/event/user_override_deleted.php new file mode 100644 index 00000000000..6fc90aac5d3 --- /dev/null +++ b/mod/lesson/classes/event/user_override_deleted.php @@ -0,0 +1,98 @@ +. + +/** + * The mod_lesson user override deleted event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson user override deleted event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_override_deleted extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverridedeleted', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrides.php', array('cmid' => $this->contextinstanceid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/event/user_override_updated.php b/mod/lesson/classes/event/user_override_updated.php new file mode 100644 index 00000000000..4c30e4bb85a --- /dev/null +++ b/mod/lesson/classes/event/user_override_updated.php @@ -0,0 +1,99 @@ +. + +/** + * The mod_lesson user override updated event. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_lesson\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_lesson user override updated event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int lessonid: the id of the lesson. + * } + * + * @package mod_lesson + * @since Moodle 2.9 + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_override_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'lesson_overrides'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventoverrideupdated', 'mod_lesson'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the override with id '$this->objectid' for the lesson with " . + "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/lesson/overrideedit.php', array('id' => $this->objectid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['lessonid'])) { + throw new \coding_exception('The \'lessonid\' value must be set in other.'); + } + } +} diff --git a/mod/lesson/classes/group_observers.php b/mod/lesson/classes/group_observers.php new file mode 100644 index 00000000000..3b925042bc8 --- /dev/null +++ b/mod/lesson/classes/group_observers.php @@ -0,0 +1,85 @@ +. + +/** + * Group observers. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_lesson; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/lesson/locallib.php'); + +/** + * Group observers class. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_observers { + + /** + * Flag whether a course reset is in progress or not. + * + * @var int The course ID. + */ + protected static $resetinprogress = false; + + /** + * A course reset has started. + * + * @param \core\event\base $event The event. + * @return void + */ + public static function course_reset_started($event) { + self::$resetinprogress = $event->courseid; + } + + /** + * A course reset has ended. + * + * @param \core\event\base $event The event. + * @return void + */ + public static function course_reset_ended($event) { + if (!empty(self::$resetinprogress)) { + if (!empty($event->other['reset_options']['reset_groups_remove'])) { + lesson_process_group_deleted_in_course($event->courseid); + } + } + + self::$resetinprogress = null; + } + + /** + * A group was deleted. + * + * @param \core\event\base $event The event. + * @return void + */ + public static function group_deleted($event) { + if (!empty(self::$resetinprogress)) { + // We will take care of that once the course reset ends. + return; + } + lesson_process_group_deleted_in_course($event->courseid); + } +} diff --git a/mod/lesson/continue.php b/mod/lesson/continue.php index 8dbac306cef..e2d683a4da6 100644 --- a/mod/lesson/continue.php +++ b/mod/lesson/continue.php @@ -36,6 +36,9 @@ $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*' require_login($course, false, $cm); require_sesskey(); +// Apply overrides. +$lesson->update_effective_access($USER->id); + $context = context_module::instance($cm->id); $canmanage = has_capability('mod/lesson:manage', $context); $lessonoutput = $PAGE->get_renderer('mod_lesson'); diff --git a/mod/lesson/db/access.php b/mod/lesson/db/access.php index 9ede52ed477..5f8d7f7d0bc 100644 --- a/mod/lesson/db/access.php +++ b/mod/lesson/db/access.php @@ -85,5 +85,15 @@ $capabilities = array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) - ) + ), + + // Edit the lesson overrides. + 'mod/lesson:manageoverrides' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), ); diff --git a/mod/lesson/db/events.php b/mod/lesson/db/events.php new file mode 100644 index 00000000000..ca9f07eefe4 --- /dev/null +++ b/mod/lesson/db/events.php @@ -0,0 +1,43 @@ +. + +/** + * Add event handlers for the lesson + * + * @package mod_lesson + * @category event + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +$observers = array( + + array( + 'eventname' => '\core\event\course_reset_started', + 'callback' => '\mod_lesson\group_observers::course_reset_started', + ), + array( + 'eventname' => '\core\event\course_reset_ended', + 'callback' => '\mod_lesson\group_observers::course_reset_ended', + ), + array( + 'eventname' => '\core\event\group_deleted', + 'callback' => '\mod_lesson\group_observers::group_deleted' + ), +); diff --git a/mod/lesson/db/install.xml b/mod/lesson/db/install.xml index df004de57ed..a9440ef466c 100644 --- a/mod/lesson/db/install.xml +++ b/mod/lesson/db/install.xml @@ -192,5 +192,26 @@ + + + + + + + + + + + + + + + + + + + + +
diff --git a/mod/lesson/db/upgrade.php b/mod/lesson/db/upgrade.php index d35ee634c8b..c26d0053aeb 100644 --- a/mod/lesson/db/upgrade.php +++ b/mod/lesson/db/upgrade.php @@ -263,5 +263,38 @@ function xmldb_lesson_upgrade($oldversion) { upgrade_mod_savepoint(true, 2015032700, 'lesson'); } + if ($oldversion < 2015033100) { + + // Define table lesson_overrides to be created. + $table = new xmldb_table('lesson_overrides'); + + // Adding fields to table lesson_overrides. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('lessonid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('groupid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('available', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('deadline', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timelimit', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('review', XMLDB_TYPE_INTEGER, '3', null, null, null, null); + $table->add_field('maxattempts', XMLDB_TYPE_INTEGER, '3', null, null, null, null); + $table->add_field('retake', XMLDB_TYPE_INTEGER, '3', null, null, null, null); + $table->add_field('password', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Adding keys to table lesson_overrides. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('lessonid', XMLDB_KEY_FOREIGN, array('lessonid'), 'lesson', array('id')); + $table->add_key('groupid', XMLDB_KEY_FOREIGN, array('groupid'), 'groups', array('id')); + $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); + + // Conditionally launch create table for lesson_overrides. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Lesson savepoint reached. + upgrade_mod_savepoint(true, 2015033100, 'lesson'); + } + return true; } diff --git a/mod/lesson/essay.php b/mod/lesson/essay.php index 468bd98d849..ee2fb4c5d2d 100644 --- a/mod/lesson/essay.php +++ b/mod/lesson/essay.php @@ -62,6 +62,8 @@ if ($attemptid > 0) { $attempt = $DB->get_record('lesson_attempts', array('id' => $attemptid)); $answer = $DB->get_record('lesson_answers', array('lessonid' => $lesson->id, 'pageid' => $attempt->pageid)); $user = $DB->get_record('user', array('id' => $attempt->userid)); + // Apply overrides. + $lesson->update_effective_access($user->id); $scoreoptions = array(); if ($lesson->custom) { $i = $answer->score; diff --git a/mod/lesson/lang/en/lesson.php b/mod/lesson/lang/en/lesson.php index 19c5f4f2058..f0e24bd3767 100644 --- a/mod/lesson/lang/en/lesson.php +++ b/mod/lesson/lang/en/lesson.php @@ -48,6 +48,8 @@ $string['addedaquestionpage'] = 'Added a question page'; $string['addedcluster'] = 'Added a cluster'; $string['addedendofcluster'] = 'Added an end of cluster'; $string['addendofcluster'] = 'Add an end of cluster'; +$string['addnewgroupoverride'] = 'Add group override'; +$string['addnewuseroverride'] = 'Add user override'; $string['addpage'] = 'Add a page'; $string['and'] = 'AND'; $string['anchortitle'] = 'Start of main content'; @@ -87,6 +89,7 @@ $string['casesensitive_help'] = 'Tick the checkbox to use regular expressions fo $string['classstats'] = 'Class statistics'; $string['clicktodownload'] = 'Click on the following link to download the file.'; $string['clicktopost'] = 'Click here to post your grade on the High Scores list.'; +$string['closebeforeopen'] = 'Could not update the lesson. You have specified a close date before the open date.'; $string['cluster'] = 'Cluster'; $string['clusterjump'] = 'Unseen question within a cluster'; $string['clustertitle'] = 'Cluster'; @@ -136,6 +139,7 @@ $string['description'] = 'Description'; $string['detailedstats'] = 'Detailed statistics'; $string['didnotanswerquestion'] = 'Did not answer this question.'; $string['didnotreceivecredit'] = 'Did not receive credit'; +$string['disabled'] = 'Disabled'; $string['displaydefaultfeedback'] = 'Use default feedback'; $string['displaydefaultfeedback_help'] = 'If enabled, when a response is not found for a particular question, the default response of "That\'s the correct answer" or "That\'s the wrong answer" will be shown.'; $string['displayhighscores'] = 'Display high scores'; @@ -154,12 +158,14 @@ $string['displayscorewithoutessays'] = 'Your score is {$a->score} (out of {$a->g $string['edit'] = 'Edit'; $string['editingquestionpage'] = 'Editing {$a} question page'; $string['editlessonsettings'] = 'Edit lesson settings'; +$string['editoverride'] = 'Edit override'; $string['editpage'] = 'Edit page contents'; $string['editpagecontent'] = 'Edit page contents'; $string['email'] = 'Email'; $string['emailallgradedessays'] = 'Email ALL graded essays'; $string['emailgradedessays'] = 'Email graded essays'; $string['emailsuccess'] = 'Emails sent successfully'; +$string['enabled'] = 'Enabled'; $string['endofbranch'] = 'End of branch'; $string['endofcluster'] = 'End of cluster'; $string['endofclustertitle'] = 'End of cluster'; @@ -185,6 +191,9 @@ $string['eventlessonended'] = 'Lesson ended'; $string['eventlessonrestarted'] = 'Lesson restarted'; $string['eventlessonresumed'] = 'Lesson resumed'; $string['eventlessonstarted'] = 'Lesson started'; +$string['eventoverridecreated'] = 'Lesson override created'; +$string['eventoverridedeleted'] = 'Lesson override deleted'; +$string['eventoverrideupdated'] = 'Lesson override updated'; $string['eventpagecreated'] = 'Page created'; $string['eventpagemoved'] = 'Page moved'; $string['eventpageupdated'] = 'Page updated'; @@ -208,6 +217,9 @@ $string['gradebetterthanerror'] = 'Earn a grade better than {$a} percent'; $string['gradeessay'] = 'Grade essay questions ({$a->notgradedcount} not graded and {$a->notsentcount} not sent)'; $string['gradeis'] = 'Grade is {$a}'; $string['gradeoptions'] = 'Grade options'; +$string['groupoverrides'] = 'Group overrides'; +$string['groupoverridesdeleted'] = 'Group overrides deleted'; +$string['groupsnone'] = 'There are no groups in this course'; $string['handlingofretakes'] = 'Handling of re-takes'; $string['handlingofretakes_help'] = 'If re-takes are allowed, this setting specifies whether the grade for the lesson is the mean or maximum of all attempts.'; $string['havenotgradedyet'] = 'Have not graded yet.'; @@ -226,6 +238,7 @@ $string['insertedpage'] = 'Inserted page'; $string['invalidfile'] = 'Invalid file'; $string['invalidid'] = 'No course module ID or lesson ID were passed'; $string['invalidlessonid'] = 'lesson ID was incorrect'; +$string['invalidoverrideid'] = 'Invalid override id'; $string['invalidpageid'] = 'Invalid page ID'; $string['jump'] = 'Jump'; $string['jumps'] = 'Jumps'; @@ -243,6 +256,7 @@ $string['lessoncloseson'] = 'Lesson closes on {$a}'; $string['lesson:edit'] = 'Edit a lesson activity'; $string['lessonformating'] = 'Lesson formatting'; $string['lesson:manage'] = 'Manage a lesson activity'; +$string['lesson:manageoverrides'] = 'Manage lesson overrides'; $string['lesson:viewreports'] = 'View lesson reports'; $string['lessonmenu'] = 'Lesson menu'; $string['lessonnotready'] = 'This lesson is not ready to be taken. Please contact your {$a}.'; @@ -310,6 +324,7 @@ $string['nextpage'] = 'Next page'; $string['noanswer'] = 'One or more questions have no answer given. Please go back and submit an answer.'; $string['noattemptrecordsfound'] = 'No attempt records found: no grade given'; $string['nobranchtablefound'] = 'No content page found'; +$string['noclose'] = 'No close date'; $string['nocommentyet'] = 'No comment yet.'; $string['nocoursemods'] = 'No activities found'; $string['nocredit'] = 'No credit'; @@ -318,12 +333,15 @@ $string['noessayquestionsfound'] = 'No essay questions found in this lesson.'; $string['nohighscores'] = 'No high scores'; $string['nolessonattempts'] = 'No attempts have been made on this lesson.'; $string['nolessonattemptsgroup'] = 'No attempts have been made by {$a} group members on this lesson.'; +$string['none'] = 'None'; $string['nooneansweredcorrectly'] = 'No one answered correctly.'; $string['nooneansweredthisquestion'] = 'No one answered this question.'; $string['nooneenteredthis'] = 'No one entered this.'; $string['noonehasanswered'] = 'No one has answered an essay question yet.'; $string['noonehasansweredgroup'] = 'No one in {$a} has answered an essay question yet.'; $string['noonecheckedthis'] = 'No one checked this.'; +$string['noopen'] = 'No open date'; +$string['nooverridedata'] = 'You must override at least one of the lesson settings.'; $string['noretake'] = 'You are not allowed to retake this lesson.'; $string['normal'] = 'Normal - follow lesson path'; $string['notcompleted'] = 'Not completed'; @@ -351,6 +369,14 @@ $string['or'] = 'OR'; $string['ordered'] = 'Ordered'; $string['other'] = 'Other'; $string['outof'] = 'Out of {$a}'; +$string['overridedeletegroupsure'] = 'Are you sure you want to delete the override for group {$a}?'; +$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?'; +$string['override'] = 'Override'; +$string['overridegroup'] = 'Override group'; +$string['overridegroupeventname'] = '{$a->lesson} - {$a->group}'; +$string['overrides'] = 'Overrides'; +$string['overrideuser'] = 'Override user'; +$string['overrideusereventname'] = '{$a->lesson} - Override'; $string['overview'] = 'Overview'; $string['overview_help'] = 'A lesson is made up of a number of pages and optionally content pages. A page contains some content and usually ends with a question. Associated with each answer to the question is a jump. The jump can be relative, such as this page or next page, or absolute, specifying any one of the pages in the lesson. A content page is a page containing a set of links to other pages in the lesson, for example a Table of Contents.'; $string['page'] = 'Page: {$a}'; @@ -398,6 +424,8 @@ $string['rank'] = 'Rank'; $string['rawgrade'] = 'Raw grade'; $string['receivedcredit'] = 'Received credit'; $string['redisplaypage'] = 'Redisplay page'; +$string['removeallgroupoverrides'] = 'Delete all group overrides'; +$string['removealluseroverrides'] = 'Delete all user overrides'; $string['report'] = 'Report'; $string['reports'] = 'Reports'; $string['response'] = 'Response'; @@ -405,13 +433,16 @@ $string['retakesallowed'] = 'Re-takes allowed'; $string['retakesallowed_help'] = 'If enabled, students can attempt the lesson more than once.'; $string['returnto'] = 'Return to {$a}'; $string['returntocourse'] = 'Return to the course'; +$string['reverttodefaults'] = 'Revert to lesson defaults'; $string['review'] = 'Review'; $string['reviewlesson'] = 'Review lesson'; $string['reviewquestionback'] = 'Yes, I\'d like to try again'; $string['reviewquestioncontinue'] = 'No, I just want to go on to the next question'; $string['sanitycheckfailed'] = 'Sanity check failed: This attempt has been deleted'; +$string['save'] = 'Save'; $string['savechanges'] = 'Save changes'; $string['savechangesandeol'] = 'Save all changes and go to the end of the lesson.'; +$string['saveoverrideandstay'] = 'Save and enter another override'; $string['savepage'] = 'Save page'; $string['score'] = 'Score'; $string['score_help'] = 'Score is only used when custom scoring is enabled. Each answer can then be given a numerical point value (positive or negative).'; @@ -464,6 +495,9 @@ $string['usemaximum'] = 'Use maximum'; $string['usemean'] = 'Use mean'; $string['usepassword'] = 'Password protected lesson'; $string['usepassword_help'] = 'If enabled, a password is required in order to access the lesson.'; +$string['useroverridesdeleted'] = 'User overrides deleted'; +$string['usersnone'] = 'No students have access to this lesson'; +$string['useroverrides'] = 'User overrides'; $string['viewgrades'] = 'View grades'; $string['viewhighscores'] = 'View high scores list'; $string['viewreports'] = 'View {$a->attempts} completed {$a->student} attempts'; diff --git a/mod/lesson/lib.php b/mod/lesson/lib.php index 8e6aa7c119f..f7d068d16a4 100644 --- a/mod/lesson/lib.php +++ b/mod/lesson/lib.php @@ -95,6 +95,166 @@ function lesson_update_instance($data, $mform) { return true; } +/** + * This function updates the events associated to the lesson. + * If $override is non-zero, then it updates only the events + * associated with the specified override. + * + * @uses LESSON_MAX_EVENT_LENGTH + * @param object $lesson the lesson object. + * @param object $override (optional) limit to a specific override + */ +function lesson_update_events($lesson, $override = null) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/calendar/lib.php'); + + // Load the old events relating to this lesson. + $conds = array('modulename' => 'lesson', + 'instance' => $lesson->id); + if (!empty($override)) { + // Only load events for this override. + if (isset($override->userid)) { + $conds['userid'] = $override->userid; + } else { + $conds['groupid'] = $override->groupid; + } + } + $oldevents = $DB->get_records('event', $conds); + + // Now make a todo list of all that needs to be updated. + if (empty($override)) { + // We are updating the primary settings for the lesson, so we + // need to add all the overrides. + $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $lesson->id)); + // As well as the original lesson (empty override). + $overrides[] = new stdClass(); + } else { + // Just do the one override. + $overrides = array($override); + } + + foreach ($overrides as $current) { + $groupid = isset($current->groupid) ? $current->groupid : 0; + $userid = isset($current->userid) ? $current->userid : 0; + $available = isset($current->available) ? $current->available : $lesson->available; + $deadline = isset($current->deadline) ? $current->deadline : $lesson->deadline; + + // Only add open/close events for an override if they differ from the lesson default. + $addopen = empty($current->id) || !empty($current->available); + $addclose = empty($current->id) || !empty($current->deadline); + + if (!empty($lesson->coursemodule)) { + $cmid = $lesson->coursemodule; + } else { + $cmid = get_coursemodule_from_instance('lesson', $lesson->id, $lesson->course)->id; + } + + $event = new stdClass(); + $event->description = format_module_intro('lesson', $lesson, $cmid); + // Events module won't show user events when the courseid is nonzero. + $event->courseid = ($userid) ? 0 : $lesson->course; + $event->groupid = $groupid; + $event->userid = $userid; + $event->modulename = 'lesson'; + $event->instance = $lesson->id; + $event->timestart = $available; + $event->timeduration = max($deadline - $available, 0); + $event->visible = instance_is_visible('lesson', $lesson); + $event->eventtype = 'open'; + + // Determine the event name. + if ($groupid) { + $params = new stdClass(); + $params->lesson = $lesson->name; + $params->group = groups_get_group_name($groupid); + if ($params->group === false) { + // Group doesn't exist, just skip it. + continue; + } + $eventname = get_string('overridegroupeventname', 'lesson', $params); + } else if ($userid) { + $params = new stdClass(); + $params->lesson = $lesson->name; + $eventname = get_string('overrideusereventname', 'lesson', $params); + } else { + $eventname = $lesson->name; + } + if ($addopen or $addclose) { + if ($deadline and $available and $event->timeduration <= LESSON_MAX_EVENT_LENGTH) { + // Single event for the whole lesson. + if ($oldevent = array_shift($oldevents)) { + $event->id = $oldevent->id; + } else { + unset($event->id); + } + $event->name = $eventname; + // The method calendar_event::create will reuse a db record if the id field is set. + calendar_event::create($event); + } else { + // Separate start and end events. + $event->timeduration = 0; + if ($available && $addopen) { + if ($oldevent = array_shift($oldevents)) { + $event->id = $oldevent->id; + } else { + unset($event->id); + } + $event->name = $eventname.' ('.get_string('lessonopens', 'lesson').')'; + // The method calendar_event::create will reuse a db record if the id field is set. + calendar_event::create($event); + } + if ($deadline && $addclose) { + if ($oldevent = array_shift($oldevents)) { + $event->id = $oldevent->id; + } else { + unset($event->id); + } + $event->name = $eventname.' ('.get_string('lessoncloses', 'lesson').')'; + $event->timestart = $deadline; + $event->eventtype = 'close'; + calendar_event::create($event); + } + } + } + } + + // Delete any leftover events. + foreach ($oldevents as $badevent) { + $badevent = calendar_event::load($badevent); + $badevent->delete(); + } +} + +/** + * This standard function will check all instances of this module + * and make sure there are up-to-date events created for each of them. + * If courseid = 0, then every lesson event in the site is checked, else + * only lesson events belonging to the course specified are checked. + * This function is used, in its new format, by restore_refresh_events() + * + * @param int $courseid + * @return bool + */ +function lesson_refresh_events($courseid = 0) { + global $DB; + + if ($courseid == 0) { + if (!$lessons = $DB->get_records('lessons')) { + return true; + } + } else { + if (!$lessons = $DB->get_records('lesson', array('course' => $courseid))) { + return true; + } + } + + foreach ($lessons as $lesson) { + lesson_update_events($lesson); + } + + return true; +} /** * Given an ID of an instance of this module, @@ -572,50 +732,8 @@ function lesson_process_pre_save(&$lesson) { * @return void **/ function lesson_process_post_save(&$lesson) { - global $DB, $CFG; - require_once($CFG->dirroot.'/calendar/lib.php'); - require_once($CFG->dirroot . '/mod/lesson/locallib.php'); - - if ($events = $DB->get_records('event', array('modulename'=>'lesson', 'instance'=>$lesson->id))) { - foreach($events as $event) { - $event = calendar_event::load($event->id); - $event->delete(); - } - } - - $event = new stdClass; - $event->description = $lesson->name; - $event->courseid = $lesson->course; - $event->groupid = 0; - $event->userid = 0; - $event->modulename = 'lesson'; - $event->instance = $lesson->id; - $event->eventtype = 'open'; - $event->timestart = $lesson->available; - - $event->visible = instance_is_visible('lesson', $lesson); - - $event->timeduration = ($lesson->deadline - $lesson->available); - - if ($lesson->deadline and $lesson->available and $event->timeduration <= LESSON_MAX_EVENT_LENGTH) { - // Single event for the whole lesson. - $event->name = $lesson->name; - calendar_event::create(clone($event)); - } else { - // Separate start and end events. - $event->timeduration = 0; - if ($lesson->available) { - $event->name = $lesson->name.' ('.get_string('lessonopens', 'lesson').')'; - calendar_event::create(clone($event)); - } - - if ($lesson->deadline) { - $event->name = $lesson->name.' ('.get_string('lessoncloses', 'lesson').')'; - $event->timestart = $lesson->deadline; - $event->eventtype = 'close'; - calendar_event::create(clone($event)); - } - } + // Update the events relating to this lesson. + lesson_update_events($lesson); } @@ -628,6 +746,10 @@ function lesson_process_post_save(&$lesson) { function lesson_reset_course_form_definition(&$mform) { $mform->addElement('header', 'lessonheader', get_string('modulenameplural', 'lesson')); $mform->addElement('advcheckbox', 'reset_lesson', get_string('deleteallattempts','lesson')); + $mform->addElement('advcheckbox', 'reset_lesson_user_overrides', + get_string('removealluseroverrides', 'lesson')); + $mform->addElement('advcheckbox', 'reset_lesson_group_overrides', + get_string('removeallgroupoverrides', 'lesson')); } /** @@ -636,7 +758,9 @@ function lesson_reset_course_form_definition(&$mform) { * @return array */ function lesson_reset_course_form_defaults($course) { - return array('reset_lesson'=>1); + return array('reset_lesson' => 1, + 'reset_lesson_group_overrides' => 1, + 'reset_lesson_user_overrides' => 1); } /** @@ -710,8 +834,35 @@ function lesson_reset_userdata($data) { $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>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); + } + // 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); + } /// updating dates - shift may be negative too if ($data->timeshift) { + $DB->execute("UPDATE {lesson_overrides} + SET available = available + ? + WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?) + AND available <> 0", array($data->timeshift, $data->courseid)); + $DB->execute("UPDATE {lesson_overrides} + SET deadline = deadline + ? + WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?) + AND deadline <> 0", array($data->timeshift, $data->courseid)); + shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid); $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false); } @@ -820,6 +971,30 @@ function lesson_get_completion_state($course, $cm, $userid, $type) { function lesson_extend_settings_navigation($settings, $lessonnode) { global $PAGE, $DB; + // We want to add these new nodes after the Edit settings node, and before the + // Locally assigned roles node. Of course, both of those are controlled by capabilities. + $keys = $lessonnode->get_children_key_list(); + $beforekey = null; + $i = array_search('modedit', $keys); + if ($i === false and array_key_exists(0, $keys)) { + $beforekey = $keys[0]; + } else if (array_key_exists($i + 1, $keys)) { + $beforekey = $keys[$i + 1]; + } + + if (has_capability('mod/lesson:manageoverrides', $PAGE->cm->context)) { + $url = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $PAGE->cm->id)); + $node = navigation_node::create(get_string('groupoverrides', 'lesson'), + new moodle_url($url, array('mode' => 'group')), + navigation_node::TYPE_SETTING, null, 'mod_lesson_groupoverrides'); + $lessonnode->add_node($node, $beforekey); + + $node = navigation_node::create(get_string('useroverrides', 'lesson'), + new moodle_url($url, array('mode' => 'user')), + navigation_node::TYPE_SETTING, null, 'mod_lesson_useroverrides'); + $lessonnode->add_node($node, $beforekey); + } + if (has_capability('mod/lesson:edit', $PAGE->cm->context)) { $url = new moodle_url('/mod/lesson/view.php', array('id' => $PAGE->cm->id)); $lessonnode->add(get_string('preview', 'lesson'), $url); diff --git a/mod/lesson/locallib.php b/mod/lesson/locallib.php index 3525e103001..df016675ab7 100644 --- a/mod/lesson/locallib.php +++ b/mod/lesson/locallib.php @@ -638,6 +638,31 @@ function lesson_get_media_html($lesson, $context) { return $code; } +/** + * Logic to happen when a/some group(s) has/have been deleted in a course. + * + * @param int $courseid The course ID. + * @return void + */ +function lesson_process_group_deleted_in_course($courseid) { + global $DB; + + // It would be nice if we got the groupid that was deleted. + // Instead, we just update all lesson with orphaned group overrides. + $sql = "SELECT o.id, o.lesson + FROM {lesson_overrides} o + JOIN {lesson} lesson ON lesson.id = o.lessonid + LEFT JOIN {groups} grp ON grp.id = o.groupid + WHERE lesson.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); + if (!$records) { + return; // Nothing to do. + } + $DB->delete_records_list('lesson_overrides', 'id', array_keys($records)); +} /** * Abstract class that page type's MUST inherit from. @@ -988,6 +1013,8 @@ class lesson extends lesson_base { $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); $context = context_module::instance($cm->id); + $this->delete_all_overrides(); + $DB->delete_records("lesson", array("id"=>$this->properties->id)); $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id)); @@ -1011,6 +1038,204 @@ class lesson extends lesson_base { return true; } + /** + * Deletes a lesson override from the database and clears any corresponding calendar events + * + * @param int $overrideid The id of the override being deleted + * @return bool true on success + */ + public function delete_override($overrideid) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/calendar/lib.php'); + + $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); + + $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST); + + // Delete the events. + $conds = array('modulename' => 'lesson', + 'instance' => $this->properties->id); + if (isset($override->userid)) { + $conds['userid'] = $override->userid; + } else { + $conds['groupid'] = $override->groupid; + } + $events = $DB->get_records('event', $conds); + foreach ($events as $event) { + $eventold = calendar_event::load($event); + $eventold->delete(); + } + + $DB->delete_records('lesson_overrides', array('id' => $overrideid)); + + // Set the common parameters for one of the events we will be triggering. + $params = array( + 'objectid' => $override->id, + 'context' => context_module::instance($cm->id), + 'other' => array( + 'lessonid' => $override->lessonid + ) + ); + // Determine which override deleted event to fire. + if (!empty($override->userid)) { + $params['relateduserid'] = $override->userid; + $event = \mod_lesson\event\user_override_deleted::create($params); + } else { + $params['other']['groupid'] = $override->groupid; + $event = \mod_lesson\event\group_override_deleted::create($params); + } + + // Trigger the override deleted event. + $event->add_record_snapshot('lesson_overrides', $override); + $event->trigger(); + + return true; + } + + /** + * Deletes all lesson overrides from the database and clears any corresponding calendar events + */ + public function delete_all_overrides() { + global $DB; + + $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id'); + foreach ($overrides as $override) { + $this->delete_override($override->id); + } + } + + /** + * Updates the lesson properties with override information for a user. + * + * Algorithm: For each lesson setting, if there is a matching user-specific override, + * then use that otherwise, if there are group-specific overrides, return the most + * lenient combination of them. If neither applies, leave the quiz setting unchanged. + * + * Special case: if there is more than one password that applies to the user, then + * lesson->extrapasswords will contain an array of strings giving the remaining + * passwords. + * + * @param int $userid The userid. + */ + public function update_effective_access($userid) { + global $DB; + + // Check for user override. + $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid)); + + if (!$override) { + $override = new stdClass(); + $override->available = null; + $override->deadline = null; + $override->timelimit = null; + $override->review = null; + $override->maxattempts = null; + $override->retake = null; + $override->password = null; + } + + // Check for group overrides. + $groupings = groups_get_user_groups($this->properties->course, $userid); + + if (!empty($groupings[0])) { + // Select all overrides that apply to the User's groups. + list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); + $sql = "SELECT * FROM {lesson_overrides} + WHERE groupid $extra AND lessonid = ?"; + $params[] = $this->properties->id; + $records = $DB->get_records_sql($sql, $params); + + // Combine the overrides. + $availables = array(); + $deadlines = array(); + $timelimits = array(); + $reviews = array(); + $attempts = array(); + $retakes = array(); + $passwords = array(); + + foreach ($records as $gpoverride) { + if (isset($gpoverride->available)) { + $availables[] = $gpoverride->available; + } + if (isset($gpoverride->deadline)) { + $deadlines[] = $gpoverride->deadline; + } + if (isset($gpoverride->timelimit)) { + $timelimits[] = $gpoverride->timelimit; + } + if (isset($gpoverride->review)) { + $reviews[] = $gpoverride->review; + } + if (isset($gpoverride->maxattempts)) { + $attempts[] = $gpoverride->maxattempts; + } + if (isset($gpoverride->retake)) { + $retakes[] = $gpoverride->retake; + } + if (isset($gpoverride->password)) { + $passwords[] = $gpoverride->password; + } + } + // 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); + } + } + if (is_null($override->timelimit) && count($timelimits)) { + if (in_array(0, $timelimits)) { + $override->timelimit = 0; + } else { + $override->timelimit = max($timelimits); + } + } + if (is_null($override->review) && count($reviews)) { + $override->review = max($reviews); + } + if (is_null($override->maxattempts) && count($attempts)) { + $override->maxattempts = max($attempts); + } + if (is_null($override->retake) && count($retakes)) { + $override->retake = max($retakes); + } + if (is_null($override->password) && count($passwords)) { + $override->password = array_shift($passwords); + if (count($passwords)) { + $override->extrapasswords = $passwords; + } + } + + } + + // Merge with lesson defaults. + $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake'); + foreach ($keys as $key) { + if (isset($override->{$key})) { + $this->properties->{$key} = $override->{$key}; + } + } + + // Special handling of lesson usepassword and password. + if (isset($override->password)) { + if ($override->password == '') { + $this->properties->usepassword = 0; + } else { + $this->properties->usepassword = 1; + $this->properties->password = $override->password; + if (isset($override->extrapasswords)) { + $this->properties->extrapasswords = $override->extrapasswords; + } + } + } + } + /** * Fetches messages from the session that may have been set in previous page * actions. @@ -1452,7 +1677,7 @@ class lesson extends lesson_base { * @return string */ public function time_remaining($starttime) { - $timeleft = $starttime + $this->timelimit - time(); + $timeleft = $starttime + $this->properties->timelimit - time(); $hours = floor($timeleft/3600); $timeleft = $timeleft - ($hours * 3600); $minutes = floor($timeleft/60); diff --git a/mod/lesson/mediafile.php b/mod/lesson/mediafile.php index 71240f9bc29..faf3d317430 100644 --- a/mod/lesson/mediafile.php +++ b/mod/lesson/mediafile.php @@ -38,6 +38,10 @@ $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*' require_login($course, false, $cm); + +// Apply overrides. +$lesson->update_effective_access($USER->id); + $context = context_module::instance($cm->id); $canmanage = has_capability('mod/lesson:manage', $context); @@ -86,11 +90,25 @@ if (!$canmanage) { if (!empty($userpassword) && (($lesson->password == md5(trim($userpassword))) || ($lesson->password == trim($userpassword)))) { // with or without md5 for backward compatibility (MDL-11090) $USER->lessonloggedin[$lesson->id] = true; + $correctpass = true; if ($lesson->highscores) { // Logged in - redirect so we go through all of these checks before starting the lesson. redirect("$CFG->wwwroot/mod/lesson/view.php?id=$cm->id"); } - } else { + } else if (isset($lesson->extrapasswords)) { + // Group overrides may have additional passwords. + foreach ($lesson->extrapasswords as $password) { + if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) { + $correctpass = true; + $USER->lessonloggedin[$lesson->id] = true; + if ($lesson->highscores) { + // Logged in - redirect so we go through all of these checks before starting the lesson. + redirect("$CFG->wwwroot/mod/lesson/view.php?id=$cm->id"); + } + } + } + } + if (!$correctpass) { echo $lessonoutput->header($lesson, $cm); echo $lessonoutput->login_prompt($lesson, $userpassword !== ''); echo $lessonoutput->footer(); diff --git a/mod/lesson/override_form.php b/mod/lesson/override_form.php new file mode 100644 index 00000000000..1e457aaf78b --- /dev/null +++ b/mod/lesson/override_form.php @@ -0,0 +1,287 @@ +. + +/** + * Settings form for overrides in the lesson module. + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/mod/lesson/mod_form.php'); + + +/** + * Form for editing settings overrides. + * + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lesson_override_form extends moodleform { + + /** @var object course module object. */ + protected $cm; + + /** @var object the lesson settings object. */ + protected $lesson; + + /** @var context the lesson context. */ + protected $context; + + /** @var bool editing group override (true) or user override (false). */ + protected $groupmode; + + /** @var int groupid, if provided. */ + protected $groupid; + + /** @var int userid, if provided. */ + protected $userid; + + /** + * Constructor. + * @param moodle_url $submiturl the form action URL. + * @param object $cm course module object. + * @param object $lesson the lesson settings object. + * @param object $context the lesson context. + * @param bool $groupmode editing group override (true) or user override (false). + * @param object $override the override being edited, if it already exists. + */ + public function __construct($submiturl, $cm, $lesson, $context, $groupmode, $override) { + + $this->cm = $cm; + $this->lesson = $lesson; + $this->context = $context; + $this->groupmode = $groupmode; + $this->groupid = empty($override->groupid) ? 0 : $override->groupid; + $this->userid = empty($override->userid) ? 0 : $override->userid; + + parent::__construct($submiturl, null, 'post'); + + } + + /** + * Define this form - called by the parent constructor + */ + protected function definition() { + global $CFG, $DB; + + $cm = $this->cm; + $mform = $this->_form; + + $mform->addElement('header', 'override', get_string('override', 'lesson')); + + if ($this->groupmode) { + // Group override. + if ($this->groupid) { + // There is already a groupid, so freeze the selector. + $groupchoices = array(); + $groupchoices[$this->groupid] = groups_get_group_name($this->groupid); + $mform->addElement('select', 'groupid', + get_string('overridegroup', 'lesson'), $groupchoices); + $mform->freeze('groupid'); + } else { + // Prepare the list of groups. + $groups = groups_get_all_groups($cm->course); + if (empty($groups)) { + // Generate an error. + $link = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $cm->id)); + print_error('groupsnone', 'lesson', $link); + } + + $groupchoices = array(); + foreach ($groups as $group) { + $groupchoices[$group->id] = $group->name; + } + unset($groups); + + if (count($groupchoices) == 0) { + $groupchoices[0] = get_string('none'); + } + + $mform->addElement('select', 'groupid', + get_string('overridegroup', 'lesson'), $groupchoices); + $mform->addRule('groupid', get_string('required'), 'required', null, 'client'); + } + } else { + // User override. + if ($this->userid) { + // There is already a userid, so freeze the selector. + $user = $DB->get_record('user', array('id' => $this->userid)); + $userchoices = array(); + $userchoices[$this->userid] = fullname($user); + $mform->addElement('select', 'userid', + get_string('overrideuser', 'lesson'), $userchoices); + $mform->freeze('userid'); + } else { + // Prepare the list of users. + $users = array(); + list($sort, $sortparams) = users_order_by_sql('u'); + if (!empty($sortparams)) { + throw new coding_exception('users_order_by_sql returned some query parameters. ' . + 'This is unexpected, and a problem because there is no way to pass these ' . + 'parameters to get_users_by_capability. See MDL-34657.'); + } + $users = get_enrolled_users($this->context, '', 0, + 'u.id, u.email, ' . get_all_user_name_fields(true, 'u'), + $sort); + + // Filter users based on any fixed restrictions (groups, profile). + $info = new \core_availability\info_module($cm); + $users = $info->filter_user_list($users); + + if (empty($users)) { + // Generate an error. + $link = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $cm->id)); + print_error('usersnone', 'lesson', $link); + } + + $userchoices = array(); + $canviewemail = in_array('email', get_extra_user_fields($this->context)); + foreach ($users as $id => $user) { + if (empty($invalidusers[$id]) || (!empty($override) && + $id == $override->userid)) { + if ($canviewemail) { + $userchoices[$id] = fullname($user) . ', ' . $user->email; + } else { + $userchoices[$id] = fullname($user); + } + } + } + unset($users); + + if (count($userchoices) == 0) { + $userchoices[0] = get_string('none'); + } + $mform->addElement('searchableselector', 'userid', + get_string('overrideuser', 'lesson'), $userchoices); + $mform->addRule('userid', get_string('required'), 'required', null, 'client'); + } + } + + // Password. + // This field has to be above the date and timelimit fields, + // otherwise browsers will clear it when those fields are changed. + $mform->addElement('passwordunmask', 'password', get_string('usepassword', 'lesson')); + $mform->setType('password', PARAM_TEXT); + $mform->addHelpButton('password', 'usepassword', 'lesson'); + $mform->setDefault('password', $this->lesson->password);; + + // Open and close dates. + $mform->addElement('date_time_selector', 'available', get_string('available', 'lesson'), array('optional' => true)); + $mform->setDefault('available', $this->lesson->available); + + $mform->addElement('date_time_selector', 'deadline', get_string('deadline', 'lesson'), array('optional' => true)); + $mform->setDefault('deadline', $this->lesson->deadline); + + // Lesson time limit. + $mform->addElement('duration', 'timelimit', + get_string('timelimit', 'lesson'), array('optional' => true)); + if ($this->lesson->timelimit != 0) { + $mform->setDefault('timelimit', 0); + } else { + $mform->setDefault('timelimit', $this->lesson->timelimit); + } + + // Try a question again. + $mform->addElement('selectyesno', 'review', get_string('displayreview', 'lesson')); + $mform->addHelpButton('review', 'displayreview', 'lesson'); + $mform->setDefault('review', $this->lesson->review); + + // Number of attempts. + $numbers = array(); + for ($i = 10; $i > 0; $i--) { + $numbers[$i] = $i; + } + $mform->addElement('select', 'maxattempts', get_string('maximumnumberofattempts', 'lesson'), $numbers); + $mform->addHelpButton('maxattempts', 'maximumnumberofattempts', 'lesson'); + $mform->setDefault('maxattempts', $this->lesson->maxattempts); + + // Retake allowed. + $mform->addElement('selectyesno', 'retake', get_string('retakesallowed', 'lesson')); + $mform->addHelpButton('retake', 'retakesallowed', 'lesson'); + $mform->setDefault('retake', $this->lesson->retake); + + // Submit buttons. + $mform->addElement('submit', 'resetbutton', + get_string('reverttodefaults', 'lesson')); + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'submitbutton', + get_string('save', 'lesson')); + $buttonarray[] = $mform->createElement('submit', 'againbutton', + get_string('saveoverrideandstay', 'lesson')); + $buttonarray[] = $mform->createElement('cancel'); + + $mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false); + $mform->closeHeaderBefore('buttonbar'); + + } + + /** + * Validate the submitted form data. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors + */ + public function validation($data, $files) { + global $COURSE, $DB; + $errors = parent::validation($data, $files); + + $mform =& $this->_form; + $lesson = $this->lesson; + + if ($mform->elementExists('userid')) { + if (empty($data['userid'])) { + $errors['userid'] = get_string('required'); + } + } + + if ($mform->elementExists('groupid')) { + if (empty($data['groupid'])) { + $errors['groupid'] = get_string('required'); + } + } + + // Ensure that the dates make sense. + if (!empty($data['available']) && !empty($data['deadline'])) { + if ($data['deadline'] < $data['available'] ) { + $errors['deadline'] = get_string('closebeforeopen', 'lesson'); + } + } + + // Ensure that at least one lesson setting was changed. + $changed = false; + $keys = array('available', 'deadline', 'review', 'timelimit', 'maxattempts', 'retake', 'password'); + foreach ($keys as $key) { + if ($data[$key] != $lesson->{$key}) { + $changed = true; + break; + } + } + + if (!$changed) { + $errors['available'] = get_string('nooverridedata', 'lesson'); + } + + return $errors; + } +} diff --git a/mod/lesson/overridedelete.php b/mod/lesson/overridedelete.php new file mode 100644 index 00000000000..ad8c4e151b9 --- /dev/null +++ b/mod/lesson/overridedelete.php @@ -0,0 +1,94 @@ +. + +/** + * This page handles deleting lesson overrides + * + * @package mod_lesson + * @copyright 2015 Jean-Michel vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot.'/mod/lesson/lib.php'); +require_once($CFG->dirroot.'/mod/lesson/locallib.php'); +require_once($CFG->dirroot.'/mod/lesson/override_form.php'); + +$overrideid = required_param('id', PARAM_INT); +$confirm = optional_param('confirm', false, PARAM_BOOL); + +if (! $override = $DB->get_record('lesson_overrides', array('id' => $overrideid))) { + print_error('invalidoverrideid', 'lesson'); +} + +$lesson = new lesson($DB->get_record('lesson', array('id' => $override->lessonid), '*', MUST_EXIST)); + +if (! $cm = get_coursemodule_from_instance("lesson", $lesson->id, $lesson->course)) { + print_error('invalidcoursemodule'); +} +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +$context = context_module::instance($cm->id); + +require_login($course, false, $cm); + +// Check the user has the required capabilities to modify an override. +require_capability('mod/lesson:manageoverrides', $context); + +$url = new moodle_url('/mod/lesson/overridedelete.php', array('id' => $override->id)); +$confirmurl = new moodle_url($url, array('id' => $override->id, 'confirm' => 1)); +$cancelurl = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $cm->id)); + +if (!empty($override->userid)) { + $cancelurl->param('mode', 'user'); +} + +// If confirm is set (PARAM_BOOL) then we have confirmation of intention to delete. +if ($confirm) { + require_sesskey(); + + $lesson->delete_override($override->id); + + redirect($cancelurl); +} + +// Prepare the page to show the confirmation form. +$stroverride = get_string('override', 'lesson'); +$title = get_string('deletecheck', null, $stroverride); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); +$PAGE->navbar->add($title); +$PAGE->set_title($title); +$PAGE->set_heading($course->fullname); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($lesson->name, true, array('context' => $context))); + +if ($override->groupid) { + $group = $DB->get_record('groups', array('id' => $override->groupid), 'id, name'); + $confirmstr = get_string("overridedeletegroupsure", "lesson", $group->name); +} else { + $namefields = get_all_user_name_fields(true); + $user = $DB->get_record('user', array('id' => $override->userid), + 'id, ' . $namefields); + $confirmstr = get_string("overridedeleteusersure", "lesson", fullname($user)); +} + +echo $OUTPUT->confirm($confirmstr, $confirmurl, $cancelurl); + +echo $OUTPUT->footer(); diff --git a/mod/lesson/overrideedit.php b/mod/lesson/overrideedit.php new file mode 100644 index 00000000000..e08b61e45eb --- /dev/null +++ b/mod/lesson/overrideedit.php @@ -0,0 +1,224 @@ +. + +/** + * This page handles editing and creation of lesson overrides + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot.'/mod/lesson/lib.php'); +require_once($CFG->dirroot.'/mod/lesson/locallib.php'); +require_once($CFG->dirroot.'/mod/lesson/override_form.php'); + + +$cmid = optional_param('cmid', 0, PARAM_INT); +$overrideid = optional_param('id', 0, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHA); +$reset = optional_param('reset', false, PARAM_BOOL); + +$override = null; +if ($overrideid) { + + if (! $override = $DB->get_record('lesson_overrides', array('id' => $overrideid))) { + print_error('invalidoverrideid', 'lesson'); + } + + $lesson = new lesson($DB->get_record('lesson', array('id' => $override->lessonid), '*', MUST_EXIST)); + + list($course, $cm) = get_course_and_cm_from_instance($lesson, 'lesson'); + +} else if ($cmid) { + list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'lesson'); + $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST)); + +} else { + print_error('invalidcoursemodule'); +} +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +$url = new moodle_url('/mod/lesson/overrideedit.php'); +if ($action) { + $url->param('action', $action); +} +if ($overrideid) { + $url->param('id', $overrideid); +} else { + $url->param('cmid', $cmid); +} + +$PAGE->set_url($url); + +require_login($course, false, $cm); + +$context = context_module::instance($cm->id); + +// Add or edit an override. +require_capability('mod/lesson:manageoverrides', $context); + +if ($overrideid) { + // Editing an override. + $data = clone $override; +} else { + // Creating a new override. + $data = new stdClass(); +} + +// Merge lesson defaults with data. +$keys = array('available', 'deadline', 'review', 'timelimit', 'maxattempts', 'retake', 'password'); +foreach ($keys as $key) { + if (!isset($data->{$key}) || $reset) { + $data->{$key} = $lesson->{$key}; + } +} + +// If we are duplicating an override, then clear the user/group and override id +// since they will change. +if ($action === 'duplicate') { + $override->id = null; + $override->userid = null; + $override->groupid = null; +} + +// True if group-based override. +$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid)); + +$overridelisturl = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $cm->id)); +if (!$groupmode) { + $overridelisturl->param('mode', 'user'); +} + +// Setup the form. +$mform = new lesson_override_form($url, $cm, $lesson, $context, $groupmode, $override); +$mform->set_data($data); + +if ($mform->is_cancelled()) { + redirect($overridelisturl); + +} else if (optional_param('resetbutton', 0, PARAM_ALPHA)) { + $url->param('reset', true); + redirect($url); + +} else if ($fromform = $mform->get_data()) { + // Process the data. + $fromform->lessonid = $lesson->id; + + // Replace unchanged values with null. + foreach ($keys as $key) { + if ($fromform->{$key} == $lesson->{$key}) { + $fromform->{$key} = null; + } + } + + // See if we are replacing an existing override. + $userorgroupchanged = false; + if (empty($override->id)) { + $userorgroupchanged = true; + } else if (!empty($fromform->userid)) { + $userorgroupchanged = $fromform->userid !== $override->userid; + } else { + $userorgroupchanged = $fromform->groupid !== $override->groupid; + } + + if ($userorgroupchanged) { + $conditions = array( + 'lessonid' => $lesson->id, + 'userid' => empty($fromform->userid) ? null : $fromform->userid, + 'groupid' => empty($fromform->groupid) ? null : $fromform->groupid); + if ($oldoverride = $DB->get_record('lesson_overrides', $conditions)) { + // There is an old override, so we merge any new settings on top of + // the older override. + foreach ($keys as $key) { + if (is_null($fromform->{$key})) { + $fromform->{$key} = $oldoverride->{$key}; + } + } + + $lesson->delete_override($oldoverride->id); + } + } + + // Set the common parameters for one of the events we may be triggering. + $params = array( + 'context' => $context, + 'other' => array( + 'lessonid' => $lesson->id + ) + ); + if (!empty($override->id)) { + $fromform->id = $override->id; + $DB->update_record('lesson_overrides', $fromform); + + // Determine which override updated event to fire. + $params['objectid'] = $override->id; + if (!$groupmode) { + $params['relateduserid'] = $fromform->userid; + $event = \mod_lesson\event\user_override_updated::create($params); + } else { + $params['other']['groupid'] = $fromform->groupid; + $event = \mod_lesson\event\group_override_updated::create($params); + } + + // Trigger the override updated event. + $event->trigger(); + } else { + unset($fromform->id); + $fromform->id = $DB->insert_record('lesson_overrides', $fromform); + + // Determine which override created event to fire. + $params['objectid'] = $fromform->id; + if (!$groupmode) { + $params['relateduserid'] = $fromform->userid; + $event = \mod_lesson\event\user_override_created::create($params); + } else { + $params['other']['groupid'] = $fromform->groupid; + $event = \mod_lesson\event\group_override_created::create($params); + } + + // Trigger the override created event. + $event->trigger(); + } + + lesson_update_events($lesson, $fromform); + + if (!empty($fromform->submitbutton)) { + redirect($overridelisturl); + } + + // The user pressed the 'again' button, so redirect back to this page. + $url->remove_params('cmid'); + $url->param('action', 'duplicate'); + $url->param('id', $fromform->id); + redirect($url); + +} + +// Print the form. +$pagetitle = get_string('editoverride', 'lesson'); +$PAGE->navbar->add($pagetitle); +$PAGE->set_pagelayout('admin'); +$PAGE->set_title($pagetitle); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($lesson->name, true, array('context' => $context))); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/mod/lesson/overrides.php b/mod/lesson/overrides.php new file mode 100644 index 00000000000..9382346b964 --- /dev/null +++ b/mod/lesson/overrides.php @@ -0,0 +1,295 @@ +. + +/** + * This page handles listing of lesson overrides + * + * @package mod_lesson + * @copyright 2015 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot.'/mod/lesson/lib.php'); +require_once($CFG->dirroot.'/mod/lesson/locallib.php'); +require_once($CFG->dirroot.'/mod/lesson/override_form.php'); + + +$cmid = required_param('cmid', PARAM_INT); +$mode = optional_param('mode', '', PARAM_ALPHA); // One of 'user' or 'group', default is 'group'. + +list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'lesson'); +$lesson = $DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST); + +// Get the course groups. +$groups = groups_get_all_groups($cm->course); +if ($groups === false) { + $groups = array(); +} + +// Default mode is "group", unless there are no groups. +if ($mode != "user" and $mode != "group") { + if (!empty($groups)) { + $mode = "group"; + } else { + $mode = "user"; + } +} +$groupmode = ($mode == "group"); + +$url = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $cm->id, 'mode' => $mode)); + +$PAGE->set_url($url); + +require_login($course, false, $cm); + +$context = context_module::instance($cm->id); + +// Check the user has the required capabilities to list overrides. +require_capability('mod/lesson:manageoverrides', $context); + +// Display a list of overrides. +$PAGE->set_pagelayout('admin'); +$PAGE->set_title(get_string('overrides', 'lesson')); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($lesson->name, true, array('context' => $context))); + +// Delete orphaned group overrides. +$sql = 'SELECT o.id + FROM {lesson_overrides} o LEFT JOIN {groups} g + ON o.groupid = g.id + WHERE o.groupid IS NOT NULL + AND g.id IS NULL + AND o.lessonid = ?'; +$params = array($lesson->id); +$orphaned = $DB->get_records_sql($sql, $params); +if (!empty($orphaned)) { + $DB->delete_records_list('lesson_overrides', 'id', array_keys($orphaned)); +} + +// Fetch all overrides. +if ($groupmode) { + $colname = get_string('group'); + $sql = 'SELECT o.*, g.name + FROM {lesson_overrides} o + JOIN {groups} g ON o.groupid = g.id + WHERE o.lessonid = :lessonid + ORDER BY g.name'; + $params = array('lessonid' => $lesson->id); +} else { + $colname = get_string('user'); + list($sort, $params) = users_order_by_sql('u'); + $sql = 'SELECT o.*, ' . get_all_user_name_fields(true, 'u') . ' + FROM {lesson_overrides} o + JOIN {user} u ON o.userid = u.id + WHERE o.lessonid = :lessonid + ORDER BY ' . $sort; + $params['lessonid'] = $lesson->id; +} + +$overrides = $DB->get_records_sql($sql, $params); + +// Initialise table. +$table = new html_table(); +$table->headspan = array(1, 2, 1); +$table->colclasses = array('colname', 'colsetting', 'colvalue', 'colaction'); +$table->head = array( + $colname, + get_string('overrides', 'lesson'), + get_string('action'), +); + +$userurl = new moodle_url('/user/view.php', array()); +$groupurl = new moodle_url('/group/overview.php', array('id' => $cm->course)); + +$overridedeleteurl = new moodle_url('/mod/lesson/overridedelete.php'); +$overrideediturl = new moodle_url('/mod/lesson/overrideedit.php'); + +$hasinactive = false; // Whether there are any inactive overrides. + +foreach ($overrides as $override) { + + $fields = array(); + $values = array(); + $active = true; + + // Check for inactive overrides. + if (!$groupmode) { + if (!is_enrolled($context, $override->userid)) { + // User not enrolled. + $active = false; + } else if (!\core_availability\info_module::is_user_visible($cm, $override->userid)) { + // User cannot access the module. + $active = false; + } + } + + // Format available. + if (isset($override->available)) { + $fields[] = get_string('lessonopens', 'lesson'); + $values[] = $override->available > 0 ? + userdate($override->available) : get_string('noopen', 'lesson'); + } + + // Format deadline. + if (isset($override->deadline)) { + $fields[] = get_string('lessoncloses', 'lesson'); + $values[] = $override->deadline > 0 ? + userdate($override->deadline) : get_string('noclose', 'lesson'); + } + + // Format timelimit. + if (isset($override->timelimit)) { + $fields[] = get_string('timelimit', 'lesson'); + $values[] = $override->timelimit > 0 ? + format_time($override->timelimit) : get_string('none', 'lesson'); + } + + // Format option to try a question again. + if (isset($override->review)) { + $fields[] = get_string('displayreview', 'lesson'); + $values[] = $override->review ? + get_string('yes') : get_string('no'); + } + + // Format number of attempts. + if (isset($override->maxattempts)) { + $fields[] = get_string('maximumnumberofattempts', 'lesson'); + $values[] = $override->maxattempts > 0 ? + $override->maxattempts : get_string('unlimited'); + } + + // Format retake allowed. + if (isset($override->retake)) { + $fields[] = get_string('retakesallowed', 'lesson'); + $values[] = $override->retake ? + get_string('yes') : get_string('no'); + } + + // Format password. + if (isset($override->password)) { + $fields[] = get_string('usepassword', 'lesson'); + $values[] = $override->password !== '' ? + get_string('enabled', 'lesson') : get_string('none', 'lesson'); + } + + // Icons. + $iconstr = ''; + + if ($active) { + // Edit. + $editurlstr = $overrideediturl->out(true, array('id' => $override->id)); + $iconstr = '' . + '' .
+                get_string('edit') . ' '; + // Duplicate. + $copyurlstr = $overrideediturl->out(true, + array('id' => $override->id, 'action' => 'duplicate')); + $iconstr .= '' . + '' .
+                get_string('copy') . ' '; + } + // Delete. + $deleteurlstr = $overridedeleteurl->out(true, + array('id' => $override->id, 'sesskey' => sesskey())); + $iconstr .= '' . + '' .
+            get_string('delete') . ' '; + + if ($groupmode) { + $usergroupstr = '' . $override->name . ''; + } else { + $usergroupstr = '' . fullname($override) . ''; + } + + $class = ''; + if (!$active) { + $class = "dimmed_text"; + $usergroupstr .= '*'; + $hasinactive = true; + } + + $usergroupcell = new html_table_cell(); + $usergroupcell->rowspan = count($fields); + $usergroupcell->text = $usergroupstr; + $actioncell = new html_table_cell(); + $actioncell->rowspan = count($fields); + $actioncell->text = $iconstr; + + for ($i = 0; $i < count($fields); ++$i) { + $row = new html_table_row(); + $row->attributes['class'] = $class; + if ($i == 0) { + $row->cells[] = $usergroupcell; + } + $cell1 = new html_table_cell(); + $cell1->text = $fields[$i]; + $row->cells[] = $cell1; + $cell2 = new html_table_cell(); + $cell2->text = $values[$i]; + $row->cells[] = $cell2; + if ($i == 0) { + $row->cells[] = $actioncell; + } + $table->data[] = $row; + } +} + +// Output the table and button. +echo html_writer::start_tag('div', array('id' => 'lessonoverrides')); +if (count($table->data)) { + echo html_writer::table($table); +} +if ($hasinactive) { + echo $OUTPUT->notification(get_string('inactiveoverridehelp', 'lesson'), 'dimmed_text'); +} + +echo html_writer::start_tag('div', array('class' => 'buttons')); +$options = array(); +if ($groupmode) { + if (empty($groups)) { + // There are no groups. + echo $OUTPUT->notification(get_string('groupsnone', 'lesson'), 'error'); + $options['disabled'] = true; + } + echo $OUTPUT->single_button($overrideediturl->out(true, + array('action' => 'addgroup', 'cmid' => $cm->id)), + get_string('addnewgroupoverride', 'lesson'), 'post', $options); +} else { + $users = array(); + // See if there are any users in the lesson. + $users = get_enrolled_users($context); + $info = new \core_availability\info_module($cm); + $users = $info->filter_user_list($users); + + if (empty($users)) { + // There are no users. + echo $OUTPUT->notification(get_string('usersnone', 'lesson'), 'error'); + $options['disabled'] = true; + } + echo $OUTPUT->single_button($overrideediturl->out(true, + array('action' => 'adduser', 'cmid' => $cm->id)), + get_string('addnewuseroverride', 'lesson'), 'get', $options); +} +echo html_writer::end_tag('div'); +echo html_writer::end_tag('div'); + +// Finish the page. +echo $OUTPUT->footer(); diff --git a/mod/lesson/report.php b/mod/lesson/report.php index 3efbeeb87db..bdf859dfa0d 100644 --- a/mod/lesson/report.php +++ b/mod/lesson/report.php @@ -410,6 +410,11 @@ if ($action === 'delete') { $userid = optional_param('userid', null, PARAM_INT); // if empty, then will display the general detailed view $try = optional_param('try', null, PARAM_INT); + if (!empty($userid)) { + // Apply overrides. + $lesson->update_effective_access($userid); + } + $lessonpages = $lesson->load_all_pages(); foreach ($lessonpages as $lessonpage) { if ($lessonpage->prevpageid == 0) { diff --git a/mod/lesson/tests/events_test.php b/mod/lesson/tests/events_test.php index 1bb5025a08e..07f8cbcf245 100644 --- a/mod/lesson/tests/events_test.php +++ b/mod/lesson/tests/events_test.php @@ -502,4 +502,213 @@ class mod_lesson_events_testcase extends advanced_testcase { $this->assertEventContextNotUsed($event); $this->assertDebuggingNotCalled(); } + + /** + * Test the user override created event. + * + * There is no external API for creating a user override, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_user_override_created() { + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + $params = array( + 'objectid' => 1, + 'context' => context_module::instance($lesson->cmid), + 'other' => array( + 'lessonid' => $lesson->id + ) + ); + $event = \mod_lesson\event\user_override_created::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\user_override_created', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the group override created event. + * + * There is no external API for creating a group override, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_group_override_created() { + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + $params = array( + 'objectid' => 1, + 'context' => context_module::instance($lesson->cmid), + 'other' => array( + 'lessonid' => $lesson->id, + 'groupid' => 2 + ) + ); + $event = \mod_lesson\event\group_override_created::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\group_override_created', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the user override updated event. + * + * There is no external API for updating a user override, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_user_override_updated() { + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + $params = array( + 'objectid' => 1, + 'relateduserid' => 2, + 'context' => context_module::instance($lesson->cmid), + 'other' => array( + 'lessonid' => $lesson->id + ) + ); + $event = \mod_lesson\event\user_override_updated::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\user_override_updated', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $expected = array($course->id, 'lesson', 'edit override', 'overrideedit.php?id=1', $lesson->id, $lesson->cmid); + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the group override updated event. + * + * There is no external API for updating a group override, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_group_override_updated() { + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + $params = array( + 'objectid' => 1, + 'context' => context_module::instance($lesson->cmid), + 'other' => array( + 'lessonid' => $lesson->id, + 'groupid' => 2 + ) + ); + $event = \mod_lesson\event\group_override_updated::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\group_override_updated', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $expected = array($course->id, 'lesson', 'edit override', 'overrideedit.php?id=1', $lesson->id, $lesson->cmid); + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the user override deleted event. + */ + public function test_user_override_deleted() { + global $DB; + + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + // Create an override. + $override = new stdClass(); + $override->lesson = $lesson->id; + $override->userid = 2; + $override->id = $DB->insert_record('lesson_overrides', $override); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $lesson->delete_override($override->id); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\user_override_deleted', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $expected = array($course->id, 'lesson', 'delete override', + 'overrides.php?cmid=' . $lesson->cmid, $lesson->id, $lesson->cmid); + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the group override deleted event. + */ + public function test_group_override_deleted() { + global $DB; + + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id)); + + // Create an override. + $override = new stdClass(); + $override->lesson = $lesson->id; + $override->groupid = 2; + $override->id = $DB->insert_record('lesson_overrides', $override); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $lesson->delete_override($override->id); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_lesson\event\group_override_deleted', $event); + $this->assertEquals(context_module::instance($lesson->cmid), $event->get_context()); + $expected = array($course->id, 'lesson', 'delete override', + 'overrides.php?cmid=' . $lesson->cmid, $lesson->id, $lesson->cmid); + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } } diff --git a/mod/lesson/version.php b/mod/lesson/version.php index ae457699415..14dfd8cea63 100644 --- a/mod/lesson/version.php +++ b/mod/lesson/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015032700; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2015033100; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2014110400; // Requires this Moodle version $plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; diff --git a/mod/lesson/view.php b/mod/lesson/view.php index 53f48186000..c36637007d3 100644 --- a/mod/lesson/view.php +++ b/mod/lesson/view.php @@ -45,6 +45,9 @@ if ($backtocourse) { redirect(new moodle_url('/course/view.php', array('id'=>$course->id))); } +// Apply overrides. +$lesson->update_effective_access($USER->id); + // Mark as viewed $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -85,12 +88,26 @@ if (!$canmanage) { $correctpass = false; if (!empty($userpassword) && (($lesson->password == md5(trim($userpassword))) || ($lesson->password == trim($userpassword)))) { // with or without md5 for backward compatibility (MDL-11090) + $correctpass = true; $USER->lessonloggedin[$lesson->id] = true; if ($lesson->highscores) { // Logged in - redirect so we go through all of these checks before starting the lesson. redirect("$CFG->wwwroot/mod/lesson/view.php?id=$cm->id"); } - } else { + } else if (isset($lesson->extrapasswords)) { + // Group overrides may have additional passwords. + foreach ($lesson->extrapasswords as $password) { + if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) { + $correctpass = true; + $USER->lessonloggedin[$lesson->id] = true; + if ($lesson->highscores) { + // Logged in - redirect so we go through all of these checks before starting the lesson. + redirect("$CFG->wwwroot/mod/lesson/view.php?id=$cm->id"); + } + } + } + } + if (!$correctpass) { echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('passwordprotectedlesson', 'lesson', format_string($lesson->name))); echo $lessonoutput->login_prompt($lesson, $userpassword !== ''); echo $lessonoutput->footer();