Merge branch 'MDL-47269_master' of https://github.com/nadavkav/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Sep 2014 23:28:29 +0000 (01:28 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Sep 2014 23:28:29 +0000 (01:28 +0200)
22 files changed:
admin/settings/users.php
availability/classes/info.php
availability/classes/tree.php
availability/classes/tree_node.php
availability/condition/date/classes/condition.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/util/plan/restore_step.class.php
backup/util/plan/restore_structure_step.class.php
lang/en/admin.php
lang/en/error.php
lib/classes/component.php
lib/form/searchableselector.js
lib/moodlelib.php
lib/tests/moodlelib_test.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/mod_form.php
mod/quiz/overrides.php
mod/quiz/settings.php
mod/quiz/tests/behat/settings_form_fields_disableif.feature [new file with mode: 0644]
question/type/multianswer/questiontype.php
user/profile.php

index 2e613e7..f8d5b74 100644 (file)
@@ -190,6 +190,9 @@ if ($hassiteconfig
                     'institution' => new lang_string('institution'),
                 )));
         $temp->add(new admin_setting_configtext('fullnamedisplay', new lang_string('fullnamedisplay', 'admin'), new lang_string('configfullnamedisplay', 'admin'), 'language', PARAM_TEXT, 50));
+        $temp->add(new admin_setting_configtext('alternativefullnameformat', new lang_string('alternativefullnameformat', 'admin'),
+                new lang_string('alternativefullnameformat_desc', 'admin'),
+                'language', PARAM_RAW, 50));
         $temp->add(new admin_setting_configtext('maxusersperpage', new lang_string('maxusersperpage','admin'), new lang_string('configmaxusersperpage','admin'), 100, PARAM_INT));
         $temp->add(new admin_setting_configcheckbox('enablegravatar', new lang_string('enablegravatar', 'admin'), new lang_string('enablegravatar_help', 'admin'), 0));
         $temp->add(new admin_setting_configtext('gravatardefaulturl', new lang_string('gravatardefaulturl', 'admin'), new lang_string('gravatardefaulturl_help', 'admin'), 'mm'));
