Merge branch 'm23_MDL-35562' of git://github.com/danmarsden/moodle into MOODLE_23_STABLE
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 2 Oct 2012 13:59:09 +0000 (15:59 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 2 Oct 2012 13:59:09 +0000 (15:59 +0200)
43 files changed:
admin/settings/users.php
course/tests/externallib_test.php
enrol/locallib.php
grade/report/user/lib.php
group/lib.php
lang/en/admin.php
lib/modinfolib.php
lib/outputcomponents.php
lib/pagelib.php
lib/questionlib.php
lib/tests/outputcomponents_test.php
mod/assign/lib.php
mod/forum/lib.php
mod/quiz/attempt.php
mod/quiz/cronlib.php
mod/quiz/locallib.php
mod/quiz/module.js
mod/quiz/review.php
mod/quiz/styles.css
mod/quiz/summary.php
mod/resource/lib.php
question/behaviour/rendererbase.php
question/engine/datalib.php
question/engine/questionattemptstep.php
question/format/xhtml/format.php
question/type/calculated/lang/en/qtype_calculated.php
question/type/calculated/questiontype.php
question/type/essay/renderer.php
question/type/match/lang/en/qtype_match.php
question/type/match/renderer.php
question/type/multianswer/renderer.php
question/type/multianswer/tests/helper.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/lang/en/qtype_numerical.php
question/type/numerical/question.php
question/type/numerical/renderer.php
question/type/numerical/tests/question_test.php
question/type/shortanswer/question.php
question/type/shortanswer/questiontype.php
question/type/shortanswer/renderer.php
user/lib.php
user/selector/module.js
user/tests/externallib_test.php [new file with mode: 0644]

index f317102..232745d 100644 (file)
@@ -150,6 +150,7 @@ if ($hassiteconfig
                     'institution' => new lang_string('institution'),
                 )));
         $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'));
     }
 
     $ADMIN->add('roles', $temp);
index 984b9ed..605c5d8 100644 (file)
@@ -580,5 +580,6 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         // Check that the course has been duplicated.
         $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
+        gc_collect_cycles();
     }
 }
