Merge branch 'MDL-43558_master' of git://github.com/dmonllao/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 7 Apr 2015 08:25:07 +0000 (16:25 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 7 Apr 2015 08:25:07 +0000 (16:25 +0800)
40 files changed:
comment/comment.js
lib/accesslib.php
lib/db/services.php
lib/tests/accesslib_test.php
mod/lesson/backup/moodle1/lib.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/classes/event/group_override_created.php [new file with mode: 0644]
mod/lesson/classes/event/group_override_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/group_override_updated.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_created.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_updated.php [new file with mode: 0644]
mod/lesson/classes/group_observers.php [new file with mode: 0644]
mod/lesson/continue.php
mod/lesson/db/access.php
mod/lesson/db/events.php [new file with mode: 0644]
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mediafile.php
mod/lesson/override_form.php [new file with mode: 0644]
mod/lesson/overridedelete.php [new file with mode: 0644]
mod/lesson/overrideedit.php [new file with mode: 0644]
mod/lesson/overrides.php [new file with mode: 0644]
mod/lesson/report.php
mod/lesson/tests/behat/lesson_course_reset.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_group_override.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_user_override.feature [new file with mode: 0644]
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lesson/view.php
user/externallib.php
user/index.php
user/lib.php
user/tests/userlib_test.php
version.php