index fc00ad8..fe0735b 100644 (file)
@@ -49,6 +49,9 @@ abstract class info {
     /** @var tree Availability configuration, decoded from JSON; null if unset */
     protected $availabilitytree;
 
+    /** @var array|null Array of information about current restore if any */
+    protected static $restoreinfo = null;
+
     /**
      * Constructs with item details.
      *
@@ -307,9 +310,12 @@ abstract class info {
      * @param string $restoreid Restore identifier
      * @param int $courseid Target course id
      * @param \base_logger $logger Logger for any warnings
+     * @param int $dateoffset Date offset to be added to any dates (0 = none)
      */
-    public function update_after_restore($restoreid, $courseid, \base_logger $logger) {
+    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $dateoffset) {
         $tree = $this->get_availability_tree();
+        // Set static data for use by get_restore_date_offset function.
+        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset);
         $changed = $tree->update_after_restore($restoreid, $courseid, $logger,
                 $this->get_thing_name());
         if ($changed) {
@@ -319,6 +325,24 @@ abstract class info {
         }
     }
 
+    /**
+     * Gets the date offset (amount by which any date values should be
+     * adjusted) for the current restore.
+     *
+     * @param string $restoreid Restore identifier
+     * @return int Date offset (0 if none)
+     * @throws coding_exception If not in a restore (or not in that restore)
+     */
+    public static function get_restore_date_offset($restoreid) {
+        if (!self::$restoreinfo) {
+            throw new coding_exception('Only valid during restore');
+        }
+        if (self::$restoreinfo['restoreid'] !== $restoreid) {
+            throw new coding_exception('Data not available for that restore id');
+        }
+        return self::$restoreinfo['dateoffset'];
+    }
+
     /**
      * Obtains the name of the item (cm_info or section_info, at present) that
      * this is controlling availability of. Name should be formatted ready
index 218748c..a7308c0 100644 (file)
@@ -606,10 +606,13 @@ class tree extends tree_node {
     public function save() {
         $result = new \stdClass();
         $result->op = $this->op;
-        if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
-            $result->showc = $this->showchildren;
-        } else {
-            $result->show = $this->show;
+        // Only root tree has the 'show' options.
+        if ($this->root) {
+            if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
+                $result->showc = $this->showchildren;
+            } else {
+                $result->show = $this->show;
+            }
         }
         $result->c = array();
         foreach ($this->children as $child) {
index 54b42cd..a8471a0 100644 (file)
@@ -87,6 +87,9 @@ abstract class tree_node {
      * The default behaviour is simply to return false. If there is a problem
      * with the update, $logger can be used to output a warning.
      *
+     * Note: If you need information about the date offset, call
+     * \core_availability\info::get_restore_date_offset($restoreid).
+     *
      * @param string $restoreid Restore ID
      * @param int $courseid ID of target course
      * @param \base_logger $logger Logger for any warnings
index 36191f9..be3be9b 100644 (file)
@@ -209,4 +209,15 @@ class condition extends \core_availability\condition {
     protected static function is_midnight($time) {
         return usergetmidnight($time) == $time;
     }
+
+    public function update_after_restore(
+            $restoreid, $courseid, \base_logger $logger, $name) {
+        // Update the date, if restoring with changed date.
+        $dateoffset = \core_availability\info::get_restore_date_offset($restoreid);
+        if ($dateoffset) {
+            $this->time += $dateoffset;
+            return true;
+        }
+        return false;
+    }
 }
index 6a92423..5492497 100644 (file)
@@ -645,6 +645,9 @@ class restore_update_availability extends restore_execution_step {
         rebuild_course_cache($this->get_courseid(), true);
         $modinfo = get_fast_modinfo($this->get_courseid());
 
+        // Get the date offset for this restore.
+        $dateoffset = $this->apply_date_offset(1) - 1;
+
         // Update all sections that were restored.
         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
@@ -667,7 +670,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($section->availability)) {
                 $info = new \core_availability\info_section($section);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger());
+                        $this->get_courseid(), $this->get_logger(), $dateoffset);
             }
         }
         $rs->close();
@@ -687,7 +690,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($cm->availability)) {
                 $info = new \core_availability\info_module($cm);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger());
+                        $this->get_courseid(), $this->get_logger(), $dateoffset);
             }
         }
         $rs->close();
index 23b2530..bb937aa 100644 (file)
@@ -331,13 +331,67 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $this->assertEquals($expected, $actual);
     }
 
+    /**
+     * When restoring a course, you can change the start date, which shifts other
+     * dates. This test checks that certain dates are correctly modified.
+     */
+    public function test_restore_dates() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $CFG->enableavailability = true;
+
+        // Create a course with specific start date.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(array(
+                'startdate' => strtotime('1 Jan 2014 00:00 GMT')));
+
+        // Add a forum with conditional availability date restriction, including
+        // one of them nested inside a tree.
+        $availability = '{"op":"&","showc":[true,true],"c":[' .
+                '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' .
+                '{"type":"date","d":"<","t":DATE2}]}';
+        $before = str_replace(
+                array('DATE1', 'DATE2'),
+                array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')),
+                $availability);
+        $forum = $generator->create_module('forum', array('course' => $course->id,
+                'availability' => $before));
+
+        // Add an assign with defined start date.
+        $assign = $generator->create_module('assign', array('course' => $course->id,
+                'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT')));
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT'));
+
+        $modinfo = get_fast_modinfo($newcourseid);
+
+        // Check forum dates are modified by the same amount as the course start.
+        $newforums = $modinfo->get_instances_of('forum');
+        $newforum = reset($newforums);
+        $after = str_replace(
+            array('DATE1', 'DATE2'),
+            array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')),
+            $availability);
+        $this->assertEquals($after, $newforum->availability);
+
+        // Check assign date.
+        $newassigns = $modinfo->get_instances_of('assign');
+        $newassign = reset($newassigns);
+        $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field(
+                'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
+    }
+
     /**
      * Backs a course up and restores it.
      *
      * @param stdClass $course Course object to backup
+     * @param int $newdate If non-zero, specifies custom date for new course
      * @return int ID of newly restored course
      */