index 41b7337..e923c2e 100644 (file)
@@ -766,13 +766,15 @@ class course_enrolment_manager {
 
         $users = array();
         foreach ($userroles as $userrole) {
+            $contextid = $userrole->contextid;
+            unset($userrole->contextid); // This would collide with user avatar.
             if (!array_key_exists($userrole->id, $users)) {
                 $users[$userrole->id] = $this->prepare_user_for_display($userrole, $extrafields, $now);
             }
             $a = new stdClass;
             $a->role = $roles[$userrole->roleid]->localname;
             $changeable = ($userrole->component == '');
-            if ($userrole->contextid == $this->context->id) {
+            if ($contextid == $this->context->id) {
                 $roletext = get_string('rolefromthiscourse', 'enrol', $a);
             } else {
                 $changeable = false;
index 915aefd..a952b33 100644 (file)
@@ -228,6 +228,26 @@ class grade_report_user extends grade_report {
         }
         $count = 1;
         foreach ($element['children'] as $key=>$child) {
+
+            $grade_object = $child['object'];
+            // If grade object isn't hidden
+            if ($grade_object->hidden != 1) {
+
+                // If grade object is an module instance
+                if (!empty($grade_object->itemmodule) && !empty($grade_object->iteminstance)) {
+
+                    $instances = $this->gtree->modinfo->get_instances();
+                    // If we can find the module instance
+                    if (!empty($instances[$grade_object->itemmodule][$grade_object->iteminstance])) {
+
+                        $cm = $instances[$grade_object->itemmodule][$grade_object->iteminstance];
+                        // Skip generating rowspans if the user cannot see the module instance
+                        if (!$cm->uservisible) {
+                            continue;
+                        }
+                    }
+                }
+            }
             $count += $this->inject_rowspans($element['children'][$key]);
         }
         $element['rowspan'] = $count;
@@ -341,23 +361,25 @@ class grade_report_user extends grade_report {
                 $hidden = ' hidden';
             }
 
+            $hide = false;
             // If this is a hidden grade item, hide it completely from the user.
             if ($grade_grade->is_hidden() && !$this->canviewhidden && (
                     $this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN ||
                     ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$grade_grade->is_hiddenuntil()))) {
-                // return false;
-            } else {
-                // The grade object can be marked visible but still be hidden
-                // if "enablegroupmembersonly" is on and its an activity assigned to a grouping the user is not in
-                if (!empty($grade_object->itemmodule) && !empty($grade_object->iteminstance)) {
-                    $instances = $this->gtree->modinfo->get_instances();
-                    if (!empty($instances[$grade_object->itemmodule][$grade_object->iteminstance])) {
-                        $cm = $instances[$grade_object->itemmodule][$grade_object->iteminstance];
-                        if (!$cm->uservisible) {
-                            return false;
-                        }
+                $hide = true;
+            } else if (!empty($grade_object->itemmodule) && !empty($grade_object->iteminstance)) {
+                // The grade object can be marked visible but still be hidden if "enablegroupmembersonly"
+                // is on and it's an activity assigned to a grouping the user is not in.
+                $instances = $this->gtree->modinfo->get_instances_of($grade_object->itemmodule);
+                if (!empty($instances[$grade_object->iteminstance])) {
+                    $cm = $instances[$grade_object->iteminstance];
+                    if ($cm->is_user_access_restricted_by_group()) {
+                        $hide = true;
                     }
                 }
+            }
+
+            if (!$hide) {
                 /// Excluded Item
                 if ($grade_grade->is_excluded()) {
                     $fullname .= ' ['.get_string('excluded', 'grades').']';
index 8addc93..024e81b 100644 (file)
@@ -239,17 +239,18 @@ function groups_update_group_icon($group, $data, $editform) {
     $context = get_context_instance(CONTEXT_COURSE, $group->courseid, MUST_EXIST);
 
     //TODO: it would make sense to allow picture deleting too (skodak)
-
-    if ($iconfile = $editform->save_temp_file('imagefile')) {
-        if (process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) {
-            $DB->set_field('groups', 'picture', 1, array('id'=>$group->id));
-            $group->picture = 1;
-        } else {
-            $fs->delete_area_files($context->id, 'group', 'icon', $group->id);
-            $DB->set_field('groups', 'picture', 0, array('id'=>$group->id));
-            $group->picture = 0;
+    if (!empty($CFG->gdversion)) {
+        if ($iconfile = $editform->save_temp_file('imagefile')) {
+            if (process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) {
+                $DB->set_field('groups', 'picture', 1, array('id'=>$group->id));
+                $group->picture = 1;
+            } else {
+                $fs->delete_area_files($context->id, 'group', 'icon', $group->id);
+                $DB->set_field('groups', 'picture', 0, array('id'=>$group->id));
+                $group->picture = 0;
+            }
+            @unlink($iconfile);
         }
-        @unlink($iconfile);
     }
 }
 
index 5d26680..8a6a935 100644 (file)
@@ -551,6 +551,8 @@ $string['googlemapkey3_help'] = 'You need to enter a special key to use Google M
 $string['gotofirst'] = 'Go to first missing string';
 $string['gradebook'] = 'Gradebook';
 $string['gradebookroles'] = 'Graded roles';
+$string['gravatardefaulturl'] = 'Gravatar default image URL';
+$string['gravatardefaulturl_help'] = 'Gravatar needs a default image to display if it is unable to find a picture for a given user. Provide a full URL for an image. If you leave this setting empty, Moodle will attempt to use the most appropriate default image for the page you are viewing. Note also that Gravatar has a number of codes which can be used to <a href="https://en.gravatar.com/site/implement/images/#default-image">generate default images</a>.';
 $string['gradeexport'] = 'Primary grade export methods';
 $string['guestroleid'] = 'Role for guest';
 $string['guestroleid_help'] = 'This role is automatically assigned to the guest user. It is also temporarily assigned to not enrolled users that enter the course via guest enrolment plugin.';
index 0739f76..c787896 100644 (file)
@@ -1095,17 +1095,31 @@ class cm_info extends stdClass {
         }
         // Check group membership. The grouping option makes the activity
         // completely invisible as it does not apply to the user at all.
+        if ($this->is_user_access_restricted_by_group()) {
+            $this->uservisible = false;
+            // Ensure activity is completely hidden from user.
+            $this->showavailability = 0;
+        }
+    }
+
+    /**
+     * Checks whether the module group settings restrict the user access.
+     * @return bool true if the user access is restricted
+     */
+    public function is_user_access_restricted_by_group() {
+        global $CFG;
+        $modcontext = context_module::instance($this->id);
+        $userid = $this->modinfo->get_user_id();
         if (!empty($CFG->enablegroupmembersonly) and !empty($this->groupmembersonly)
                 and !has_capability('moodle/site:accessallgroups', $modcontext, $userid)) {
             // If the activity has 'group members only' and you don't have accessallgroups...
             $groups = $this->modinfo->get_groups($this->groupingid);
             if (empty($groups)) {
                 // ...and you don't belong to a group, then set it so you can't see/access it
-                $this->uservisible = false;
-                // Ensure activity is completely hidden from user.
-                $this->showavailability = 0;
+                return true;
             }
         }
+        return false;
     }
 
     /**
index a20bd7c..5e1cf96 100644 (file)
@@ -384,12 +384,26 @@ class user_picture implements renderable {
             // Hash the users email address
             $md5 = md5(strtolower(trim($this->user->email)));
             // Build a gravatar URL with what we know.
+
+            // Find the best default image URL we can (MDL-35669)
+            if (empty($CFG->gravatardefaulturl)) {
+                $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
+                if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
+                    $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
+                } else {
+                    $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
+                }
+            } else {
+                $gravatardefault = $CFG->gravatardefaulturl;
+            }
+
             // If the currently requested page is https then we'll return an
             // https gravatar page.
             if (strpos($CFG->httpswwwroot, 'https:') === 0) {
-                return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $defaulturl->out(false)));
+                $gravatardefault = str_replace($CFG->wwwroot, $CFG->httpswwwroot, $gravatardefault); // Replace by secure url.
+                return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
             } else {
-                return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $defaulturl->out(false)));
+                return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
             }
         }
 
index 36172c7..ba0c596 100644 (file)
@@ -1073,6 +1073,7 @@ class moodle_page {
      */
     public function set_title($title) {
         $title = format_string($title);
+        $title = strip_tags($title);
         $title = str_replace('"', '&quot;', $title);
         $this->_title = $title;
     }
index c8aeacd..311629d 100644 (file)
@@ -1179,12 +1179,25 @@ function question_add_tops($categories, $pcontexts) {
 function question_categorylist($categoryid) {
     global $DB;
 
-    $subcategories = $DB->get_records('question_categories',
-            array('parent' => $categoryid), 'sortorder ASC', 'id, 1');
+    // final list of category IDs
+    $categorylist = array();
 
-    $categorylist = array($categoryid);
-    foreach ($subcategories as $subcategory) {
-        $categorylist = array_merge($categorylist, question_categorylist($subcategory->id));
+    // a list of category IDs to check for any sub-categories
+    $subcategories = array($categoryid);
+
+    while ($subcategories) {
+        foreach ($subcategories as $subcategory) {
+            // if anything from the temporary list was added already, then we have a loop
+            if (isset($categorylist[$subcategory])) {
+                throw new coding_exception("Category id=$subcategory is already on the list - loop of categories detected.");
+            }
+            $categorylist[$subcategory] = $subcategory;
+        }
+
+        list ($in, $params) = $DB->get_in_or_equal($subcategories);
+
+        $subcategories = $DB->get_records_select_menu('question_categories',
+                "parent $in", $params, NULL, 'id,id AS id2');
     }
 
     return $categorylist;
index c515888..d5d50bf 100644 (file)
@@ -126,6 +126,7 @@ class user_picture_testcase extends advanced_testcase {
         $this->assertEquals('http://www.example.com/moodle', $CFG->wwwroot);
         $this->assertEquals($CFG->wwwroot, $CFG->httpswwwroot);
         $this->assertEquals(0, $CFG->enablegravatar);
+        $this->assertEquals('mm', $CFG->gravatardefaulturl);
 
         // create some users
         $page = new moodle_page();
@@ -197,21 +198,25 @@ class user_picture_testcase extends advanced_testcase {
         // test gravatar
         set_config('enablegravatar', 1);
 
-        $up2 = new user_picture($user2);
-        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fimage.php%2Fstandard%2Fcore%2F1%2Fu%2Ff2', $up2->get_url($page, $renderer)->out(false));
-
-        // uploaded image takes precedence before gravatar
-        $up1 = new user_picture($user1);
-        $this->assertEquals($CFG->wwwroot.'/pluginfile.php/15/user/icon/standard/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
-
         // deleted user can not have gravatar
         $user3->email = 'deleted';
         $user3->picture = 0;
         $up3 = new user_picture($user3);
         $this->assertEquals($CFG->wwwroot.'/theme/image.php/standard/core/1/u/f2', $up3->get_url($page, $renderer)->out(false));
 
+        // verify defaults to misteryman (mm)
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=mm', $up2->get_url($page, $renderer)->out(false));
+
+        // without gravatardefaulturl, verify we pick own file
+        set_config('gravatardefaulturl', '');
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Fpix%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+        // uploaded image takes precedence before gravatar
+        $up1 = new user_picture($user1);
+        $this->assertEquals($CFG->wwwroot.'/pluginfile.php/15/user/icon/standard/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
 
-        // https versions
+        // https version
         $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
 
         $up1 = new user_picture($user1);
@@ -221,7 +226,26 @@ class user_picture_testcase extends advanced_testcase {
         $this->assertEquals($CFG->httpswwwroot.'/theme/image.php/standard/core/1/u/f2', $up3->get_url($page, $renderer)->out(false));
 
         $up2 = new user_picture($user2);
-        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fimage.php%2Fstandard%2Fcore%2F1%2Fu%2Ff2', $up2->get_url($page, $renderer)->out(false));
+        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Fpix%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+
+        // now test gravatar with one theme having own images (afterburner)
+        $CFG->httpswwwroot = $CFG->wwwroot;
+        $this->assertTrue(file_exists("$CFG->dirroot/theme/afterburner/config.php"));
+        set_config('theme', 'afterburner');
+        $page = new moodle_page();
+        $page->set_url('/user/profile.php');
+        $page->set_context(context_system::instance());
+        $renderer = $page->get_renderer('core');
+
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fafterburner%2Fpix_core%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+
+        // https version
+        $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
+
+        $up2 = new user_picture($user2);
+        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fafterburner%2Fpix_core%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+        // end of gravatar tests
 
         // test themed images
         set_config('enablegravatar', 0);
index 070dc50..74cfd3b 100644 (file)
@@ -757,7 +757,9 @@ function assign_get_user_grades($assign, $userid=0) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
 
-    $assignment = new assign(null, null, null);
+    $cm = get_coursemodule_from_instance('assign', $assign->id, 0, false, MUST_EXIST);
+    $context = context_module::instance($cm->id);
+    $assignment = new assign($context, null, null);
     $assignment->set_instance($assign);
     return $assignment->get_user_grades_for_gradebook($userid);
 }
index 9c3e801..7cfb638 100644 (file)
@@ -5322,7 +5322,7 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
 
         return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)) ||
                 $firstpost->id == $post->id || $post->userid == $user->id || $firstpost->userid == $user->id ||
-                has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id, false));
+                has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id));
     }
     return true;
 }
index 135ae14..1cbda08 100644 (file)
@@ -76,8 +76,9 @@ if ($attemptobj->is_finished()) {
 
 // Check the access rules.
 $accessmanager = $attemptobj->get_access_manager(time());
-$messages = $accessmanager->prevent_access();
+$accessmanager->setup_attempt_page($PAGE);
 $output = $PAGE->get_renderer('mod_quiz');
+$messages = $accessmanager->prevent_access();
 if (!$attemptobj->is_preview_user() && $messages) {
     print_error('attempterror', 'quiz', $attemptobj->view_url(),
             $output->access_messages($messages));
@@ -120,7 +121,6 @@ $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number());
 $headtags = $attemptobj->get_html_head_contributions($page);
 $PAGE->set_title(format_string($attemptobj->get_quiz_name()));
 $PAGE->set_heading($attemptobj->get_course()->fullname);
-$accessmanager->setup_attempt_page($PAGE);
 
 if ($attemptobj->is_last_page($page)) {
     $nextpage = -1;
index 7913b63..9e4e6a4 100644 (file)
@@ -117,8 +117,8 @@ class mod_quiz_overdue_attempt_updater {
            FROM {quiz_attempts} iquiza
            JOIN {quiz} quiz ON quiz.id = iquiza.quiz
       LEFT JOIN {quiz_overrides} quo ON quo.quiz = quiz.id AND quo.userid = iquiza.userid
-      LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id
-      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid AND gm.groupid = qgo.groupid
+      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
+      LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id AND qgo.groupid = gm.groupid
 
           WHERE iquiza.state IN ('inprogress', 'overdue')
             AND iquiza.timemodified >= :processfrom
index f535e9b..fc59804 100644 (file)
@@ -1456,13 +1456,14 @@ function quiz_get_js_module() {
         'name' => 'mod_quiz',
         'fullpath' => '/mod/quiz/module.js',
         'requires' => array('base', 'dom', 'event-delegate', 'event-key',
-                'core_question_engine'),
+                'core_question_engine', 'moodle-core-formchangechecker'),
         'strings' => array(
             array('cancel', 'moodle'),
             array('flagged', 'question'),
             array('functiondisabledbysecuremode', 'quiz'),
             array('startattempt', 'quiz'),
             array('timesup', 'quiz'),
+            array('changesmadereallygoaway', 'moodle'),
         ),
     );
 }
index 89fa16a..ce0d359 100644 (file)
@@ -27,6 +27,7 @@ M.mod_quiz = M.mod_quiz || {};
 M.mod_quiz.init_attempt_form = function(Y) {
     M.core_question_engine.init_form(Y, '#responseform');
     Y.on('submit', M.mod_quiz.timer.stop, '#responseform');
+    M.core_formchangechecker.init({formid: 'responseform'});
 };
 
 M.mod_quiz.init_review_form = function(Y) {
index c19afaa..eacc703 100644 (file)
@@ -52,6 +52,8 @@ $attemptobj->check_review_capability();
 
 // Create an object to manage all the other (non-roles) access rules.
 $accessmanager = $attemptobj->get_access_manager(time());
+$accessmanager->setup_attempt_page($PAGE);
+
 $options = $attemptobj->get_display_options(true);
 
 // Check permissions.
@@ -103,7 +105,6 @@ if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
 $headtags = $attemptobj->get_html_head_contributions($page, $showall);
 $PAGE->set_title(format_string($attemptobj->get_quiz_name()));
 $PAGE->set_heading($attemptobj->get_course()->fullname);
-$accessmanager->setup_attempt_page($PAGE);
 
 // Summary table start. ============================================================================
 
index d07442b..3ab0f5a 100644 (file)
@@ -11,6 +11,8 @@
 
 #page-mod-quiz-attempt .submitbtns,
 #page-mod-quiz-review .submitbtns {clear: left; text-align: left; padding-top: 1.5em;}
+#page-mod-quiz-attempt.dir-rtl .submitbtns,
+#page-mod-quiz-review.dir-rtl .submitbtns {text-align: right;}
 
 body.jsenabled .questionflagcheckbox {display: none;}
 
@@ -27,6 +29,7 @@ body.jsenabled .questionflagcheckbox {display: none;}
 .path-mod-quiz #user-picture img {width: auto;height: auto;float: left;}
 
 .path-mod-quiz .qnbutton {display: block; position: relative; float: left; width: 1.5em; height: 1.5em; overflow: hidden; margin: 0.3em 0.3em 0.3em 0; padding: 0; border: 1px solid #bbb; background: #ddd; text-align: center; vertical-align: middle;line-height: 1.5em !important; font-weight: bold; text-decoration: none;}
+.path-mod-quiz.dir-rtl  .qnbutton {float: right;}
 
 .path-mod-quiz .qnbutton .trafficlight,
 .path-mod-quiz .qnbutton .thispageholder {display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0;}
@@ -135,6 +138,7 @@ table.quizattemptsummary .noreviewmessage {color: gray;}
 table.quizreviewsummary {width: 100%;}
 table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;}
 table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;}
+.dir-rtl table.quizreviewsummary td.cell {text-align: right;}
 
 /** Mod quiz make comment or override grade popup. **/
 #page-mod-quiz-comment .mform {width: 100%;}
index 303638f..822e307 100644 (file)
@@ -52,8 +52,9 @@ if ($attemptobj->is_preview_user()) {
 
 // Check access.
 $accessmanager = $attemptobj->get_access_manager(time());
-$messages = $accessmanager->prevent_access();
+$accessmanager->setup_attempt_page($PAGE);
 $output = $PAGE->get_renderer('mod_quiz');
+$messages = $accessmanager->prevent_access();
 if (!$attemptobj->is_preview_user() && $messages) {
     print_error('attempterror', 'quiz', $attemptobj->view_url(),
             $output->access_messages($messages));
@@ -89,7 +90,6 @@ $PAGE->blocks->add_fake_block($navbc, reset($regions));
 $PAGE->navbar->add(get_string('summaryofattempt', 'quiz'));
 $PAGE->set_title(format_string($attemptobj->get_quiz_name()));
 $PAGE->set_heading($attemptobj->get_course()->fullname);
-$accessmanager->setup_attempt_page($PAGE);
 
 // Display the page.
 echo $output->summary_page($attemptobj, $displayoptions);
index b500784..cac3069 100644 (file)
@@ -512,8 +512,8 @@ function resource_dndupload_handle($uploadinfo) {
     $data->popupwidth = $config->popupwidth;
     $data->printheading = $config->printheading;
     $data->printintro = $config->printintro;
-    $data->showsize = $config->showsize;
-    $data->showtype = $config->showtype;
+    $data->showsize = (isset($config->showsize)) ? $config->showsize : 0;
+    $data->showtype = (isset($config->showtype)) ? $config->showtype : 0;
     $data->filterfiles = $config->filterfiles;
 
     return resource_add_instance($data, null);
index adf9363..e04a638 100644 (file)
@@ -86,24 +86,30 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
 
         $commenteditor = html_writer::tag('div', html_writer::tag('textarea', s($commenttext),
                 array('id' => $id, 'name' => $inputname, 'rows' => 10, 'cols' => 60)));
+        $commenteditor .= html_writer::end_tag('div');
 
-        $commenteditor .= html_writer::start_tag('div');
-        if (count($formats == 1)) {
+        $editorformat = '';
+        if (count($formats) == 1) {
             reset($formats);
-            $commenteditor .= html_writer::empty_tag('input', array('type' => 'hidden',
+            $editorformat .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => $inputname . 'format', 'value' => key($formats)));
-
         } else {
-            $commenteditor .= html_writer::select(
-                    $formats, $inputname . 'format', $commentformat, '');
+            $editorformat = html_writer::start_tag('div', array('class' => 'fitem'));
+            $editorformat .= html_writer::start_tag('div', array('class' => 'fitemtitle'));
+            $editorformat .= html_writer::tag('label', get_string('format'), array('for'=>'menu'.$inputname.'format'));
+            $editorformat .= html_writer::end_tag('div');
+            $editorformat .= html_writer::start_tag('div', array('class' => 'felement fhtmleditor'));
+            $editorformat .= html_writer::select($formats, $inputname.'format', $commentformat, '');
+            $editorformat .= html_writer::end_tag('div');
+            $editorformat .= html_writer::end_tag('div');
         }
-        $commenteditor .= html_writer::end_tag('div');
 
         $comment = html_writer::tag('div', html_writer::tag('div',
                 html_writer::tag('label', get_string('comment', 'question'),
                 array('for' => $id)), array('class' => 'fitemtitle')) .
                 html_writer::tag('div', $commenteditor, array('class' => 'felement fhtmleditor')),
                 array('class' => 'fitem'));
+        $comment .= $editorformat;
 
         $mark = '';
         if ($qa->get_max_mark()) {
@@ -117,6 +123,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
                 'type' => 'text',
                 'size' => $fieldsize,
                 'name' => $markfield,
+                'id'=> $markfield
             );
             if (!is_null($currentmark)) {
                 $attributes['value'] = $qa->format_fraction_as_mark(
index 6fbccbf..86d58f3 100644 (file)
@@ -117,6 +117,7 @@ class question_engine_data_mapper {
         $record->responsesummary = $qa->get_response_summary();
         $record->timemodified = time();
         $record->id = $this->db->insert_record('question_attempts', $record);
+        $qa->set_database_id($record->id);
 
         foreach ($qa->get_step_iterator() as $seq => $step) {
             $this->insert_question_attempt_step($step, $record->id, $seq, $context);
index 4cd813a..20a34da 100644 (file)
@@ -386,7 +386,7 @@ class question_attempt_step {
         $record = $currentrec;
         $data = array();
         while ($currentrec && $currentrec->attemptstepid == $attemptstepid) {
-            if ($currentrec->name) {
+            if (!is_null($currentrec->name)) {
                 $data[$currentrec->name] = $currentrec->value;
             }
             $records->next();
index 0b70c45..7c929d6 100644 (file)
@@ -95,12 +95,14 @@ class qformat_xhtml extends qformat_default {
             break;
         case SHORTANSWER:
             $expout .= "<ul class=\"shortanswer\">\n";
-            $expout .= "  <li><input name=\"quest_$id\" type=\"text\" /></li>\n";
+            $expout .= "  <li>" . html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
+            $expout .= "    <input id=\"quest_$id\" name=\"quest_$id\" type=\"text\" /></li>\n";
             $expout .= "</ul>\n";
             break;
         case NUMERICAL:
             $expout .= "<ul class=\"numerical\">\n";
-            $expout .= "  <li><input name=\"quest_$id\" type=\"text\" /></li>\n";
+            $expout .= "  <li>" . html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
+            $expout .= "    <input id=\"quest_$id\" name=\"quest_$id\" type=\"text\" /></li>\n";
             $expout .= "</ul>\n";
             break;
         case MATCH:
@@ -113,18 +115,22 @@ class qformat_xhtml extends qformat_default {
             }
             shuffle( $ans_list ); // random display order
 
-            // build drop down for answers
-            $dropdown = "<select name=\"quest_$id\">\n";
+            // Build select options.
+            $selectoptions = '';
             foreach($ans_list as $ans) {
-                $dropdown .= "<option value=\"" . s($ans) . "\">" . s($ans) . "</option>\n";
+                $selectoptions .= "<option value=\"" . s($ans) . "\">" . s($ans) . "</option>\n";
             }
-            $dropdown .= "</select>\n";
 
-            // finally display
+            // display
+            $option = 0;
             foreach($question->options->subquestions as $subquestion) {
-              $quest_text = $this->repchar( $subquestion->questiontext );
-              $expout .= "  <li>$quest_text</li>\n";
-              $expout .= $dropdown;
+                // build drop down for answers
+                $quest_text = $this->repchar( $subquestion->questiontext );
+                $dropdown = html_writer::label(get_string('answer', 'qtype_match', $option+1), 'quest_'.$id.'_'.$option, false, array('class' => 'accesshide'));
+                $dropdown .= "<select id=\"quest_{$id}_{$option}\" name=\"quest_{$id}_{$option}\">\n".$selectoptions."</select>\n";
+                $expout .= "  <li>$quest_text</li>\n";
+                $expout .= $dropdown;
+                $option++;
             }
             $expout .= "</ul>\n";
             break;
index 36e2d1d..a27d9be 100644 (file)
@@ -49,6 +49,7 @@ $string['datasetrole']= ' The wild cards <strong>{x..}</strong> will be substitu
 $string['decimals'] = 'with {$a}';
 $string['deleteitem'] = 'Delete item';
 $string['deletelastitem'] = 'Delete last item';
+$string['distributionoption'] = 'Select distribution option';
 $string['editdatasets'] = 'Edit the wildcards datasets';
 $string['editdatasets_help'] = 'Wildcard values may be created by entering a number in each wild card field then clicking the add button. To automatically generate 10 or more values, select the number of values required before clicking the add button. A uniform distribution means any value between the limits is equally likely to be generated; a loguniform distribution means that values towards the lower limit are more likely.';
 $string['editdatasets_link'] = 'question/type/calculated';
@@ -79,6 +80,7 @@ $string['keptlocal1'] = 'will use the same existing private dataset as before';
 $string['keptlocal2'] = 'a file from the same question private set of files as before';
 $string['keptlocal3'] = 'a link from the same question private set of links as before';
 $string['lastitem(s)'] = 'last items(s)';
+$string['lengthoption'] = 'Select length option';
 $string['loguniform'] = 'Loguniform';
 $string['loguniformbit'] = 'digits, from a loguniform distribution';
 $string['makecopynextpage'] = 'Next page (new question)';
index a4655a4..c0ab753 100644 (file)
@@ -751,11 +751,13 @@ class qtype_calculated extends question_type {
                     ? 'decimals'
                     : 'significantfigures'), 'qtype_calculated', $i);
             }
-            $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
+            $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'), 'menucalclength', false, array('class' => 'accesshide'));
+            $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
 
             $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
                     'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
-            $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null);
+            $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'), 'menucalcdistribution', false, array('class' => 'accesshide'));
+            $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
             return '<input type="submit" onclick="'
                 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
index ac2bac9..1905e2e 100644 (file)
@@ -219,12 +219,14 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
                 array('id' => $id, 'name' => $inputname, 'rows' => $lines, 'cols' => 60)));
 
         $output .= html_writer::start_tag('div');
-        if (count($formats == 1)) {
+        if (count($formats) == 1) {
             reset($formats);
             $output .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => $inputname . 'format', 'value' => key($formats)));
 
         } else {
+            $output .= html_writer::label(get_string('format'), 'menu' . $inputname . 'format', false);
+            $output .= ' ';
             $output .= html_writer::select($formats, $inputname . 'format', $responseformat, '');
         }
         $output .= html_writer::end_tag('div');
index a096607..5dd3c4a 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['addmoreqblanks'] = '{no} More Sets of Blanks';
+$string['answer'] = 'Answer {$a}';
 $string['availablechoices'] = 'Available choices';
 $string['correctansweris'] = 'The correct answer is: {$a}';
 $string['filloutthreeqsandtwoas'] = 'You must provide at least two questions and three answers. You can provide extra wrong answers by giving an answer with a blank question. Entries where both the question and the answer are blank will be ignored.';
index 8a3372c..2cadb7e 100644 (file)
@@ -53,6 +53,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
         $result .= html_writer::start_tag('tbody');
 
         $parity = 0;
+        $i = 1;
         foreach ($stemorder as $key => $stemid) {
 
             $result .= html_writer::start_tag('tr', array('class' => 'r' . $parity));
@@ -80,12 +81,14 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
             }
 
             $result .= html_writer::tag('td',
+                    html_writer::label(get_string('answer', 'qtype_match', $i), 'menu' . $qa->get_qt_field_name('sub' . $key), false, array('class' => 'accesshide')) .
                     html_writer::select($choices, $qa->get_qt_field_name('sub' . $key), $selected,
                             array('0' => 'choose'), array('disabled' => $options->readonly)) .
                     ' ' . $feedbackimage, array('class' => $classes));
 
             $result .= html_writer::end_tag('tr');
             $parity = 1 - $parity;
+            $i++;
         }
         $result .= html_writer::end_tag('tbody');
         $result .= html_writer::end_tag('table');
index 547a370..0134eb4 100644 (file)
@@ -177,13 +177,18 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
         if ($subq->qtype->name() == 'shortanswer') {
             $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
         } else if ($subq->qtype->name() == 'numerical') {
-            $matchinganswer = $subq->get_matching_answer($response, 1);
+            list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
+            $matchinganswer = $subq->get_matching_answer($value, 1);
         } else {
             $matchinganswer = $subq->get_matching_answer($response);
         }
 
         if (!$matchinganswer) {
-            $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+            if (is_null($response) || $response === '') {
+                $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+            } else {
+                $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
+            }
         }
 
         // Work out a good input field size.
@@ -223,11 +228,11 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
                 s($correctanswer->answer), $options);
 
         $output = '';
-        $output .= html_writer::start_tag('label', array('class' => 'subq'));
+        $output .= html_writer::tag('label', get_string('answer'),
+                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= html_writer::empty_tag('input', $inputattributes);
         $output .= $feedbackimg;
         $output .= $feedbackpopup;
-        $output .= html_writer::end_tag('label');
 
         return $output;
     }
@@ -274,13 +279,15 @@ class qtype_multianswer_multichoice_inline_renderer
             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
         }
-
         $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
                 $response, array('' => ''), $inputattributes);
 
         $order = $subq->get_order($qa);
         $correctresponses = $subq->get_correct_response();
         $rightanswer = $subq->answers[$order[reset($correctresponses)]];
+        if (!$matchinganswer) {
+            $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+        }
         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
@@ -288,11 +295,11 @@ class qtype_multianswer_multichoice_inline_renderer
                         $qa, 'question', 'answer', $rightanswer->id), $options);
 
         $output = '';
-        $output .= html_writer::start_tag('label', array('class' => 'subq'));
+        $output .= html_writer::tag('label', get_string('answer'),
+                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= $select;
         $output .= $feedbackimg;
         $output .= $feedbackpopup;
-        $output .= html_writer::end_tag('label');
 
         return $output;
     }
@@ -366,16 +373,30 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_
 
         $result .= $this->all_choices_wrapper_end();
 
+        $feedback = array();
         if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
                 $subq->maxmark > 0) {
             $a = new stdClass();
             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
             $a->max =  format_float($subq->maxmark, $options->markdp);
 
-            $result .= html_writer::tag('div', get_string('markoutofmax', 'question', $a),
-                    array('class' => 'outcome'));
+            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
+        }
+
+        if ($options->rightanswer) {
+            foreach ($subq->answers as $ans) {
+                if (question_state::graded_state_for_fraction($ans->fraction) ==
+                        question_state::$gradedright) {
+                    $feedback[] = get_string('correctansweris', 'qtype_multichoice',
+                            $subq->format_text($ans->answer, $ans->answerformat,
+                                    $qa, 'question', 'answer', $ansid));
+                    break;
+                }
+            }
         }
 
+        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
+
         return $result;
     }
 
index 75a3d7d..3a77001 100644 (file)
@@ -38,7 +38,7 @@ require_once($CFG->dirroot . '/question/type/multianswer/question.php');
  */
 class qtype_multianswer_test_helper extends question_test_helper {
     public function get_test_questions() {
-        return array('twosubq', 'fourmc');
+        return array('twosubq', 'fourmc', 'numericalzero');
     }
 
     /**
@@ -287,4 +287,48 @@ class qtype_multianswer_test_helper extends question_test_helper {
 
         return $q;
     }
+
+    /**
+     * Makes a multianswer question with one numerical subquestion, right answer 0.
+     * This is used for testing the MDL-35370 bug.
+     * @return qtype_multianswer_question
+     */
+    public function make_multianswer_question_numericalzero() {
+        question_bank::load_question_definition_classes('multianswer');
+        $q = new qtype_multianswer_question();
+        test_question_maker::initialise_a_question($q);
+        $q->name = 'Numerical zero';
+        $q->questiontext =
+                'Enter zero: {#1}.';
+        $q->generalfeedback = '';
+        $q->qtype = question_bank::get_qtype('multianswer');
+
+        $q->textfragments = array(
+            'Enter zero: ',
+            '.',
+        );
+        $q->places = array('1' => '1');
+
+        // Numerical subquestion.
+        question_bank::load_question_definition_classes('numerical');
+        $sub = new qtype_numerical_question();
+        test_question_maker::initialise_a_question($sub);
+        $sub->name = 'Numerical zero';
+        $sub->questiontext = '{1:NUMERICAL:=0:0}';
+        $sub->questiontextformat = FORMAT_HTML;
+        $sub->generalfeedback = '';
+        $sub->generalfeedbackformat = FORMAT_HTML;
+        $sub->answers = array(
+            13 => new qtype_numerical_answer(13, '0', 1.0, '', FORMAT_HTML, 0),
+        );
+        $sub->qtype = question_bank::get_qtype('numerical');
+        $sub->ap = new qtype_numerical_answer_processor(array());
+        $sub->maxmark = 1;
+
+        $q->subquestions = array(
+            1 => $sub,
+        );
+
+        return $q;
+    }
 }
index 19af3c7..9c9d291 100644 (file)
@@ -37,9 +37,15 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+    protected function get_contains_subq_status(question_state $state) {
+        return new question_pattern_expectation('~' .
+                preg_quote($state->default_string(true), '~') . '<br />~');
+    }
+
     public function test_deferred_feedback() {
 
-        // Create a gapselect question.
+        // Create a multianswer question.
         $q = test_question_maker::make_question('multianswer', 'fourmc');
         $this->start_attempt_at_question($q, 'deferredfeedback', 4);
 
@@ -86,4 +92,110 @@ class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 $this->get_contains_partcorrect_expectation(),
                 $this->get_does_not_contain_validation_error_expectation());
     }
+
+    public function test_deferred_feedback_numericalzero_not_answered() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                new question_pattern_expectation('~<input[^>]* class="incorrect" [^>]*/>~'),
+                $this->get_contains_subq_status(question_state::$gaveup),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_deferred_feedback_numericalzero_0_answer() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Save a the correct answer.
+        $this->process_submission(array('sub1_answer' => '0'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_correct_expectation(),
+                $this->get_contains_subq_status(question_state::$gradedright),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_deferred_feedback_numericalzero_0_wrong() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Save a the correct answer.
+        $this->process_submission(array('sub1_answer' => '42'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_subq_status(question_state::$gradedwrong),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
 }
index 75dc457..fb4784b 100644 (file)
@@ -26,6 +26,7 @@
 $string['acceptederror'] = 'Accepted error';
 $string['addmoreanswerblanks'] = 'Blanks for {no} More Answers';
 $string['addmoreunitblanks'] = 'Blanks for {no} More Units';
+$string['answercolon'] = 'Answer:';
 $string['answermustbenumberorstar'] = 'The answer must be a number, for example -1.234 or 3e8, or \'*\'.';
 $string['answerno'] = 'Answer {$a}';
 $string['decfractionofquestiongrade'] = 'as a fraction (0-1) of the question grade';
index 110cfdc..0e48155 100644 (file)
@@ -179,6 +179,10 @@ class qtype_numerical_question extends question_graded_automatically {
      * @return question_answer the matching answer.
      */
     public function get_matching_answer($value, $multiplier) {
+        if (is_null($value) || $value === '') {
+            return null;
+        }
+
         if (!is_null($multiplier)) {
             $scaledvalue = $value * $multiplier;
         } else {
@@ -193,6 +197,7 @@ class qtype_numerical_question extends question_graded_automatically {
                 return $answer;
             }
         }
+
         return null;
     }
 
@@ -273,18 +278,17 @@ class qtype_numerical_question extends question_graded_automatically {
     public function check_file_access($qa, $options, $component, $filearea, $args,
             $forcedownload) {
         if ($component == 'question' && $filearea == 'answerfeedback') {
-            $question = $qa->get_question();
             $currentanswer = $qa->get_last_qt_var('answer');
             if ($this->has_separate_unit_field()) {
                 $selectedunit = $qa->get_last_qt_var('unit');
             } else {
                 $selectedunit = null;
             }
-            list($value, $unit, $multiplier) = $question->ap->apply_units(
+            list($value, $unit, $multiplier) = $this->ap->apply_units(
                     $currentanswer, $selectedunit);
-            $answer = $question->get_matching_answer($value, $multiplier);
+            $answer = $this->get_matching_answer($value, $multiplier);
             $answerid = reset($args); // itemid is answer id.
-            return $options->feedback && $answerid == $answer->id;
+            return $options->feedback && $answer && $answerid == $answer->id;
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index 7689a2c..d1b7c0a 100644 (file)
@@ -98,7 +98,9 @@ class qtype_numerical_renderer extends qtype_renderer {
                         array('class' => 'unitchoices'));
 
             } else if ($question->unitdisplay == qtype_numerical::UNITSELECT) {
-                $unitchoice = html_writer::select($question->ap->get_unit_options(),
+                $unitchoice = html_writer::label(get_string('selectunit', 'qtype_numerical'),
+                        'menu' . $qa->get_qt_field_name('unit'), false, array('class' => 'accesshide'));
+                $unitchoice .= html_writer::select($question->ap->get_unit_options(),
                         $qa->get_qt_field_name('unit'), $selectedunit, array(''=>'choosedots'),
                         array('disabled' => $options->readonly));
             }
@@ -111,7 +113,10 @@ class qtype_numerical_renderer extends qtype_renderer {
         }
 
         if ($placeholder) {
-            $questiontext = substr_replace($questiontext, $input,
+            $inputinplace = html_writer::tag('label', get_string('answer'),
+                    array('for' => $inputattributes['id'], 'class' => 'accesshide'));
+            $inputinplace .= $input;
+            $questiontext = substr_replace($questiontext, $inputinplace,
                     strpos($questiontext, $placeholder), strlen($placeholder));
         }
 
@@ -119,8 +124,8 @@ class qtype_numerical_renderer extends qtype_renderer {
 
         if (!$placeholder) {
             $result .= html_writer::start_tag('div', array('class' => 'ablock'));
-            $result .= get_string('answer', 'qtype_shortanswer',
-                    html_writer::tag('div', $input, array('class' => 'answer')));
+            $result .= html_writer::tag('label', get_string('answercolon', 'qtype_numerical'), array('for' => $inputattributes['id']));
+            $result .= html_writer::tag('span', $input, array('class' => 'answer'));
             $result .= html_writer::end_tag('div');
         }
 
index d4ba6f8..84fc7d0 100644 (file)
@@ -181,6 +181,11 @@ class qtype_numerical_question_test extends advanced_testcase {
         $this->assertEquals('3.1', $num->summarise_response(array('answer' => '3.1')));
     }
 
+    public function test_summarise_response_zero() {
+        $num = test_question_maker::make_question('numerical');
+        $this->assertEquals('0', $num->summarise_response(array('answer' => '0')));
+    }
+
     public function test_summarise_response_unit() {
         $num = test_question_maker::make_question('numerical', 'unit');
         $this->assertEquals('3.1', $num->summarise_response(array('answer' => '3.1')));
index a875c8c..72f0f4a 100644 (file)
@@ -78,6 +78,10 @@ class qtype_shortanswer_question extends question_graded_by_strategy
     }
 
     public function compare_response_with_answer(array $response, question_answer $answer) {
+        if (!array_key_exists('answer', $response) || is_null($response['answer'])) {
+            return false;
+        }
+
         return self::compare_string_with_wildcard(
                 $response['answer'], $answer->answer, !$this->usecase);
     }
@@ -129,7 +133,7 @@ class qtype_shortanswer_question extends question_graded_by_strategy
             $currentanswer = $qa->get_last_qt_var('answer');
             $answer = $qa->get_question()->get_matching_answer(array('answer' => $currentanswer));
             $answerid = reset($args); // itemid is answer id.
-            return $options->feedback && $answerid == $answer->id;
+            return $options->feedback && $answer && $answerid == $answer->id;
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index 31212ce..2cf77d7 100644 (file)
@@ -117,7 +117,7 @@ class qtype_shortanswer extends question_type {
 
         $this->save_hints($question);
 
-        // Perform sanity checks on fractional grades
+        // Perform sanity checks on fractional grades.
         if ($maxfraction != 1) {
             $result->noticeyesno = get_string('fractionsnomax', 'question', $maxfraction * 100);
             return $result;
index e7879bf..cfdca17 100644 (file)
@@ -71,11 +71,13 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             $placeholder = $matches[0];
             $inputattributes['size'] = round(strlen($placeholder) * 1.1);
         }
-
         $input = html_writer::empty_tag('input', $inputattributes) . $feedbackimg;
 
         if ($placeholder) {
-            $questiontext = substr_replace($questiontext, $input,
+            $inputinplace = html_writer::tag('label', get_string('answer'),
+                    array('for' => $inputattributes['id'], 'class' => 'accesshide'));
+            $inputinplace .= $input;
+            $questiontext = substr_replace($questiontext, $inputinplace,
                     strpos($questiontext, $placeholder), strlen($placeholder));
         }
 
@@ -83,8 +85,9 @@ class qtype_shortanswer_renderer extends qtype_renderer {
 
         if (!$placeholder) {
             $result .= html_writer::start_tag('div', array('class' => 'ablock'));
-            $result .= get_string('answer', 'qtype_shortanswer',
-                    html_writer::tag('div', $input, array('class' => 'answer')));
+            $result .= html_writer::tag('label', get_string('answer', 'qtype_shortanswer',
+                    html_writer::tag('span', $input, array('class' => 'answer'))),
+                    array('for' => $inputattributes['id']));
             $result .= html_writer::end_tag('div');
         }
 
index 0da89c7..2f8bc60 100644 (file)
@@ -324,15 +324,13 @@ function user_get_user_details($user, $course = null, array $userfields = array(
         $userdetails['phone2'] = $user->phone2;
     }
 
-    if (isset($user->description) && (!isset($hiddenfields['description']) or $isadmin)) {
-        if (!$cannotviewdescription) {
-
-            if (in_array('description', $userfields)) {
-                // Always return the descriptionformat if description is requested.
-                list($userdetails['description'], $userdetails['descriptionformat']) =
-                        external_format_text($user->description, $user->descriptionformat,
-                                $usercontext->id, 'user', 'profile', null);
-            }
+    if (isset($user->description) &&
+        ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
+        if (in_array('description', $userfields)) {
+            // Always return the descriptionformat if description is requested.
+            list($userdetails['description'], $userdetails['descriptionformat']) =
+                    external_format_text($user->description, $user->descriptionformat,
+                            $usercontext->id, 'user', 'profile', null);
         }
     }
 
index 68b743d..de2f500 100644 (file)
@@ -43,6 +43,8 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
         listbox : Y.one('#'+name),
         /** Used to hold the timeout id of the timeout that waits before doing a search. */
         timeoutid : null,
+        /** Stores any in-progress remote requests. */
+        iotransactions : {},
         /** The last string that we searched for, so we can avoid unnecessary repeat searches. */
         lastsearch : lastsearch,
         /** Whether any options where selected last time we checked. Used by
@@ -140,7 +142,12 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
                 return;
             }
 
-            Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
+            // Try to cancel existing transactions.
+            Y.Object.each(this.iotransactions, function(trans) {
+                trans.abort();
+            });
+
+            var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
                 method: 'POST',
                 data: 'selectorid='+hash+'&sesskey='+M.cfg.sesskey+'&search='+value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
                 on: {
@@ -149,6 +156,7 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
                 },
                 context:this
             });
+            this.iotransactions[iotrans.id] = iotrans;
 
             this.lastsearch = value;
             this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center');
@@ -160,17 +168,27 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
          */
         handle_response : function(requestid, response) {
             try {
+                delete this.iotransactions[requestid];
+                if (!Y.Object.isEmpty(this.iotransactions)) {
+                    // More searches pending. Wait until they are all done.
+                    return;
+                }
                 this.listbox.setStyle('background','');
                 var data = Y.JSON.parse(response.responseText);
                 this.output_options(data);
             } catch (e) {
-                this.handle_failure();
+                this.handle_failure(requestid);
             }
         },
         /**
          * Handles what happens when the ajax request fails.
          */
-        handle_failure : function() {
+        handle_failure : function(requestid) {
+            delete this.iotransactions[requestid];
+            if (!Y.Object.isEmpty(this.iotransactions)) {
+                // More searches pending. Wait until they are all done.
+                return;
+            }
             this.listbox.setStyle('background','');
             this.searchfield.addClass('error');
 
diff --git a/user/tests/externallib_test.php b/user/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..53670e9
--- /dev/null
@@ -0,0 +1,347 @@
+<?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/>.
+
+/**
+ * User external PHPunit tests
+ *
+ * @package    core_user
+ * @category   external
+ * @copyright  2012 Jerome Mouneyrac
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.4
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/user/externallib.php');
+
+class core_user_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test get_course_user_profiles
+     */
+    public function test_get_course_user_profiles() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+        $user1 = array(
+            'username' => 'usernametest1',
+            'idnumber' => 'idnumbertest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'email' => 'usertest1@email.com',
+            'address' => '2 Test Street Perth 6000 WA',
+            'phone1' => '01010101010',
+            'phone2' => '02020203',
+            'icq' => 'testuser1',
+            'skype' => 'testuser1',
+            'yahoo' => 'testuser1',
+            'aim' => 'testuser1',
+            'msn' => 'testuser1',
+            'department' => 'Department of user 1',
+            'institution' => 'Institution of user 1',
+            'description' => 'This is a description for user 1',
+            'descriptionformat' => FORMAT_MOODLE,
+            'city' => 'Perth',
+            'url' => 'http://moodle.org',
+            'country' => 'au'
+            );
+        $user1 = self::getDataGenerator()->create_user($user1);
+        if (!empty($CFG->usetags)) {
+            require_once($CFG->dirroot . '/user/editlib.php');
+            require_once($CFG->dirroot . '/tag/lib.php');
+            $user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking');
+            useredit_update_interests($user1, $user1->interests);
+        }
+        $user2 = self::getDataGenerator()->create_user();
+
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/user:viewdetails', $context->id);
+
+        // Enrol the users in the course.
+        // We use the manual plugin.
+        $enrol = enrol_get_plugin('manual');
+        $enrolinstances = enrol_get_instances($course->id, true);
+        foreach ($enrolinstances as $courseenrolinstance) {
+            if ($courseenrolinstance->enrol == "manual") {
+                $instance = $courseenrolinstance;
+                break;
+            }
+        }
+        $enrol->enrol_user($instance, $user1->id, $roleid);
+        $enrol->enrol_user($instance, $user2->id, $roleid);
+        $enrol->enrol_user($instance, $USER->id, $roleid);
+
+        // Call the external function.
+        $enrolledusers = core_user_external::get_course_user_profiles(array(
+                    array('userid' => $USER->id, 'courseid' => $course->id),
+                    array('userid' => $user1->id, 'courseid' => $course->id),
+                    array('userid' => $user2->id, 'courseid' => $course->id)));
+
+        // Check we retrieve the good total number of enrolled users + no error on capability.
+        $this->assertEquals(3, count($enrolledusers));
+
+        // Do the same call as admin to receive all possible fields.
+        $this->setAdminUser();
+        $USER->email = "admin@fakeemail.com";
+
+        // Call the external function.
+        $enrolledusers = core_user_external::get_course_user_profiles(array(
+                    array('userid' => $USER->id, 'courseid' => $course->id),
+                    array('userid' => $user1->id, 'courseid' => $course->id),
+                    array('userid' => $user2->id, 'courseid' => $course->id)));
+
+        foreach($enrolledusers as $enrolleduser) {
+            if ($enrolleduser['username'] == $user1->username) {
+                $this->assertEquals($user1->idnumber, $enrolleduser['idnumber']);
+                $this->assertEquals($user1->firstname, $enrolleduser['firstname']);
+                $this->assertEquals($user1->lastname, $enrolleduser['lastname']);
+                $this->assertEquals($user1->email, $enrolleduser['email']);
+                $this->assertEquals($user1->address, $enrolleduser['address']);
+                $this->assertEquals($user1->phone1, $enrolleduser['phone1']);
+                $this->assertEquals($user1->phone2, $enrolleduser['phone2']);
+                $this->assertEquals($user1->icq, $enrolleduser['icq']);
+                $this->assertEquals($user1->skype, $enrolleduser['skype']);
+                $this->assertEquals($user1->yahoo, $enrolleduser['yahoo']);
+                $this->assertEquals($user1->aim, $enrolleduser['aim']);
+                $this->assertEquals($user1->msn, $enrolleduser['msn']);
+                $this->assertEquals($user1->department, $enrolleduser['department']);
+                $this->assertEquals($user1->institution, $enrolleduser['institution']);
+                $this->assertEquals($user1->description, $enrolleduser['description']);
+                $this->assertEquals(FORMAT_HTML, $enrolleduser['descriptionformat']);
+                $this->assertEquals($user1->city, $enrolleduser['city']);
+                $this->assertEquals($user1->country, $enrolleduser['country']);
+                $this->assertEquals($user1->url, $enrolleduser['url']);
+                if (!empty($CFG->usetags)) {
+                    $this->assertEquals(implode(', ', $user1->interests), $enrolleduser['interests']);
+                }
+            }
+        }
+    }
+
+    /**
+     * Test create_users
+     */
+    public function test_create_users() {
+         global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = array(
+            'username' => 'usernametest1',
+            'password' => 'Moodle2012!',
+            'idnumber' => 'idnumbertest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'email' => 'usertest1@email.com',
+            'description' => 'This is a description for user 1',
+            'city' => 'Perth',
+            'country' => 'au'
+            );
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/user:create', $context->id);
+
+        // Call the external function.
+        $createdusers = core_user_external::create_users(array($user1));
+
+        // Check we retrieve the good total number of created users + no error on capability.
+        $this->assertEquals(1, count($createdusers));
+
+        foreach($createdusers as $createduser) {
+            $dbuser = $DB->get_record('user', array('id' => $createduser['id']));
+            $this->assertEquals($dbuser->username, $user1['username']);
+            $this->assertEquals($dbuser->idnumber, $user1['idnumber']);
+            $this->assertEquals($dbuser->firstname, $user1['firstname']);
+            $this->assertEquals($dbuser->lastname, $user1['lastname']);
+            $this->assertEquals($dbuser->email, $user1['email']);
+            $this->assertEquals($dbuser->description, $user1['description']);
+            $this->assertEquals($dbuser->city, $user1['city']);
+            $this->assertEquals($dbuser->country, $user1['country']);
+        }
+
+        // Call without required capability
+        $this->unassignUserCapability('moodle/user:create', $context->id, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        $createdusers = core_user_external::create_users(array($user1));
+    }
+
+    /**
+     * Test delete_users
+     */
+    public function test_delete_users() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Check the users were correctly created.
+        $this->assertEquals(2, $DB->count_records_select('user', 'deleted = 0 AND (id = :userid1 OR id = :userid2)',
+                array('userid1' => $user1->id, 'userid2' => $user2->id)));
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/user:delete', $context->id);
+
+        // Call the external function.
+        core_user_external::delete_users(array($user1->id, $user2->id));
+
+        // Check we retrieve no users + no error on capability.
+        $this->assertEquals(0, $DB->count_records_select('user', 'deleted = 0 AND (id = :userid1 OR id = :userid2)',
+                array('userid1' => $user1->id, 'userid2' => $user2->id)));
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/user:delete', $context->id, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        core_user_external::delete_users(array($user1->id, $user2->id));
+    }
+
+    /**
+     * Test get_users_by_id
+     */
+    public function test_get_users_by_id() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $user1 = array(
+            'username' => 'usernametest1',
+            'idnumber' => 'idnumbertest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'email' => 'usertest1@email.com',
+            'address' => '2 Test Street Perth 6000 WA',
+            'phone1' => '01010101010',
+            'phone2' => '02020203',
+            'icq' => 'testuser1',
+            'skype' => 'testuser1',
+            'yahoo' => 'testuser1',
+            'aim' => 'testuser1',
+            'msn' => 'testuser1',
+            'department' => 'Department of user 1',
+            'institution' => 'Institution of user 1',
+            'description' => 'This is a description for user 1',
+            'descriptionformat' => FORMAT_MOODLE,
+            'city' => 'Perth',
+            'url' => 'http://moodle.org',
+            'country' => 'au'
+            );
+        $user1 = self::getDataGenerator()->create_user($user1);
+        if (!empty($CFG->usetags)) {
+            require_once($CFG->dirroot . '/user/editlib.php');
+            require_once($CFG->dirroot . '/tag/lib.php');
+            $user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking');
+            useredit_update_interests($user1, $user1->interests);
+        }
+        $user2 = self::getDataGenerator()->create_user();
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/user:viewdetails', $context->id);
+
+        // Call the external function.
+        $returnedusers = core_user_external::get_users_by_id(array(
+                    $USER->id, $user1->id, $user2->id));
+
+        // Check we retrieve the good total number of enrolled users + no error on capability.
+        $this->assertEquals(3, count($returnedusers));
+
+        // Do the same call as admin to receive all possible fields.
+        $this->setAdminUser();
+        $USER->email = "admin@fakeemail.com";
+
+        // Call the external function.
+        $returnedusers = core_user_external::get_users_by_id(array(
+                    $USER->id, $user1->id, $user2->id));
+
+        foreach($returnedusers as $enrolleduser) {
+            if ($enrolleduser['username'] == $user1->username) {
+                $this->assertEquals($user1->idnumber, $enrolleduser['idnumber']);
+                $this->assertEquals($user1->firstname, $enrolleduser['firstname']);
+                $this->assertEquals($user1->lastname, $enrolleduser['lastname']);
+                $this->assertEquals($user1->email, $enrolleduser['email']);
+                $this->assertEquals($user1->address, $enrolleduser['address']);
+                $this->assertEquals($user1->phone1, $enrolleduser['phone1']);
+                $this->assertEquals($user1->phone2, $enrolleduser['phone2']);
+                $this->assertEquals($user1->icq, $enrolleduser['icq']);
+                $this->assertEquals($user1->skype, $enrolleduser['skype']);
+                $this->assertEquals($user1->yahoo, $enrolleduser['yahoo']);
+                $this->assertEquals($user1->aim, $enrolleduser['aim']);
+                $this->assertEquals($user1->msn, $enrolleduser['msn']);
+                $this->assertEquals($user1->department, $enrolleduser['department']);
+                $this->assertEquals($user1->institution, $enrolleduser['institution']);
+                $this->assertEquals($user1->description, $enrolleduser['description']);
+                $this->assertEquals(FORMAT_HTML, $enrolleduser['descriptionformat']);
+                $this->assertEquals($user1->city, $enrolleduser['city']);
+                $this->assertEquals($user1->country, $enrolleduser['country']);
+                $this->assertEquals($user1->url, $enrolleduser['url']);
+                if (!empty($CFG->usetags)) {
+                    $this->assertEquals(implode(', ', $user1->interests), $enrolleduser['interests']);
+                }
+            }
+        }
+    }
+
+    /**
+     * Test update_users
+     */
+    public function test_update_users() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+
+        $user1 = array(
+            'id' => $user1->id,
+            'username' => 'usernametest1',
+            'password' => 'Moodle2012!',
+            'idnumber' => 'idnumbertest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'email' => 'usertest1@email.com',
+            'description' => 'This is a description for user 1',
+            'city' => 'Perth',
+            'country' => 'au'
+            );
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/user:update', $context->id);
+
+        // Call the external function.
+        core_user_external::update_users(array($user1));
+
+        $dbuser = $DB->get_record('user', array('id' => $user1['id']));
+        $this->assertEquals($dbuser->username, $user1['username']);
+        $this->assertEquals($dbuser->idnumber, $user1['idnumber']);
+        $this->assertEquals($dbuser->firstname, $user1['firstname']);
+        $this->assertEquals($dbuser->lastname, $user1['lastname']);
+        $this->assertEquals($dbuser->email, $user1['email']);
+        $this->assertEquals($dbuser->description, $user1['description']);
+        $this->assertEquals($dbuser->city, $user1['city']);
+        $this->assertEquals($dbuser->country, $user1['country']);
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/user:update', $context->id, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        core_user_external::update_users(array($user1));
+    }
+}
\ No newline at end of file