index 90c767d..91f87a4 100644 (file)
@@ -65,6 +65,12 @@ M.core_comment = {
                 var scope = this;
                 var value = ta.get('value');
                 if (value && value != M.util.get_string('addcomment', 'moodle')) {
+                    ta.set('disabled', true);
+                    ta.setStyles({
+                        'backgroundImage': 'url(' + M.util.image_url('i/loading_small', 'core') + ')',
+                        'backgroundRepeat': 'no-repeat',
+                        'backgroundPosition': 'center center'
+                    });
                     var params = {'content': value};
                     this.request({
                         action: 'add',
@@ -75,6 +81,8 @@ M.core_comment = {
                             var cid = scope.client_id;
                             var ta = Y.one('#dlg-content-'+cid);
                             ta.set('value', '');
+                            ta.set('disabled', false);
+                            ta.setStyle('backgroundImage', 'none');
                             scope.toggle_textarea(false);
                             var container = Y.one('#comment-list-'+cid);
                             var result = scope.render([obj], true);
index 1c0f8b0..51675e9 100644 (file)
@@ -2256,9 +2256,10 @@ function can_access_course(stdClass $course, $user = null, $withcapability = '',
  * @param string $withcapability
  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
+ * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
  * @return array list($sql, $params)
  */
-function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
+function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false) {
     global $DB, $CFG;
 
     // use unique prefix just in case somebody makes some SQL magic with the result
@@ -2271,6 +2272,13 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
 
     $isfrontpage = ($coursecontext->instanceid == SITEID);
 
+    if ($onlyactive && $onlysuspended) {
+        throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
+    }
+    if ($isfrontpage && $onlysuspended) {
+        throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
+    }
+
     $joins  = array();
     $wheres = array();
     $params = array();
@@ -2387,13 +2395,28 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
     if ($isfrontpage) {
         // all users are "enrolled" on the frontpage
     } else {
-        $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
-        $joins[] = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
+        $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
+        $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
+        $ejoin = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
         $params[$prefix.'courseid'] = $coursecontext->instanceid;
 
-        if ($onlyactive) {
-            $wheres[] = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
-            $wheres[] = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
+        if (!$onlysuspended) {
+            $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
+            $joins[] = $ejoin;
+            if ($onlyactive) {
+                $wheres[] = "$where1 AND $where2";
+            }
+        } else {
+            // Suspended only where there is enrolment but ALL are suspended.
+            // Consider multiple enrols where one is not suspended or plain role_assign.
+            $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
+            $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = {$prefix}u.id";
+            $joins[] = "JOIN {enrol} {$prefix}e1 ON ({$prefix}e1.id = {$prefix}ue1.enrolid AND {$prefix}e1.courseid = :{$prefix}_e1_courseid)";
+            $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
+            $wheres[] = "{$prefix}u.id NOT IN ($enrolselect)";
+        }
+
+        if ($onlyactive || $onlysuspended) {
             $now = round(time(), -2); // rounding helps caching in DB
             $params = array_merge($params, array($prefix.'enabled'=>ENROL_INSTANCE_ENABLED,
                                                  $prefix.'active'=>ENROL_USER_ACTIVE,
@@ -7506,7 +7529,6 @@ function extract_suspended_users($context, &$users, $ignoreusers=array()) {
 function get_suspended_userids(context $context, $usecache = false) {
     global $DB;
 
-    // Check the cache first for performance reasons if enabled.
     if ($usecache) {
         $cache = cache::make('core', 'suspended_userids');
         $susers = $cache->get($context->id);
@@ -7515,21 +7537,14 @@ function get_suspended_userids(context $context, $usecache = false) {
         }
     }
 
-    // Get all enrolled users.
-    list($sql, $params) = get_enrolled_sql($context);
-    $users = $DB->get_records_sql($sql, $params);
-
-    // Get active enrolled users.
-    list($sql, $params) = get_enrolled_sql($context, null, null, true);
-    $activeusers = $DB->get_records_sql($sql, $params);
-
+    $coursecontext = $context->get_course_context();
     $susers = array();
-    if (sizeof($activeusers) != sizeof($users)) {
-        foreach ($users as $userid => $user) {
-            if (!array_key_exists($userid, $activeusers)) {
-                $susers[$userid] = $userid;
-            }
-        }
+
+    // Front page users are always enrolled, so suspended list is empty.
+    if ($coursecontext->instanceid != SITEID) {
+        list($sql, $params) = get_enrolled_sql($context, null, null, false, true);
+        $susers = $DB->get_fieldset_sql($sql, $params);
+        $susers = array_combine($susers, $susers);
     }
 
     // Cache results for the remainder of this request.
@@ -7537,6 +7552,5 @@ function get_suspended_userids(context $context, $usecache = false) {
         $cache->set($context->id, $susers);
     }
 
-    // Return.
     return $susers;
 }
index 62e0e62..4caab60 100644 (file)
@@ -503,6 +503,15 @@ $functions = array(
         'capabilities'  => '',
     ),
 
+    'core_user_view_user_list' => array(
+        'classname'     => 'core_user_external',
+        'methodname'    => 'view_user_list',
+        'classpath'     => 'user/externallib.php',
+        'description'   => 'Simulates the web-interface view of user/index.php (triggering events).',
+        'type'          => 'write',
+        'capabilities'  => 'moodle/course:viewparticipants',
+    ),
+
     // === enrol related functions ===
 
     'core_enrol_get_enrolled_users_with_capability' => array(
@@ -1075,6 +1084,7 @@ $services = array(
             'core_completion_get_activities_completion_status',
             'core_notes_get_course_notes',
             'core_completion_get_course_completion_status',
+            'core_user_view_user_list',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index d616c77..b589033 100644 (file)
@@ -1763,6 +1763,288 @@ class core_accesslib_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Test that enrolled users SQL does not return any values for users in
+     * other courses.
+     */
+    public function test_get_enrolled_sql_different_course() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // This user should not appear anywhere, we're not interested in that context.
+        $course2 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user->id, $course2->id, $student->id);
+
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertFalse(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(0, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(0, $suspended);
+    }
+
+    /**
+     * Test that enrolled users SQL does not return any values for role
+     * assignments without an enrolment.
+     */
+    public function test_get_enrolled_sql_role_only() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // Role assignment is not the same as course enrollment.
+        role_assign($student->id, $user->id, $context->id);
+
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertFalse(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(0, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(0, $suspended);
+    }
+
+    /**
+     * Test that multiple enrolments for the same user are counted correctly.
+     */
+    public function test_get_enrolled_sql_multiple_enrolments() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a suspended enrol.
+        $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self'));
+        $selfplugin = enrol_get_plugin('self');
+        $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id, 'self', 0, 0, ENROL_USER_SUSPENDED);
+
+        // Should be enrolled, but not active - user is suspended.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertTrue(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertTrue(isset($suspended[$user->id]));
+        $this->assertCount(1, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(1, $suspended);
+
+        // Add an active enrol for the user. Any active enrol makes them enrolled.
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id);
+
+        // User should be active now.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertTrue(isset($enrolled[$user->id]));
+        $this->assertTrue(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(1, $enrolled);
+        $this->assertCount(1, $active);
+        $this->assertCount(0, $suspended);
+
+    }
+
+    public function get_enrolled_sql_provider() {
+        return array(
+            array(
+                // Two users who are enrolled.
+                'users' => array(
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 2,
+                    'suspended'     => 0,
+                ),
+            ),
+            array(
+                // A user who is suspended.
+                'users' => array(
+                    array(
+                        'status'    => ENROL_USER_SUSPENDED,
+                        'enrolled'  => true,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One of each.
+                'users' => array(
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                    array(
+                        'status'    => ENROL_USER_SUSPENDED,
+                        'enrolled'  => true,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 1,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is not yet enrolled.
+                'users' => array(
+                    array(
+                        'timestart' => DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is no longer enrolled
+                'users' => array(
+                    array(
+                        'timeend'   => -DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is not yet enrolled, and one who is no longer enrolled.
+                'users' => array(
+                    array(
+                        'timeend'   => -DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                    array(
+                        'timestart' => DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 0,
+                    'suspended'     => 2,
+                ),
+            ),
+        );
+    }
+
+    /**
+     * @dataProvider get_enrolled_sql_provider
+     */
+    public function test_get_enrolled_sql_course($users, $counts) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $createdusers = array();
+
+        foreach ($users as &$userdata) {
+            $user = $this->getDataGenerator()->create_user();
+            $userdata['id'] = $user->id;
+
+            $timestart  = 0;
+            $timeend    = 0;
+            $status     = null;
+            if (isset($userdata['timestart'])) {
+                $timestart = time() + $userdata['timestart'];
+            }
+            if (isset($userdata['timeend'])) {
+                $timeend = time() + $userdata['timeend'];
+            }
+            if (isset($userdata['status'])) {
+                $status = $userdata['status'];
+            }
+
+            // Enrol the user in the course.
+            $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id, 'manual', $timestart, $timeend, $status);
+        }
+
+        // After all users have been enroled, check expectations.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        foreach ($users as $userdata) {
+            if (isset($userdata['enrolled']) && $userdata['enrolled']) {
+                $this->assertTrue(isset($enrolled[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($enrolled[$userdata['id']]));
+            }
+
+            if (isset($userdata['active']) && $userdata['active']) {
+                $this->assertTrue(isset($active[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($active[$userdata['id']]));
+            }
+
+            if (isset($userdata['suspended']) && $userdata['suspended']) {
+                $this->assertTrue(isset($suspended[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($suspended[$userdata['id']]));
+            }
+        }
+
+        $this->assertCount($counts['enrolled'],     $enrolled);
+        $this->assertCount($counts['active'],       $active);
+        $this->assertCount($counts['suspended'],    $suspended);
+    }
+
     /**
      * A small functional test of permission evaluations.
      */
index 5a9b0d2..72796d5 100644 (file)
@@ -233,6 +233,9 @@ class moodle1_mod_lesson_handler extends moodle1_mod_handler {
      * This is executed when we reach the closing </MOD> tag of our 'lesson' path
      */
     public function on_lesson_end() {
+        // Append empty <overrides> subpath element.
+        $this->write_xml('overrides', array());
+
         // finish writing lesson.xml
         $this->xmlwriter->end_tag('lesson');
         $this->xmlwriter->end_tag('activity');
index 037353d..380d7c7 100644 (file)
  *
  * This is the "graphical" structure of the lesson module:
  *
- *                  lesson ------------>---------------|-------------->-----------|------------->------------|
- *               (CL,pk->id)                           |                          |                          |
- *                     |                               |                          |                          |
- *                     |                         lesson_grades              lesson_high_scores         lesson_timer
- *                     |                  (UL, pk->id,fk->lessonid)    (UL, pk->id,fk->lessonid)   (UL, pk->id,fk->lessonid)
- *                     |                               |
- *                     |                               |
- *                     |                               |
- *                     |                               |
- *              lesson_pages----------->---------lesson_branch
- *          (CL,pk->id,fk->lessonid)       (UL, pk->id,fk->pageid)
- *                     |
- *                     |
- *                     |
- *               lesson_answers
- *            (CL,pk->id,fk->pageid)
- *                     |
- *                     |
- *                     |
- *               lesson_attempts
- *          (UL,pk->id,fk->answerid)
+ *         lesson ---------->-------------|------------>---------|----------->----------|----------->----------|
+ *      (CL,pk->id)                       |                      |                      |                      |
+ *            |                           |                      |                      |                      |
+ *            |                     lesson_grades          lesson_high_scores     lesson_timer           lesson_overrides
+ *            |            (UL, pk->id,fk->lessonid) (UL, pk->id,fk->lessonid) (UL, pk->id,fk->lessonid) (UL, pk->id,fk->lessonid)
+ *            |                           |
+ *            |                           |
+ *            |                           |
+ *            |                           |
+ *      lesson_pages-------->-------lesson_branch
+ *   (CL,pk->id,fk->lessonid)     (UL, pk->id,fk->pageid)
+ *            |
+ *            |
+ *            |
+ *      lesson_answers
+ *   (CL,pk->id,fk->pageid)
+ *            |
+ *            |
+ *            |
+ *      lesson_attempts
+ *  (UL,pk->id,fk->answerid)
  *
  * Meaning: pk->primary key field of the table
  *          fk->foreign key to link with parent
@@ -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);
index b79b5e6..5129183 100644 (file)
@@ -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 (file)
index 0000000..1edc21b
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..1670085
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..7096cd1
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..9e47d57
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..6fc90aa
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..4c30e4b
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..93298b9
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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, $event->objectid);
+    }
+}
index 8dbac30..e2d683a 100644 (file)
@@ -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');
index 9ede52e..5f8d7f7 100644 (file)
@@ -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 (file)
index 0000000..ca9f07e
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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'
+    ),
+);
index df004de..a9440ef 100644 (file)
         <INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="lesson_overrides" COMMENT="The overrides to lesson settings.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="lessonid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references lesson.id"/>
+        <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Foreign key references groups.id.  Can be null if this is a per-user override."/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Foreign key references user.id.  Can be null if this is a per-group override."/>
+        <FIELD NAME="available" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time at which students may start attempting this lesson. Can be null, in which case the lesson default is used."/>
+        <FIELD NAME="deadline" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time by which students must have completed their attempt.  Can be null, in which case the lesson default is used."/>
+        <FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time limit in seconds.  Can be null, in which case the lesson default is used."/>
+        <FIELD NAME="review" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="maxattempts" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="retake" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="password" TYPE="char" LENGTH="32" NOTNULL="false" SEQUENCE="false" COMMENT="Lesson password.  Can be null, in which case the lesson default is used."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="lessonid" TYPE="foreign" FIELDS="lessonid" REFTABLE="lesson" REFFIELDS="id"/>
+        <KEY NAME="groupid" TYPE="foreign" FIELDS="groupid" REFTABLE="groups" REFFIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
 </XMLDB>
index d35ee63..c26d005 100644 (file)
@@ -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;
 }
index 468bd98..ee2fb4c 100644 (file)
@@ -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;
index 19c5f4f..de04c56 100644 (file)
@@ -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.';
@@ -222,10 +234,12 @@ $string['checkquestion'] = 'Check question';
 $string['importcount'] = 'Importing {$a} questions';
 $string['importquestions'] = 'Import questions';
 $string['importquestions_help'] = 'This feature enables questions in a variety of formats to be imported via text file.';
+$string['inactiveoverridehelp'] = '* Student does not have the correct group or role to view/attempt the lesson';
 $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 +257,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 +325,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 +334,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 +370,14 @@ $string['or'] = 'OR';
 $string['ordered'] = 'Ordered';
 $string['other'] = 'Other';
 $string['outof'] = 'Out of {$a}';
+$string['override'] = 'Override';
+$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['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 +425,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 +434,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 +496,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['useroverrides'] = 'User overrides';
+$string['useroverridesdeleted'] = 'User overrides deleted';
+$string['usersnone'] = 'No students have access to this lesson';
 $string['viewgrades'] = 'View grades';
 $string['viewhighscores'] = 'View high scores list';
 $string['viewreports'] = 'View {$a->attempts} completed {$a->student} attempts';
index 8e6aa7c..f7d068d 100644 (file)
@@ -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);
index 3525e10..f6f90fc 100644 (file)
@@ -638,6 +638,41 @@ 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.
+ * @param int $groupid The group id if it is known
+ * @return void
+ */
+function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
+    global $DB;
+
+    $params = array('courseid' => $courseid);
+    if ($groupid) {
+        $params['groupid'] = $groupid;
+        // We just update the group that was deleted.
+        $sql = "SELECT o.id, o.lessonid
+                  FROM {lesson_overrides} o
+                  JOIN {lesson} lesson ON lesson.id = o.lessonid
+                 WHERE lesson.course = :courseid
+                   AND o.groupid = :groupid";
+    } else {
+        // No groupid, we update all orphaned group overrides for all lessons in course.
+        $sql = "SELECT o.id, o.lessonid
+                  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";
+    }
+    $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 +1023,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 +1048,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 +1687,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);
index 71240f9..faf3d31 100644 (file)
@@ -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 (file)
index 0000000..0d7fbed
--- /dev/null
@@ -0,0 +1,279 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 = get_enrolled_users($this->context, '', 0,
+                        'u.id, u.email, ' . get_all_user_name_fields(true, 'u'));
+
+                // 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 (file)
index 0000000..ad8c4e1
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..26a517f
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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};
+    }
+}
+
+// True if group-based override.
+$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid));
+
+// If we are duplicating an override, then clear the user/group and override id
+// since they will change.
+if ($action === 'duplicate') {
+    $override->id = $data->id = null;
+    $override->userid = $data->userid = null;
+    $override->groupid = $data->groupid = null;
+}
+
+$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 (file)
index 0000000..9382346
--- /dev/null
@@ -0,0 +1,295 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 = '<a title="' . get_string('edit') . '" href="'. $editurlstr . '">' .
+                '<img src="' . $OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="' .
+                get_string('edit') . '" /></a> ';
+        // Duplicate.
+        $copyurlstr = $overrideediturl->out(true,
+                array('id' => $override->id, 'action' => 'duplicate'));
+        $iconstr .= '<a title="' . get_string('copy') . '" href="' . $copyurlstr . '">' .
+                '<img src="' . $OUTPUT->pix_url('t/copy') . '" class="iconsmall" alt="' .
+                get_string('copy') . '" /></a> ';
+    }
+    // Delete.
+    $deleteurlstr = $overridedeleteurl->out(true,
+            array('id' => $override->id, 'sesskey' => sesskey()));
+    $iconstr .= '<a title="' . get_string('delete') . '" href="' . $deleteurlstr . '">' .
+            '<img src="' . $OUTPUT->pix_url('t/delete') . '" class="iconsmall" alt="' .
+            get_string('delete') . '" /></a> ';
+
+    if ($groupmode) {
+        $usergroupstr = '<a href="' . $groupurl->out(true,
+                array('group' => $override->groupid)) . '" >' . $override->name . '</a>';
+    } else {
+        $usergroupstr = '<a href="' . $userurl->out(true,
+                array('id' => $override->userid)) . '" >' . fullname($override) . '</a>';
+    }
+
+    $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();
index 3efbeeb..bdf859d 100644 (file)
@@ -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/behat/lesson_course_reset.feature b/mod/lesson/tests/behat/lesson_course_reset.feature
new file mode 100644 (file)
index 0000000..28fae86
--- /dev/null
@@ -0,0 +1,109 @@
+@mod @mod_lesson
+Feature: Lesson reset
+  In order to reuse past lessons
+  As a teacher
+  I need to remove all previous data.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Tina | Teacher1 | teacher1@asd.com |
+      | student1 | Sam1 | Student1 | student1@asd.com |
+      | student2 | Sam2 | Student2 | student2@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+      | Group 2 | C1     | G2       |
+    And the following "activities" exist:
+      | activity | name             | intro                   | course | idnumber |
+      | lesson   | Test lesson name | Test lesson description | C1     | lesson1  |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test lesson name"
+    And I follow "Add a question page"
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title           | True/false question 1 |
+      | Page contents        | Cat is an amphibian |
+      | id_answer_editor_0   | False |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0          | Next page |
+      | id_answer_editor_1   | True |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1          | This page |
+    And I press "Save page"
+
+  Scenario: Use course reset to clear all attempt data
+    When I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I navigate to "Overview" node in "Reports"
+    And I should see "Sam1 Student1"
+    And I navigate to "Reset" node in "Course administration"
+    And I set the following fields to these values:
+        | Delete all lesson attempts | 1  |
+    And I press "Reset course"
+    And I press "Continue"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I navigate to "Overview" node in "Reports"
+    Then I should see "No attempts have been made on this lesson"
+
+  Scenario: Use course reset to remove user overrides.
+    When I follow "Test lesson name"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+        | Override user    | Student1  |
+        | Re-takes allowed | 1 |
+    And I press "Save"
+    And I should see "Sam1 Student1"
+    And I navigate to "Reset" node in "Course administration"
+    And I set the following fields to these values:
+        | Delete all user overrides | 1  |
+    And I press "Reset course"
+    And I press "Continue"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I navigate to "User overrides" node in "Lesson administration"
+    Then I should not see "Sam1 Student1"
+
+  Scenario: Use course reset to remove group overrides.
+    When I follow "Test lesson name"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+        | Override group   | Group 1  |
+        | Re-takes allowed | 1 |
+    And I press "Save"
+    And I should see "Group 1"
+    And I navigate to "Reset" node in "Course administration"
+    And I set the following fields to these values:
+        | Delete all group overrides | 1  |
+    And I press "Reset course"
+    And I press "Continue"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    Then I should not see "Group 1"
diff --git a/mod/lesson/tests/behat/lesson_group_override.feature b/mod/lesson/tests/behat/lesson_group_override.feature
new file mode 100644 (file)
index 0000000..cf24236
--- /dev/null
@@ -0,0 +1,340 @@
+@mod @mod_lesson
+Feature: Lesson user override
+  In order to grant a student special access to a lesson
+  As a teacher
+  I need to create an override for that user.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Tina | Teacher1 | teacher1@asd.com |
+      | student1 | Sam1 | Student1 | student1@asd.com |
+      | student2 | Sam2 | Student2 | student2@asd.com |
+      | student3 | Sam3 | Student3 | student3@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | student3 | C1 | student |
+    And the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+      | Group 2 | C1     | G2       |
+    Given the following "group members" exist:
+      | user     | group   |
+      | student1 | G1 |
+      | student2 | G2 |
+      | student3 | G1 |
+    And the following "activities" exist:
+      | activity | name             | intro                   | groupmode  | course | idnumber |
+      | lesson   | Test lesson name | Test lesson description | 1          | C1     | lesson1  |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test lesson name"
+    And I follow "Add a question page"
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title           | True/false question 1 |
+      | Page contents        | Cat is an amphibian |
+      | id_answer_editor_0   | False |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0          | Next page |
+      | id_answer_editor_1   | True |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1          | This page |
+    And I press "Save page"
+
+  Scenario: Add, modify then delete a group override
+    When I follow "Test lesson name"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group      | Group 1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Wednesday, 1 January 2020, 8:00"
+    Then I click on "Edit" "link"
+    And I set the following fields to these values:
+      | deadline[year] | 2030 |
+    And I press "Save"
+    And I should see "Tuesday, 1 January 2030, 8:00"
+    And I click on "Delete" "link"
+    And I press "Continue"
+    And I should not see "Group 1"
+
+  Scenario: Duplicate a user override
+    When I follow "Test lesson name"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group      | Group 1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Wednesday, 1 January 2020, 8:00"
+    Then I click on "copy" "link"
+    And I set the following fields to these values:
+      | Override group | Group 2  |
+      | deadline[year] | 2030 |
+    And I press "Save"
+    And I should see "Tuesday, 1 January 2030, 8:00"
+    And I should see "Group 2"
+
+  Scenario: Allow a single group to have re-take the lesson
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Re-takes allowed | 0 |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group   | Group 1 |
+      | Re-takes allowed | 1 |
+    And I press "Save"
+    And I should see "Re-takes allowed"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I follow "Test lesson name"
+    Then I should not see "You are not allowed to retake this lesson."
+    And I should see "Cat is an amphibian"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I follow "Test lesson name"
+    And I should see "You are not allowed to retake this lesson."
+
+  Scenario: Allow a single group to have a different password
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Password protected lesson | Yes |
+      | id_password               | moodle_rules |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group            | Group 1 |
+      | Password protected lesson | 12345 |
+    And I press "Save"
+    And I should see "Password protected lesson"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    Then I should see "Test lesson name is a password protected lesson"
+    And I should not see "Cat is an amphibian"
+    And I set the field "userpassword" to "moodle_rules"
+    And I press "Continue"
+    And I should see "Login failed, please try again..."
+    And I should see "Test lesson name is a password protected lesson"
+    And I set the field "userpassword" to "12345"
+    And I press "Continue"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Test lesson name is a password protected lesson"
+    And I should not see "Cat is an amphibian"
+    And I set the field "userpassword" to "12345"
+    And I press "Continue"
+    And I should see "Login failed, please try again..."
+    And I should see "Test lesson name is a password protected lesson"
+    And I set the field "userpassword" to "moodle_rules"
+    And I press "Continue"
+
+  Scenario: Allow a group to have a different due date
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2000 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group      | Group 1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Lesson closes"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    Then I should see "This lesson closed on Saturday, 1 January 2000, 8:00"
+    And I should not see "Cat is an amphibian"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "Cat is an amphibian"
+
+  Scenario: Allow a group to have a different start date
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2020 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group       | Group 1 |
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2015 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save"
+    And I should see "Lesson opens"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    Then  I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
+    And I should not see "Cat is an amphibian"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "Cat is an amphibian"
+
+  Scenario: Allow a single group to have multiple attempts at each question
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Re-takes allowed | 1 |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group             | Group 1 |
+      | Maximum number of attempts | 2 |
+    And I press "Save"
+    And I should see "Maximum number of attempts"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    Then I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+
+  Scenario: Add both a user and group override and verify that both are applied correctly
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2030 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save and display"
+    And I navigate to "Group overrides" node in "Lesson administration"
+    And I press "Add group override"
+    And I set the following fields to these values:
+      | Override group       | Group 1 |
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2020 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save"
+    And I should see "Wednesday, 1 January 2020, 8:00"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user        | Student1 |
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2021 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save"
+    And I should see "Friday, 1 January 2021, 8:00"
+    And I log out
+    Then I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "This lesson will be open on Friday, 1 January 2021, 8:00"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
+    And I log out
+    And I log in as "student3"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
diff --git a/mod/lesson/tests/behat/lesson_user_override.feature b/mod/lesson/tests/behat/lesson_user_override.feature
new file mode 100644 (file)
index 0000000..14e94a5
--- /dev/null
@@ -0,0 +1,282 @@
+@mod @mod_lesson
+Feature: Lesson user override
+  In order to grant a student special access to a lesson
+  As a teacher
+  I need to create an override for that user.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Tina | Teacher1 | teacher1@asd.com |
+      | student1 | Sam1 | Student1 | student1@asd.com |
+      | student2 | Sam2 | Student2 | student2@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+      | Group 2 | C1     | G2       |
+    And the following "activities" exist:
+      | activity | name             | intro                   | course | idnumber |
+      | lesson   | Test lesson name | Test lesson description | C1     | lesson1  |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test lesson name"
+    And I follow "Add a question page"
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title           | True/false question 1 |
+      | Page contents        | Cat is an amphibian |
+      | id_answer_editor_0   | False |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0          | Next page |
+      | id_answer_editor_1   | True |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1          | This page |
+    And I press "Save page"
+
+  Scenario: Add, modify then delete a user override
+    When I follow "Test lesson name"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user       | Student1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Wednesday, 1 January 2020, 8:00"
+    Then I click on "Edit" "link"
+    And I set the following fields to these values:
+      | deadline[year] | 2030 |
+    And I press "Save"
+    And I should see "Tuesday, 1 January 2030, 8:00"
+    And I click on "Delete" "link"
+    And I press "Continue"
+    And I should not see "Sam1 Student1"
+
+  Scenario: Duplicate a user override
+    When I follow "Test lesson name"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user       | Student1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Wednesday, 1 January 2020, 8:00"
+    Then I click on "copy" "link"
+    And I set the following fields to these values:
+      | Override user  | Student2  |
+      | deadline[year] | 2030 |
+    And I press "Save"
+    And I should see "Tuesday, 1 January 2030, 8:00"
+    And I should see "Sam2 Student2"
+
+  Scenario: Allow a single user to have re-take the lesson
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Re-takes allowed | 0 |
+    And I press "Save and display"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user    | Student1  |
+      | Re-takes allowed | 1 |
+    And I press "Save"
+    And I should see "Re-takes allowed"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I follow "Test lesson name"
+    Then I should not see "You are not allowed to retake this lesson."
+    And I should see "Cat is an amphibian"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I follow "Test lesson name"
+    And I should see "You are not allowed to retake this lesson."
+
+  Scenario: Allow a single user to have a different password
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Password protected lesson | Yes |
+      | id_password               | moodle_rules |
+    And I press "Save and display"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user             | Student1  |
+      | Password protected lesson | 12345 |
+    And I press "Save"
+    And I should see "Password protected lesson"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    Then I should see "Test lesson name is a password protected lesson"
+    And I should not see "Cat is an amphibian"
+    And I set the field "userpassword" to "moodle_rules"
+    And I press "Continue"
+    And I should see "Login failed, please try again..."
+    And I should see "Test lesson name is a password protected lesson"
+    And I set the field "userpassword" to "12345"
+    And I press "Continue"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Test lesson name is a password protected lesson"
+    And I should not see "Cat is an amphibian"
+    And I set the field "userpassword" to "12345"
+    And I press "Continue"
+    And I should see "Login failed, please try again..."
+    And I should see "Test lesson name is a password protected lesson"
+    And I set the field "userpassword" to "moodle_rules"
+    And I press "Continue"
+
+  Scenario: Allow a user to have a different due date
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2000 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save and display"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user       | Student1 |
+      | id_deadline_enabled | 1 |
+      | deadline[day]       | 1 |
+      | deadline[month]     | January |
+      | deadline[year]      | 2020 |
+      | deadline[hour]      | 08 |
+      | deadline[minute]    | 00 |
+    And I press "Save"
+    And I should see "Lesson closes"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    Then I should see "This lesson closed on Saturday, 1 January 2000, 8:00"
+    And I should not see "Cat is an amphibian"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "Cat is an amphibian"
+
+  Scenario: Allow a user to have a different start date
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2020 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save and display"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user        | Student1 |
+      | id_available_enabled | 1 |
+      | available[day]       | 1 |
+      | available[month]     | January |
+      | available[year]      | 2015 |
+      | available[hour]      | 08 |
+      | available[minute]    | 00 |
+    And I press "Save"
+    And I should see "Lesson opens"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    Then  I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
+    And I should not see "Cat is an amphibian"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson"
+    And I should see "Cat is an amphibian"
+
+  Scenario: Allow a single user to have multiple attempts at each question
+    When I follow "Test lesson name"
+    And I navigate to "Edit settings" node in "Lesson administration"
+    And I set the following fields to these values:
+      | Re-takes allowed | 1 |
+    And I press "Save and display"
+    And I navigate to "User overrides" node in "Lesson administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+      | Override user              | Student1  |
+      | Maximum number of attempts | 2 |
+    And I press "Save"
+    And I should see "Maximum number of attempts"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Cat is an amphibian"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    Then I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
index 1bb5025..b03f063 100644 (file)
@@ -502,4 +502,172 @@ 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() {
+
+        $params = array(
+            'objectid' => 1,
+            'relateduserid' => 2,
+            'context' => context_module::instance($this->lesson->properties()->cmid),
+            'other' => array(
+                'lessonid' => $this->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($this->lesson->properties()->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() {
+
+        $params = array(
+            'objectid' => 1,
+            'context' => context_module::instance($this->lesson->properties()->cmid),
+            'other' => array(
+                'lessonid' => $this->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($this->lesson->properties()->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() {
+
+        $params = array(
+            'objectid' => 1,
+            'relateduserid' => 2,
+            'context' => context_module::instance($this->lesson->properties()->cmid),
+            'other' => array(
+                'lessonid' => $this->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($this->lesson->properties()->cmid), $event->get_context());
+        $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() {
+
+        $params = array(
+            'objectid' => 1,
+            'context' => context_module::instance($this->lesson->properties()->cmid),
+            'other' => array(
+                'lessonid' => $this->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($this->lesson->properties()->cmid), $event->get_context());
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the user override deleted event.
+     */
+    public function test_user_override_deleted() {
+        global $DB;
+
+        // Create an override.
+        $override = new stdClass();
+        $override->lesson = $this->lesson->id;
+        $override->userid = 2;
+        $override->id = $DB->insert_record('lesson_overrides', $override);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $this->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($this->lesson->properties()->cmid), $event->get_context());
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the group override deleted event.
+     */
+    public function test_group_override_deleted() {
+        global $DB;
+
+        // Create an override.
+        $override = new stdClass();
+        $override->lesson = $this->lesson->id;
+        $override->groupid = 2;
+        $override->id = $DB->insert_record('lesson_overrides', $override);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $this->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($this->lesson->properties()->cmid), $event->get_context());
+        $this->assertEventContextNotUsed($event);
+    }
 }
index ae45769..14dfd8c 100644 (file)
@@ -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;
index 53f4818..c366370 100644 (file)
@@ -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();
index a701073..b5bd747 100644 (file)
@@ -1243,6 +1243,81 @@ class core_user_external extends external_api {
         );
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.9
+     */
+    public static function view_user_list_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'id of the course, 0 for site')
+            )
+        );
+    }
+
+    /**
+     * Simulate the /user/index.php web interface page triggering events
+     *
+     * @param int $courseid id of course
+     * @return array of warnings and status result
+     * @since Moodle 2.9
+     * @throws moodle_exception
+     */
+    public static function view_user_list($courseid) {
+        global $CFG;
+        require_once($CFG->dirroot . "/user/lib.php");
+
+        $params = self::validate_parameters(self::view_user_list_parameters(),
+                                            array(
+                                                'courseid' => $courseid
+                                            ));
+
+        $warnings = array();
+
+        if (empty($params['courseid'])) {
+            $params['courseid'] = SITEID;
+        }
+
+        $course = get_course($params['courseid']);
+
+        if ($course->id == SITEID) {
+            $context = context_system::instance();
+        } else {
+            $context = context_course::instance($course->id);
+        }
+        self::validate_context($context);
+
+        if ($course->id == SITEID) {
+            require_capability('moodle/site:viewparticipants', $context);
+        } else {
+            require_capability('moodle/course:viewparticipants', $context);
+        }
+
+        user_list_view($course, $context);
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.9
+     */
+    public static function view_user_list_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
 }
 
  /**
index c1adf89..fb253ff 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 require_once('../config.php');
+require_once($CFG->dirroot.'/user/lib.php');
 require_once($CFG->libdir.'/tablelib.php');
 require_once($CFG->libdir.'/filelib.php');
 
@@ -105,16 +106,8 @@ if (empty($rolenames) && !$isfrontpage) {
     }
 }
 
-$event = \core\event\user_list_viewed::create(array(
-    'objectid' => $course->id,
-    'courseid' => $course->id,
-    'context' => $context,
-    'other' => array(
-        'courseshortname' => $course->shortname,
-        'coursefullname' => $course->fullname
-    )
-));
-$event->trigger();
+// Trigger events.
+user_list_view($course, $context);
 
 $bulkoperations = has_capability('moodle/course:bulkmessaging', $context);
 
index 737c793..c751fa9 100644 (file)
@@ -1002,3 +1002,24 @@ function user_remove_user_device($uuid, $appid = "") {
 
     return true;
 }
+
+/**
+ * Trigger user_list_viewed event.
+ *
+ * @param stdClass  $course course  object
+ * @param stdClass  $context course context object
+ * @since Moodle 2.9
+ */
+function user_list_view($course, $context) {
+
+    $event = \core\event\user_list_viewed::create(array(
+        'objectid' => $course->id,
+        'courseid' => $course->id,
+        'context' => $context,
+        'other' => array(
+            'courseshortname' => $course->shortname,
+            'coursefullname' => $course->fullname
+        )
+    ));
+    $event->trigger();
+}
index 002d7dc..23a68d2 100644 (file)
@@ -347,4 +347,34 @@ class core_userliblib_testcase extends advanced_testcase {
         $this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id)));
         $this->assertEquals(0, $DB->count_records('user_password_history', array('userid' => $user2->id)));
     }
+
+    /**
+     * Test user_list_view function
+     */
+    public function test_user_list_view() {
+
+        $this->resetAfterTest();
+
+        // Course without sections.
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        $this->setAdminUser();
+
+        // Redirect events to the sink, so we can recover them later.
+        $sink = $this->redirectEvents();
+
+        user_list_view($course, $context);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Check the event details are correct.
+        $this->assertInstanceOf('\core\event\user_list_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $this->assertEquals($course->shortname, $event->other['courseshortname']);
+        $this->assertEquals($course->fullname, $event->other['coursefullname']);
+
+    }
+
 }
index 5f9ce9c..135958a 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2015040600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2015040600.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.