-    protected function backup_and_restore($course) {
+    protected function backup_and_restore($course, $newdate = 0) {
         global $USER, $CFG;
 
         // Turn off file logging, otherwise it can't delete the file (Windows).
@@ -358,6 +412,9 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $rc = new restore_controller($backupid, $newcourseid,
                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
                 backup::TARGET_NEW_COURSE);
+        if ($newdate) {
+            $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
+        }
         $this->assertTrue($rc->execute_precheck());
         $rc->execute_plan();
         $rc->destroy();
index 29b2734..92c840a 100644 (file)
@@ -45,6 +45,55 @@ abstract class restore_step extends base_step {
         }
         return $this->task->get_restoreid();
     }
+
+    /**
+     * Apply course startdate offset based in original course startdate and course_offset_startdate setting
+     * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple
+     * executions in the same request
+     *
+     * @param int $value Time value (seconds since epoch), or empty for nothing
+     * @return int Time value after applying the date offset, or empty for nothing
+     */
+    public function apply_date_offset($value) {
+
+        // Empties don't offset - zeros (int and string), false and nulls return original value.
+        if (empty($value)) {
+            return $value;
+        }
+
+        static $cache = array();
+        // Lookup cache.
+        if (isset($cache[$this->get_restoreid()])) {
+            return $value + $cache[$this->get_restoreid()];
+        }
+        // No cache, let's calculate the offset.
+        $original = $this->task->get_info()->original_course_startdate;
+        $setting = 0;
+        if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019).
+            $setting  = $this->get_setting_value('course_startdate');
+        }
+
+        if (empty($original) || empty($setting)) {
+            // Original course has not startdate or setting doesn't exist, offset = 0.
+            $cache[$this->get_restoreid()] = 0;
+
+        } else if (abs($setting - $original) < 24 * 60 * 60) {
+            // Less than 24h of difference, offset = 0 (this avoids some problems with timezones).
+            $cache[$this->get_restoreid()] = 0;
+
+        } else if (!has_capability('moodle/restore:rolldates',
+               context_course::instance($this->get_courseid()), $this->task->get_userid())) {
+            // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case.
+            $cache[$this->get_restoreid()] = 0;
+
+        } else {
+            // Arrived here, let's calculate the real offset.
+            $cache[$this->get_restoreid()] = $setting - $original;
+        }
+
+        // Return the passed value with cached offset applied.
+        return $value + $cache[$this->get_restoreid()];
+    }
 }
 
 /*
index e4685b0..94e5ad6 100644 (file)
@@ -245,53 +245,6 @@ abstract class restore_structure_step extends restore_step {
         $this->task->add_result($resultstoadd);
     }
 
-    /**
-     * Apply course startdate offset based in original course startdate and course_offset_startdate setting
-     * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple
-     * executions in the same request
-     */
-    public function apply_date_offset($value) {
-
-        // empties don't offset - zeros (int and string), false and nulls return original value
-        if (empty($value)) {
-            return $value;
-        }
-
-        static $cache = array();
-        // Lookup cache
-        if (isset($cache[$this->get_restoreid()])) {
-            return $value + $cache[$this->get_restoreid()];
-        }
-        // No cache, let's calculate the offset
-        $original = $this->task->get_info()->original_course_startdate;
-        $setting = 0;
-        if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019)
-            $setting  = $this->get_setting_value('course_startdate');
-        }
-
-        // Original course has not startdate or setting doesn't exist, offset = 0
-        if (empty($original) || empty($setting)) {
-            $cache[$this->get_restoreid()] = 0;
-
-        // Less than 24h of difference, offset = 0 (this avoids some problems with timezones)
-        } else if (abs($setting - $original) < 24 * 60 * 60) {
-            $cache[$this->get_restoreid()] = 0;
-
-        // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case
-        } else if (!has_capability('moodle/restore:rolldates',
-                                   context_course::instance($this->get_courseid()),
-                                   $this->task->get_userid())) {
-            $cache[$this->get_restoreid()] = 0;
-
-        // Arrived here, let's calculate the real offset
-        } else {
-            $cache[$this->get_restoreid()] = $setting - $original;
-        }
-
-        // Return the passed value with cached offset applied
-        return $value + $cache[$this->get_restoreid()];
-    }
-
     /**
      * As far as restore structure steps are implementing restore_plugin stuff, they need to
      * have the parent task available for wrapping purposes (get course/context....)
index 8176a33..4503afb 100644 (file)
@@ -56,6 +56,8 @@ $string['allowthemechangeonurl'] = 'Allow theme changes in the URL';
 $string['allowuserblockhiding'] = 'Allow users to hide blocks';
 $string['allowuserswitchrolestheycantassign'] = 'Allow users without the assign roles capability to switch roles';
 $string['allowuserthemes'] = 'Allow user themes';
+$string['alternativefullnameformat'] = 'Alternative full name format';
+$string['alternativefullnameformat_desc'] = 'This defines how names are shown to users with the viewfullnames capability (by default users with the role of manager, teacher or non-editing teacher). Placeholders that can be used are as for the "Full name format" setting.';
 $string['antivirus'] = 'Anti-Virus';
 $string['appearance'] = 'Appearance';
 $string['aspellpath'] = 'Path to aspell';
index 7bef32a..82b3a01 100644 (file)
@@ -262,6 +262,7 @@ $string['groupnotaddedtogroupingerror'] = 'Group "{$a->groupname}" not added to
 $string['groupingnotaddederror'] = 'Grouping "{$a}" not added';
 $string['groupunknown'] = 'Group {$a} not associated to specified course';
 $string['groupusernotmember'] = 'User is not member of this group.';
+$string['guestcantaccessprofiles'] = 'Guests cannot access user profiles. Log in with a full user account to continue.';
 $string['guestnocomment'] = 'Guests are not allowed to post comments!';
 $string['guestnoeditprofile'] = 'The guest user cannot edit their profile';
 $string['guestnoeditprofileother'] = 'The guest user profile cannot be edited';
index f7b0454..2f916de 100644 (file)
@@ -591,6 +591,7 @@ $cache = '.var_export($cache, true).';
                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
             }
         }
+        ksort(self::$classmap);
     }
 
     /**
index dbd3f51..13632b2 100644 (file)
@@ -25,12 +25,22 @@ selector = {
     select: null,
     input: null,
     button: null,
+    goodbrowser: false,
+    alloptions: [],
 
     filter_init: function(strsearch, selectinputid) {
+        selector.goodbrowser = !(' ' + document.body.className + ' ').match(/ ie | safari /);
+
         // selector.id = selectinputid
         selector.select = document.getElementById(selectinputid);
         selector.button = document.getElementById('settingssubmit');
 
+        // Copy all selector options into a plain array. selector.select.options
+        // is linked live to the document, which causes problems in IE and Safari.
+        for (var i = 0; i < selector.select.options.length; i++) {
+            selector.alloptions[i] = selector.select.options[i];
+        }
+
         // Create a div to hold the search UI.
         var div = document.createElement('div');
         div.id = 'searchui';
@@ -50,37 +60,60 @@ selector = {
         div.appendChild(label);
         div.appendChild(input);
         selector.select.parentNode.insertBefore(div, selector.select);
-        YUI().use('yui2-event', function(Y) {
-            Y.YUI2.util.Event.addListener(input, 'keyup', selector.filter_change);
-        });
+        input.addEventListener('keyup', selector.filter_change);
     },
 
     filter_change: function() {
         var searchtext = selector.input.value.toLowerCase();
-        var options = selector.select.options;
-        var matchingoption = -1;
-        for (var i = 0; i < options.length; i++) {
-            var optiontext = options[i].text.toLowerCase();
-            if (optiontext.indexOf(searchtext) >= 0) { //the option is matching the search text
-                options[i].disabled = false; //the option must be visible
-                options[i].style.display = 'block';
-                if (matchingoption == -1) { //we found at least one
-                    matchingoption = i;
-                }
+        var found = false;
+        for (var i = 0; i < selector.alloptions.length; i++) {
+            var option = selector.alloptions[i];
+            if (option.text.toLowerCase().indexOf(searchtext) >= 0) {
+                // The option is matching the search text.
+                selector.set_visible(option, true);
+                found = true;
             } else {
-                options[i].disabled = true;
-                options[i].selected = false;
-                options[i].style.display = 'none';
+                selector.set_visible(option, false);
             }
         }
 
-        if (matchingoption == -1) { //the search didn't find any matching, color the search text in red
-            selector.input.className = "error";
-        } else {
+        if (found) {
+            // No error.
             selector.input.className = "";
+        } else {
+            // The search didn't find any matching, color the search text in red.
+            selector.input.className = "error";
         }
+    },
 
+    set_visible: function(element, visible) {
+        if (selector.goodbrowser) {
+            if (visible) {
+                element.style.display = 'block';
+            } else {
+                element.style.display = 'none';
+                element.selected = false;
+            }
+        } else {
+            // This is a deeply evil hack to make the filtering work in IE.
+            // IE ignores display: none; on select options, but wrapping the
+            // option in a span does seem to hide the option.
+            // Thanks http://work.arounds.org/issue/96/option-elements-do-not-hide-in-IE/
+            if (visible) {
+                if (element.parentNode.tagName.toLowerCase() === 'span') {
+                    element.parentNode.parentNode.replaceChild(element, element.parentNode); // New, old.
+                }
+                element.enabled = true;
+            } else {
+                if (element.parentNode.tagName.toLowerCase() !== 'span') {
+                    var span = document.createElement('span');
+                    element.parentNode.replaceChild(span, element); // New, old.
+                    span.appendChild(element);
+                    span.style.display = 'none';
+                }
+                element.enabled = false;
+                element.selected = false;
+            }
+        }
     }
-
 };
-
index 763d726..6f92cf9 100644 (file)
@@ -3602,11 +3602,22 @@ function fullname($user, $override=false) {
     if (isset($CFG->fullnamedisplay)) {
         $template = $CFG->fullnamedisplay;
     }
-    // If the template is empty, or set to language, or $override is set, return the language string.
-    if (empty($template) || $template == 'language' || $override) {
+    // If the template is empty, or set to language, return the language string.
+    if ((empty($template) || $template == 'language') && !$override) {
         return get_string('fullnamedisplay', null, $user);
     }
 
+    // Check to see if we are displaying according to the alternative full name format.
+    if ($override) {
+        if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
+            // Default to show just the user names according to the fullnamedisplay string.
+            return get_string('fullnamedisplay', null, $user);
+        } else {
+            // If the override is true, then change the template to use the complete name.
+            $template = $CFG->alternativefullnameformat;
+        }
+    }
+
     $requirednames = array();
     // With each name, see if it is in the display name template, and add it to the required names array if it is.
     foreach ($allnames as $allname) {
index 776d1fc..f875a02 100644 (file)
@@ -2321,6 +2321,7 @@ class core_moodlelib_testcase extends advanced_testcase {
         // Back up config settings for restore later.
         $originalcfg = new stdClass();
         $originalcfg->fullnamedisplay = $CFG->fullnamedisplay;
+        $originalcfg->alternativefullnameformat = $CFG->alternativefullnameformat;
 
         // Testing existing fullnamedisplay settings.
         $CFG->fullnamedisplay = 'firstname';
@@ -2348,6 +2349,25 @@ class core_moodlelib_testcase extends advanced_testcase {
         $testname = fullname($user, true);
         $this->assertSame($expectedname, $testname);
 
+        // Test alternativefullnameformat setting.
+        // Test alternativefullnameformat that has been set to nothing.
+        $CFG->alternativefullnameformat = '';
+        $expectedname = "$user->firstname $user->lastname";
+        $testname = fullname($user, true);
+        $this->assertSame($expectedname, $testname);
+
+        // Test alternativefullnameformat that has been set to 'language'.
+        $CFG->alternativefullnameformat = 'language';
+        $expectedname = "$user->firstname $user->lastname";
+        $testname = fullname($user, true);
+        $this->assertSame($expectedname, $testname);
+
+        // Test customising the alternativefullnameformat setting with all additional name fields.
+        $CFG->alternativefullnameformat = 'firstname lastname firstnamephonetic lastnamephonetic middlename alternatename';
+        $expectedname = "$user->firstname $user->lastname $user->firstnamephonetic $user->lastnamephonetic $user->middlename $user->alternatename";
+        $testname = fullname($user, true);
+        $this->assertSame($expectedname, $testname);
+
         // Test additional name fields.
         $CFG->fullnamedisplay = 'lastname lastnamephonetic firstname firstnamephonetic';
         $expectedname = "$user->lastname $user->lastnamephonetic $user->firstname $user->firstnamephonetic";
@@ -2428,6 +2448,7 @@ class core_moodlelib_testcase extends advanced_testcase {
 
         // Tidy up after we finish testing.
         $CFG->fullnamedisplay = $originalcfg->fullnamedisplay;
+        $CFG->alternativefullnameformat = $originalcfg->alternativefullnameformat;
     }
 
     public function test_get_all_user_name_fields() {
index 1cd3abb..de7f1eb 100644 (file)
@@ -265,7 +265,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
             // There was a question_instance in the backup file for a question
             // that was not acutally in the quiz. Drop it.
             $this->log('question ' . $data->questionid . ' was associated with quiz ' .
-                    $quiz->id . ' but not actually used. ' .
+                    $this->get_new_parentid('quiz') . ' but not actually used. ' .
                     'The instance has been ignored.', backup::LOG_INFO);
             return;
         }
index 912bcae..6c8c1f5 100644 (file)
@@ -42,6 +42,9 @@ class mod_quiz_mod_form extends moodleform_mod {
     protected $_feedbacks;
     protected static $reviewfields = array(); // Initialised in the constructor.
 
+    /** @var int the max number of attempts allowed in any user or group override on this quiz. */
+    protected $maxattemptsanyoverride = null;
+
     public function __construct($current, $section, $cm, $course) {
         self::$reviewfields = array(
             'attempt'          => array('theattempt', 'quiz'),
@@ -136,7 +139,9 @@ class mod_quiz_mod_form extends moodleform_mod {
         $mform->addHelpButton('grademethod', 'grademethod', 'quiz');
         $mform->setAdvanced('grademethod', $quizconfig->grademethod_adv);
         $mform->setDefault('grademethod', $quizconfig->grademethod);
-        $mform->disabledIf('grademethod', 'attempts', 'eq', 1);
+        if ($this->get_max_attempts_for_any_override() < 2) {
+            $mform->disabledIf('grademethod', 'attempts', 'eq', 1);
+        }
 
         // -------------------------------------------------------------------------------
         $mform->addElement('header', 'layouthdr', get_string('layout', 'quiz'));
@@ -219,7 +224,9 @@ class mod_quiz_mod_form extends moodleform_mod {
         $mform->addHelpButton('attemptonlast', 'eachattemptbuildsonthelast', 'quiz');
         $mform->setAdvanced('attemptonlast', $quizconfig->attemptonlast_adv);
         $mform->setDefault('attemptonlast', $quizconfig->attemptonlast);
-        $mform->disabledIf('attemptonlast', 'attempts', 'eq', 1);
+        if ($this->get_max_attempts_for_any_override() < 2) {
+            $mform->disabledIf('attemptonlast', 'attempts', 'eq', 1);
+        }
 
         // -------------------------------------------------------------------------------
         $mform->addElement('header', 'reviewoptionshdr',
@@ -309,15 +316,19 @@ class mod_quiz_mod_form extends moodleform_mod {
         $mform->addHelpButton('delay1', 'delay1st2nd', 'quiz');
         $mform->setAdvanced('delay1', $quizconfig->delay1_adv);
         $mform->setDefault('delay1', $quizconfig->delay1);
-        $mform->disabledIf('delay1', 'attempts', 'eq', 1);
+        if ($this->get_max_attempts_for_any_override() < 2) {
+            $mform->disabledIf('delay1', 'attempts', 'eq', 1);
+        }
 
         $mform->addElement('duration', 'delay2', get_string('delaylater', 'quiz'),
                 array('optional' => true));
         $mform->addHelpButton('delay2', 'delaylater', 'quiz');
         $mform->setAdvanced('delay2', $quizconfig->delay2_adv);
         $mform->setDefault('delay2', $quizconfig->delay2);
-        $mform->disabledIf('delay2', 'attempts', 'eq', 1);
-        $mform->disabledIf('delay2', 'attempts', 'eq', 2);
+        if ($this->get_max_attempts_for_any_override() < 3) {
+            $mform->disabledIf('delay2', 'attempts', 'eq', 1);
+            $mform->disabledIf('delay2', 'attempts', 'eq', 2);
+        }
 
         // Browser security choices.
         $mform->addElement('select', 'browsersecurity', get_string('browsersecurity', 'quiz'),
@@ -618,4 +629,33 @@ class mod_quiz_mod_form extends moodleform_mod {
     public function completion_rule_enabled($data) {
         return !empty($data['completionattemptsexhausted']) || !empty($data['completionpass']);
     }
+
+    /**
+     * Get the maximum number of attempts that anyone might have due to a user
+     * or group override. Used to decide whether disabledIf rules should be applied.
+     * @return int the number of attempts allowed. For the purpose of this method,
+     * unlimited is returned as 1000, not 0.
+     */
+    public function get_max_attempts_for_any_override() {
+        global $DB;
+
+        if (empty($this->_instance)) {
+            // Quiz not created yet, so no overrides.
+            return 1;
+        }
+
+        if ($this->maxattemptsanyoverride === null) {
+            $this->maxattemptsanyoverride = $DB->get_field_sql("
+                    SELECT MAX(CASE WHEN attempts = 0 THEN 1000 ELSE attempts END)
+                      FROM {quiz_overrides}
+                     WHERE quiz = ?",
+                    array($this->_instance));
+            if ($this->maxattemptsanyoverride < 1) {
+                // This happens when no override alters the number of attempts.
+                $this->maxattemptsanyoverride = 1;
+            }
+        }
+
+        return $this->maxattemptsanyoverride;
+    }
 }
index 4b904f7..7814b10 100644 (file)
@@ -272,7 +272,7 @@ if ($groupmode) {
     }
     echo $OUTPUT->single_button($overrideediturl->out(true,
             array('action' => 'adduser', 'cmid' => $cm->id)),
-            get_string('addnewuseroverride', 'quiz'), 'post', $options);
+            get_string('addnewuseroverride', 'quiz'), 'get', $options);
 }
 echo html_writer::end_tag('div');
 echo html_writer::end_tag('div');
index 97a6a88..7cd8cff 100644 (file)
@@ -67,7 +67,7 @@ if ($ADMIN->fulltree) {
     // What to do with overdue attempts.
     $quizsettings->add(new mod_quiz_admin_setting_overduehandling('quiz/overduehandling',
             get_string('overduehandling', 'quiz'), get_string('overduehandling_desc', 'quiz'),
-            array('value' => 'autoabandon', 'adv' => false), null));
+            array('value' => 'autosubmit', 'adv' => false), null));
 
     // Grace period time.
     $quizsettings->add(new admin_setting_configtext_with_advanced('quiz/graceperiod',
diff --git a/mod/quiz/tests/behat/settings_form_fields_disableif.feature b/mod/quiz/tests/behat/settings_form_fields_disableif.feature
new file mode 100644 (file)
index 0000000..0e7213e
--- /dev/null
@@ -0,0 +1,92 @@
+@mod @mod_quiz
+Feature: Settings form fields disabled if not required
+  In to create quizzes as simply as possible
+  As a teacher
+  I don't need to to use certain form fields.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname |
+      | teacher  | Teach     |
+      | student1 | Student1  |
+      | student2 | Student2  |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0 |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher  | C1     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+    And I log in as "teacher"
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  @javascript
+  Scenario: Depending on the number of attempts, different form fields are disabled.
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Name" to "Test quiz"
+    And I set the field "Attempts allowed" to "1"
+    Then the "Grading method" "field" should be disabled
+    And the "Each attempt builds on the last" "field" should be disabled
+    And the "id_delay1_enabled" "field" should be disabled
+    And the "id_delay2_enabled" "field" should be disabled
+
+    When I set the field "Attempts allowed" to "2"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    And the "id_delay1_enabled" "field" should be enabled
+    And the "id_delay2_enabled" "field" should be disabled
+
+    When I set the field "Attempts allowed" to "3"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    And the "id_delay1_enabled" "field" should be enabled
+    And the "id_delay2_enabled" "field" should be enabled
+
+    When I set the field "Attempts allowed" to "Unlimited"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    # And the "id_delay1_enabled" "field" should be enabled
+    # And the "id_delay2_enabled" "field" should be enabled
+
+    When I press "Save and display"
+    And I navigate to "User overrides" node in "Quiz administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+        | Override user    | Student1 |
+        | Attempts allowed | 3        |
+    And I press "Save"
+    And I navigate to "Edit settings" node in "Quiz administration"
+    And I set the field "Attempts allowed" to "1"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    And the "id_delay1_enabled" "field" should be enabled
+    And the "id_delay2_enabled" "field" should be enabled
+
+    When I press "Save and display"
+    And I navigate to "User overrides" node in "Quiz administration"
+    And I follow "Edit"
+    And I set the field "Attempts allowed" to "2"
+    And I press "Save"
+    And I navigate to "Edit settings" node in "Quiz administration"
+    And I set the field "Attempts allowed" to "1"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    And the "id_delay1_enabled" "field" should be enabled
+    And the "id_delay2_enabled" "field" should be disabled
+
+    When I press "Save and display"
+    And I navigate to "User overrides" node in "Quiz administration"
+    And I press "Add user override"
+    And I set the following fields to these values:
+        | Override user    | Student2  |
+        | Attempts allowed | Unlimited |
+    And I press "Save"
+    And I navigate to "Edit settings" node in "Quiz administration"
+    And I set the field "Attempts allowed" to "1"
+    Then the "Grading method" "field" should be enabled
+    And the "Each attempt builds on the last" "field" should be enabled
+    And the "id_delay1_enabled" "field" should be enabled
+    And the "id_delay2_enabled" "field" should be enabled
index 00c412d..147ed20 100644 (file)
@@ -209,6 +209,8 @@ class qtype_multianswer extends question_type {
 
         foreach ($questiondata->options->questions as $key => $subqdata) {
             $subqdata->contextid = $questiondata->contextid;
+            $subqdata->options->shuffleanswers = !isset($questiondata->options->shuffleanswers) ||
+                    $questiondata->options->shuffleanswers;
             $question->subquestions[$key] = question_bank::make_question($subqdata);
             $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
             if (isset($subqdata->options->layout)) {
index 8b1bb5c..03db81f 100644 (file)
@@ -49,7 +49,14 @@ if (!empty($CFG->forceloginforprofiles)) {
     require_login();
     if (isguestuser()) {
         $SESSION->wantsurl = $PAGE->url->out(false);
-        redirect(get_login_url());
+
+        $PAGE->set_context(context_system::instance());
+        echo $OUTPUT->header();
+        echo $OUTPUT->confirm(get_string('guestcantaccessprofiles', 'error'),
+                              get_login_url(),
+                              $CFG->wwwroot);
+        echo $OUTPUT->footer();
+        die;
     }
 } else if (!empty($CFG->forcelogin)) {
     require_login();