Merge branch 'MDL-35754-23' of git://github.com/FMCorz/moodle into MOODLE_23_STABLE
authorDan Poltawski <dan@moodle.com>
Wed, 3 Oct 2012 02:26:22 +0000 (10:26 +0800)
committerDan Poltawski <dan@moodle.com>
Wed, 3 Oct 2012 02:26:22 +0000 (10:26 +0800)
56 files changed:
admin/settings/users.php
course/tests/externallib_test.php
enrol/database/lib.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/glossary/lib.php
mod/glossary/locallib.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/quiz/tests/locallib_test.php
mod/resource/lib.php
mod/scorm/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
theme/anomaly/config.php
theme/anomaly/pix/menu/nav-arrow-left.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrow-right.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrowover-left.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrowover-right.jpg [new file with mode: 0644]
theme/anomaly/renderers.php
theme/anomaly/style/general.css
theme/anomaly/style/menu.css [new file with mode: 0644]
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 1755a89..286bb00 100644 (file)
@@ -481,9 +481,8 @@ class enrol_database_plugin extends enrol_plugin {
                 }
                 $rs->Close();
             } else {
-                mtrace('Error while communicating with external enrolment database');
-                $extdb->Close();
-                return;
+                mtrace("  error: skipping course '$course->mapping' - could not match with external database");
+                continue;
             }
             unset($user_mapping);
 
@@ -630,7 +629,7 @@ class enrol_database_plugin extends enrol_plugin {
         if ($idnumber) {
             $sqlfields[] = $idnumber;
         }
-        $sql = $this->db_get_sql($table, array(), $sqlfields);
+        $sql = $this->db_get_sql($table, array(), $sqlfields, true);
         $createcourses = array();
         if ($rs = $extdb->Execute($sql)) {
             if (!$rs->EOF) {
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 2a307bd..35e7197 100644 (file)
@@ -1282,7 +1282,9 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h
             }
         }
         $fs = get_file_storage();
-        if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) {
+        if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)
+         || $files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'entry', $entry->id, "timemodified", false)) {
+
             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
         } else {
             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
index 2f0b448..c569b18 100644 (file)
@@ -266,6 +266,12 @@ class glossary_entry_portfolio_caller extends portfolio_module_caller_base {
             $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false),
             $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false)
         );
+
+        if (!empty($this->multifiles)) {
+            $this->add_format(PORTFOLIO_FORMAT_RICHHTML);
+        } else {
+            $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
+        }
     }
 
     /**
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..f55f590 100644 (file)
@@ -987,7 +987,7 @@ function quiz_get_flag_option($attempt, $context) {
  *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
  */
 function quiz_attempt_state($quiz, $attempt) {
-    if ($attempt->state != quiz_attempt::FINISHED) {
+    if ($attempt->state == quiz_attempt::IN_PROGRESS) {
         return mod_quiz_display_options::DURING;
     } else if (time() < $attempt->timefinish + 120) {
         return mod_quiz_display_options::IMMEDIATELY_AFTER;
@@ -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 d058b3c..177947c 100644 (file)
@@ -151,4 +151,92 @@ class mod_quiz_locallib_testcase extends basic_testcase {
         $this->assertEquals(1, quiz_get_slot_for_question($quiz, 1));
         $this->assertEquals(3, quiz_get_slot_for_question($quiz, 7));
     }
+
+    public function test_quiz_attempt_state_in_progress() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::IN_PROGRESS;
+        $attempt->timefinish = 0;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::DURING, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_recently_submitted() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 10;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::IMMEDIATELY_AFTER, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_never_closes() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_closes_later() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() + 3600;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_closed() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() - 3600;
+
+        $this->assertEquals(mod_quiz_display_options::AFTER_CLOSE, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_never_closes() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = 1000; // A very long time ago!
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_closes_later() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() + 3600;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_closed() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() - 3600;
+
+        $this->assertEquals(mod_quiz_display_options::AFTER_CLOSE, quiz_attempt_state($quiz, $attempt));
+    }
 }
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 c587b68..27b4722 100644 (file)
@@ -150,7 +150,7 @@ function scorm_add_instance($scorm, $mform=null) {
 
     scorm_parse($record, true);
 
-    scorm_grade_item_update($record, null, false);
+    scorm_grade_item_update($record);
 
     return $record->id;
 }
@@ -591,18 +591,21 @@ function scorm_get_user_grades($scorm, $userid=0) {
  * @param bool $nullifnone
  */
 function scorm_update_grades($scorm, $userid=0, $nullifnone=true) {
-    global $CFG, $DB;
+    global $CFG;
     require_once($CFG->libdir.'/gradelib.php');
+    require_once($CFG->libdir.'/completionlib.php');
 
     if ($grades = scorm_get_user_grades($scorm, $userid)) {
         scorm_grade_item_update($scorm, $grades);
-
+        //set complete
+        scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades);
     } else if ($userid and $nullifnone) {
         $grade = new stdClass();
         $grade->userid   = $userid;
         $grade->rawgrade = null;
         scorm_grade_item_update($scorm, $grade);
-
+        //set incomplete.
+        scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE);
     } else {
         scorm_grade_item_update($scorm);
     }
@@ -646,10 +649,9 @@ function scorm_upgrade_grades() {
  * @uses GRADE_TYPE_NONE
  * @param object $scorm object with extra cmidnumber
  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
- * @param boolean $updatecompletion  set whether to update completion stuff
  * @return object grade_item
  */
-function scorm_grade_item_update($scorm, $grades=null, $updatecompletion=true) {
+function scorm_grade_item_update($scorm, $grades=null) {
     global $CFG, $DB;
     require_once($CFG->dirroot.'/mod/scorm/locallib.php');
     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
@@ -680,19 +682,6 @@ function scorm_grade_item_update($scorm, $grades=null, $updatecompletion=true) {
         $grades = null;
     }
 
-    // Update activity completion if applicable
-    if ($updatecompletion) {
-        // Get course info
-        $course = new stdClass();
-        $course->id = $scorm->course;
-
-        $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id);
-        if (!empty($cm)) {
-            $completion = new completion_info($course);
-            $completion->update_state($cm, COMPLETION_COMPLETE);
-        }
-    }
-
     return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, $grades, $params);
 }
 
@@ -1329,3 +1318,32 @@ function scorm_dndupload_handle($uploadinfo) {
 
     return scorm_add_instance($scorm, null);
 }
+
+/**
+ * Sets activity completion state
+ *
+ * @param object $scorm object
+ * @param int $userid User ID
+ * @param int $completionstate Completion state
+ * @param array $grades grades array of users with grades - used when $userid = 0
+ */
+function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) {
+    if (!completion_info::is_enabled()) {
+        return;
+    }
+
+    $course = new stdClass();
+    $course->id = $scorm->course;
+
+    $cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course);
+    if (!empty($cm)) {
+        $completion = new completion_info($course);
+        if (empty($userid)) { //we need to get all the relevant users from $grades param.
+            foreach ($grades as $grade) {
+                $completion->update_state($cm, $completionstate, $grade->userid);
+            }
+        } else {
+            $completion->update_state($cm, $completionstate, $userid);
+        }
+    }
+}
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 1f32394..d435a8d 100644 (file)
@@ -7,7 +7,7 @@
 
 $THEME->name = 'anomaly';
 
-$THEME->sheets = array('base', 'general', 'browser','dock');
+$THEME->sheets = array('base', 'general', 'browser', 'dock', 'menu');
 /// This variable is an array containing the names of all the
 /// stylesheet files you want included in this theme, and in what order
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/theme/anomaly/pix/menu/nav-arrow-left.jpg b/theme/anomaly/pix/menu/nav-arrow-left.jpg
new file mode 100644 (file)
index 0000000..177f823
Binary files /dev/null and b/theme/anomaly/pix/menu/nav-arrow-left.jpg differ
diff --git a/theme/anomaly/pix/menu/nav-arrow-right.jpg b/theme/anomaly/pix/menu/nav-arrow-right.jpg
new file mode 100644 (file)
index 0000000..e9a89f5
Binary files /dev/null and b/theme/anomaly/pix/menu/nav-arrow-right.jpg differ
diff --git a/theme/anomaly/pix/menu/nav-arrowover-left.jpg b/theme/anomaly/pix/menu/nav-arrowover-left.jpg
new file mode 100644 (file)
index 0000000..596b6ff
Binary files /dev/null and b/theme/anomaly/pix/menu/nav-arrowover-left.jpg differ
diff --git a/theme/anomaly/pix/menu/nav-arrowover-right.jpg b/theme/anomaly/pix/menu/nav-arrowover-right.jpg
new file mode 100644 (file)
index 0000000..15f8cf1
Binary files /dev/null and b/theme/anomaly/pix/menu/nav-arrowover-right.jpg differ
index d5b8296..016f2fb 100644 (file)
@@ -81,4 +81,89 @@ class theme_anomaly_core_renderer extends core_renderer {
         return $output;
     }
 
+    /**
+     * Renders a custom menu object (located in outputcomponents.php)
+     *
+     * The custom menu this method override the render_custom_menu function
+     * in outputrenderers.php
+     * @staticvar int $menucount
+     * @param custom_menu $menu
+     * @return string
+     */
+    protected function render_custom_menu(custom_menu $menu) {
+
+        // If the menu has no children return an empty string
+        if (!$menu->has_children()) {
+            return '';
+        }
+
+        // Add a login or logout link
+        if (isloggedin()) {
+            $branchlabel = get_string('logout');
+            $branchurl   = new moodle_url('/login/logout.php');
+        } else {
+            $branchlabel = get_string('login');
+            $branchurl   = new moodle_url('/login/index.php');
+        }
+        $branch = $menu->add($branchlabel, $branchurl, $branchlabel, -1);
+
+        // Initialise this custom menu
+        $content = html_writer::start_tag('ul', array('class'=>'dropdown dropdown-horizontal'));
+        // Render each child
+        foreach ($menu->get_children() as $item) {
+            $content .= $this->render_custom_menu_item($item);
+        }
+        // Close the open tags
+        $content .= html_writer::end_tag('ul');
+        // Return the custom menu
+        return $content;
+    }
+
+    /**
+     * Renders a custom menu node as part of a submenu
+     *
+     * The custom menu this method override the render_custom_menu_item function
+     * in outputrenderers.php
+     *
+     * @see render_custom_menu()
+     *
+     * @staticvar int $submenucount
+     * @param custom_menu_item $menunode
+     * @return string
+     */
+    protected function render_custom_menu_item(custom_menu_item $menunode) {
+        // Required to ensure we get unique trackable id's
+        static $submenucount = 0;
+        $content = html_writer::start_tag('li');
+        if ($menunode->has_children()) {
+            // If the child has menus render it as a sub menu
+            $submenucount++;
+            if ($menunode->get_url() !== null) {
+                $url = $menunode->get_url();
+            } else {
+                $url = '#cm_submenu_'.$submenucount;
+            }
+            $content .= html_writer::start_tag('span', array('class'=>'customitem'));
+            $content .= html_writer::link($url, $menunode->get_text(), array('title'=>$menunode->get_title()));
+            $content .= html_writer::end_tag('span');
+            $content .= html_writer::start_tag('ul');
+            foreach ($menunode->get_children() as $menunode) {
+                $content .= $this->render_custom_menu_item($menunode);
+            }
+            $content .= html_writer::end_tag('ul');
+        } else {
+            // The node doesn't have children so produce a final menuitem
+
+            if ($menunode->get_url() !== null) {
+                $url = $menunode->get_url();
+            } else {
+                $url = '#';
+            }
+            $content .= html_writer::link($url, $menunode->get_text(), array('title'=>$menunode->get_title()));
+        }
+        $content .= html_writer::end_tag('li');
+        // Return the sub menu
+        return $content;
+    }
+
 }
\ No newline at end of file
index d695186..cfc2ecf 100644 (file)
@@ -11,21 +11,17 @@ a:visited {
 a:hover {
     text-decoration: underline;
 }
-
 img.icon,
 img.iconhelp {
     vertical-align: middle;
 }
-
 html, body {
     background-color: #C8C9C7;
 }
-
 #page-content {
     background-color: #FFF;
     min-width: 0;
 }
-
 /** Header **/
 
 #page-header {
@@ -36,14 +32,12 @@ html, body {
     padding: 0;
     width: 100%;
 }
-
 h1.headermain {
     float: left;
     font-size: 2.3em;
     margin: 15px;
     line-height: 1;
 }
-
 #page-header .headermain span {
     color: #C8C9C7;
 }
@@ -54,7 +48,6 @@ h1.headermain {
     border-bottom-color: #3A4D28;
     border-bottom-width: 3px;
 }
-
 #page-header .navbar {
     background-color: #697F55;
     width: 100%;
@@ -96,7 +89,6 @@ h1.headermain {
     background-color: #E3E3E3;
     padding: 4px 5px;
 }
-
 .coursebox {
     width: 100%;
     margin: 10px 0;
@@ -120,55 +112,44 @@ h1.headermain {
 .course-content .headingblock.outline {
     margin-top: 0;
 }
-
 .course-content .section.main {
     border:1px solid #E3E3E3;
     margin-bottom: 10px;
 }
-
 .course-content .section.main .left.side {
     float:left;width:20px;padding:5px;
 }
-
 .course-content .section.main .right.side {
     float: right;
     width: 20px;
     padding: 5px;
 }
-
 .course-content .section.main .content {
     padding: 5px 5px 10px;
     background-color: #FFF;
 }
-
 .course-content .section.main .content .section_add_menus {
     text-align: right;
 }
-
 #page-report-outline-user .section {
     border: 1px solid #DDD;
     margin: 0 5% 1.5em 5%;
 }
-
 #page-report-outline-user .section h2,
 #page-report-outline-user .section .content {
     margin: 5px 1em;
 }
-
 #page-report-outline-user .section table td {
     border: 0;
 }
-
 .generaltable {
     border: 1px solid #DDD;
 }
-
 .generaltable .cell {
     background-color: #FFF;
     border:1px solid #EEE;
     border-collapse: collapse;
 }
-
 .generaltable .header {
     background-color: #EEE;
     border: 1px solid #EEE;
@@ -180,17 +161,14 @@ h1.headermain {
     margin-top: 15px;
     margin-bottom: 15px;
 }
-
 .loginbox .loginform {
     margin-top: 15px;
 }
-
 .loginbox .loginform .form-label {
     width: 44%;
     float: left;
     text-align: right;
 }
-
 .loginbox .loginform .form-input {
     width: 55%;
     float: right;
@@ -200,42 +178,34 @@ h1.headermain {
 .loginbox .loginform .form-input input {
     width: 6em;
 }
-
 .loginbox.twocolumns {
     border: 1px solid #DDD;
 }
-
 .loginbox.twocolumns .loginpanel {
     float: left;
     width: 49%;
     text-align: center;
 }
-
 .loginbox.twocolumns .signuppanel {
     float: left;
     width: 50%;
     border-left: 1px solid #DDD;
 }
-
 .loginbox.twocolumns .signuppanel h2 {
     text-align: center;
 }
-
 .loginbox.twocolumns .signuppanel div {
     margin: 1em;
 }
-
 .loginbox.twocolumns .signuppanel div li {
     font-size: 90%;
 }
-
 .loginbox .loginsub {
     margin-left: 10%;
     margin-right: 10%;
     padding: 10px;
     margin-bottom: 5px;
 }
-
 .loginbox .guestsub {
     margin-left: 10%;
     margin-right: 10%;
@@ -243,7 +213,6 @@ h1.headermain {
     margin-bottom: 5px;
     border-top: 1px solid #DDD;
 }
-
 .dir-rtl .loginbox .loginform .form-input {width:50%}
 
 /** Blocks **/
@@ -260,15 +229,12 @@ h1.headermain {
 .block h4 {
     margin: 0;
 }
-
 .block .header {
     margin: 10px 6px 3px 6px;
 }
-
 .block .content {
     margin: 10px 6px 3px 6px;
 }
-
 /** Admin **/
 .box.adminwarning {
     text-align: center;
@@ -282,62 +248,50 @@ h1.headermain {
     font-size: 90%;
     padding: 10px 10%;
 }
-
 #adminsettings fieldset {
     border: 1px solid #C8C9C7;
     background-color: #E3E3E3;
 }
-
 #adminsettings fieldset .generalbox {
     margin: 1em 0.5em;
     border-color: #C8C9C7;
 }
-
 #adminsettings .form-buttons {
     margin-left: 13em;
 }
-
 .form-item {
     width: 100%;
     margin: 1em 1em 2em 1em;
 }
-
 .form-item .form-label {
     width: 12.5em;
     text-align: right;
     float: left;
     margin-right: 0.5em;
 }
-
 .form-item .form-label .form-shortname {
     display: block;
     color: #666;
     font-size: 75%;
 }
-
 .form-item .form-setting {
     margin-left: 13em;
 }
-
 .form-item .form-setting .defaultsnext {
     display:inline;
 }
-
 .form-item .form-setting .form-defaultinfo {
     display: inline;
     margin-left: 0.5em;
     font-size: 90%;
     color: #666;
 }
-
 .form-item .form-description {
     margin: 0.5em 1em 0.5em 13em;
 }
-
 .form-item .form-textarea textarea {
     width: 495px;
 }
-
 #authmenu .informationbox {
     width: 80%;
     margin: 0 auto 10px;
@@ -347,13 +301,11 @@ h1.headermain {
 #authmenu table td {
     border-width: 0;
 }
-
 #categoryquestions {
     margin-left: auto;
     margin-right: auto;
     width: 100%;
 }
-
 #categoryquestions th,
 .user th,
 .user th.header,
@@ -365,7 +317,6 @@ h1.headermain {
     border: 2px solid #697F55;
     border-bottom-color: #111;
 }
-
 .user th a:link,
 #categoryquestions th a:link,
 .group  th a:link,
@@ -373,7 +324,6 @@ h1.headermain {
     color: #FFF;
     text-decoration: none;
 }
-
 .user th a:visited,
 #categoryquestions th a:visited,
 .group th a:visited,
@@ -381,7 +331,6 @@ h1.headermain {
     color: #FFF;
     text-decoration: underline;
 }
-
 .user tr td.cell,
 #categoryquestions tr td.cell,
 .group tr td.cell,
@@ -389,39 +338,32 @@ h1.headermain {
     border: 1px solid #C8C9C7;
     border-width: 0 1px;
 }
-
 .user .r1 .cell,
 #categoryquestions .r1 .cell,
 .group .r1 .cell,
 .admin table .r1 .cell {
     background-color: #EEE;
 }
-
 .singlebutton,
 .buttons {
     text-align: center;
     margin: 20px;
 }
-
 .buttons form {
     display: inline;
 }
-
 .buttons div {
     display: inline;
 }
-
 .buttons .singlebutton {
     display: inline;
     padding: 5px;
     margin: 0;
 }
-
 .admin .generalbox {
     background-color: #EEE;
     border-color: #C8C9C7;
 }
-
 #admin-mnet-index table td,
 #files-index .column-content table td {
     border-width: 0;
@@ -436,7 +378,6 @@ h1.headermain {
 .tag-management-form {
     text-align:center;
 }
-
 #tag-management-list {
     margin-top:1em;
 }
@@ -446,12 +387,10 @@ h1.headermain {
     border-width: 0;
     vertical-align: top;
 }
-
 .userinfobox .side {
     width: 120px;
     text-align: center;
 }
-
 .userinfobox .list .label {font-weight:bold;text-align:right;
 }
 
@@ -463,7 +402,6 @@ h1.headermain {
     border: 1px solid #DDD;
     border-collapse: separate;
 }
-
 .forumpost,
 .forumpost .left.picture {
     background-color: #EEE;
@@ -484,13 +422,11 @@ h1.headermain {
 .forumpost .topic .author {
     padding-left: 10px;
 }
-
 .forumpost .content,
 .forumpost .options {
     background-color: white;
     padding-top: 10px;
 }
-
 .forumpost .content .shortenedpost a {
     margin: 0 10px;
     padding: 0;
@@ -505,7 +441,6 @@ h1.headermain {
 .forumpost .options .link {
     padding-right: 10px;
 }
-
 .forumpost .content .shortenedpost a,
 .forumpost .content .shortenedpost span.post-word-count,
 .forumpost .commands,
@@ -517,13 +452,11 @@ h1.headermain {
 .forumpost .row .left {
     clear: left;
 }
-
 .forumpost .posting.shortenedpost {margin-left: 10px;}
 
 #page-mod-forum-discuss #page-header { /* fixes broken header in forum discuss */
     margin-top: 10px;
 }
-
 /** Calendar **/
 .block.block_calendar_month td,
 .block.block_calendar_month th {
@@ -533,12 +466,10 @@ h1.headermain {
     width: 14%;
     line-height: 18px;
 }
-
 #calendar abbr,
 .block.block_calendar_month abbr {
     border-bottom-width: 0;
 }
-
 #calendar .weekend,
 .block.block_calendar_month .weekend {
     color: #A00;
@@ -547,19 +478,16 @@ h1.headermain {
 .block.block_calendar_month .today {
     border: 1px solid #444;
 }
-
 #calendar .eventnone a,
 .block.block_calendar_month .eventnone a {
     color:#444;
 }
-
 #calendar {
     width: 98%;
     margin: 0 1%;
     border-spacing: 5px;
     border-collapse: separate;
 }
-
 #calendar td,
 #calendar th {
     border-width: 0;
@@ -569,43 +497,35 @@ h1.headermain {
     line-height: 18px;
     vertical-align: top;
 }
-
 #calendar .maincalendar {
     width: auto;
     border: 1px solid #DDD;
 }
-
 #calendar .maincalendar .heightcontainer {
     height: 100%;
     position: relative;
     margin: 1em;
 }
-
 #calendar .maincalendar .header {
     padding: 5px;
     font-weight: bold;
 }
-
 #calendar .maincalendar .header .buttons {
     float: right;
 }
-
 #calendar .maincalendar table {
     width: 100%;
 }
-
 #calendar .maincalendar .calendar-controls {
     width: 100%;
     overflow: hidden;
     font-size: 1.1em;
 }
-
 #calendar .maincalendar .calendar-controls .previous {
     display: block;
     float: left;
     width: 20%;
 }
-
 #calendar .maincalendar .calendar-controls .current {
     display: block;
     float: left;
@@ -613,43 +533,35 @@ h1.headermain {
     text-align: center;
     margin-top: 0;
 }
-
 #calendar .maincalendar .calendar-controls .next {
     display: block;
     float: left;
     width: 20%;
     text-align: right;
 }
-
 #calendar .sidecalendar {
     width: 200px;
 }
-
 #calendar .sidecalendar h2,
 #calendar .sidecalendar h3 {
     margin: 5px;
     font-size: 95%;
 }
-
 #calendar .sidecalendar .block {
     border: 1px solid #DDD;
     margin-bottom: 10px;
     text-align: center;
 }
-
 #calendar .sidecalendar .block table {
     margin: 0 auto 5px;
 }
-
 #calendar .sidecalendar .block .filters table {
     width: 95%;
     margin: 0 auto 1em;
 }
-
 #calendar .sidecalendar .block .minicalendarblock {
     border-top: 1px solid #DDD;
 }
-
 #calendar .filters table {
     padding: 2px;
     background-color: #EEE;
@@ -657,7 +569,6 @@ h1.headermain {
     border-spacing: 2px;
     border-collapse: separate;
 }
-
 #calendar .filters table td {
     font-size: 100%;
     width: auto;
@@ -667,91 +578,73 @@ h1.headermain {
     border: 1px solid #444;
     overflow: hidden;
 }
-
 #calendar .calendar_event_global {
     background-color: #D6F8CD;
 }
-
 #calendar .calendar_event_course {
     background-color: #FFD3BD;
 }
-
 #calendar .calendar_event_group {
     background-color: #FEE7AE;
 }
-
 #calendar .calendar_event_user {
     background-color: #DCE7EC;
 }
-
 #calendar .maincalendar .calendarmonth {
     border-collapse: separate;
 }
-
 #calendar .maincalendar .calendarmonth th {
     font-size: 0.9em;
     border-bottom: 2px solid #444;
 }
-
 #calendar .maincalendar .calendarmonth td {
     border: 1px solid #EEE;
     border-bottom-color: #CCC;
     border-right-color: #CCC;
     height: 6em;
 }
-
 #calendar .maincalendar .calendarmonth td div {margin:4px;font-size:0.9em;
 }
 
 #calendar .maincalendar .calendarmonth td .day {font-weight:bold;
 }
-
 #calendar .maincalendar .calendarmonth tr td:first-child {
     border-left-color: #CCC;
 }
-
 #calendar .maincalendar .event {
     border-spacing: 0;
     border: 1px solid #DDD;
     background-color: #EEE;
 }
-
 #calendar .maincalendar .event .picture {
     width: 32px;
     text-align: center;
 }
-
 #calendar .maincalendar .event .topic {
     width: auto;
     padding: 5px;
 }
-
 #calendar .maincalendar .event .side {
     width: 32px;
 }
-
 #calendar .maincalendar .event .description {
     width: auto;
     border-top: 1px solid #DDD;
     border-left:1px solid #DDD;
     padding: 5px;
 }
-
 #calendar .maincalendar .bottom {
     text-align: center;
 }
-
 #calendar .calendarmonth ul {
     margin: 0;
     padding: 0;
 }
-
 #calendar .calendarmonth ul li {
     list-style: none;
     margin: 0;
     padding: 2px;
 }
-
 /** User **/
 
 .user .rolesform,
@@ -760,14 +653,11 @@ h1.headermain {
 .user #participantsform {
     text-align:center;
 }
-
 .user #participantsform table {
     margin-top:1em;
 }
-
 .user #participantsform td {text-align:left;
 }
-
 .user table.controls {
     margin: 5px auto;
     border: 1px solid #DDD;
@@ -777,7 +667,6 @@ h1.headermain {
 .user table.controls td {
     border-width:0px;
 }
-
 /** Overide for RTL layout **/
 
 .dir-rtl #page-header .navbar .breadcrumb {
@@ -787,53 +676,6 @@ h1.headermain {
     float:left;
 }
 
-/** Custom menu **/
-
-#custommenu {
-    margin-bottom: 0;
-}
-
-#custommenu .yui3-menu-horizontal .yui3-menu-content,
-#custommenu .yui3-menu-horizontal.javascript-disabled .yui3-menu-content,
-#custommenu .yui3-menu-horizontal .yui3-menu-content ul,
-#custommenu .yui3-menu-horizontal.javascript-disabled .yui3-menu-content ul,
-#custommenu .yui3-menu-horizontal.javascript-disabled .yui3-menu-content li li:hover > a,
-#custommenu .yui3-menu-horizontal .yui3-menu-label,
-#custommenu .yui3-menuitem,
-#custommenu .yui3-menuitem .yui3-menuitem-content {
-    border-width: 0;
-}
-
-#custommenu .yui3-menu .yui3-menu-label,
-#custommenu .yui3-menu .yui3-menuitem-content {
-    color: #FFF;
-    font-weight: bold;
-    line-height: 30px;
-    padding: 0 14px;
-}
-
-#custommenu .custom_menu_submenu .yui3-menu-content{
-    background-color: #3A4D28;
-}
-
-#custommenu .yui3-menuitem-active .yui3-menuitem-content {
-    background-image: none;
-}
-
-#custommenu .custom_menu_submenu .yui3-menu-label,
-#custommenu .custom_menu_submenu .yui3-menuitem-content {
-    line-height: 25px;
-    padding: 0 20px;
-}
-
-#custommenu .yui3-menu-label-active,
-#custommenu .yui3-menu-label-menuvisible,
-#custommenu .yui3-menuitem-active .yui3-menuitem-content,
-#custommenu .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content,
-#custommenu .yui3-menu-horizontal.javascript-disabled li a:hover {
-    background-color: #697F55;
-}
-
 /* Add Block
 -------------------------*/
 .block .content .singleselect form#add_block .select.menubui_addblock { width: 160px;}
diff --git a/theme/anomaly/style/menu.css b/theme/anomaly/style/menu.css
new file mode 100644 (file)
index 0000000..09a8c14
--- /dev/null
@@ -0,0 +1,221 @@
+/* Custom menu, in LTR mode
+--------------------------*/
+#custommenu {
+    width: 100%;
+    margin: 0;
+    padding: 0;
+    clear: both;
+    height: 30px;
+    background: #222;
+    margin:0;
+}
+/*
+Dropdown Menu - CSS from DeCaf Theme by Lei Zhang
+-------------------------------------------------*/
+ul.dropdown span.customitem {
+    padding: 0;
+    border: 0;
+    width: 100%;
+}
+ul.dropdown span.customitem {
+    padding: 0;
+    width: 100%;
+}
+ul.dropdown li a,
+ul.dropdown span.customitem a {
+    padding: 6px 20px;
+}
+ul.dropdown span.customitem a:hover {
+    border: 0;
+}
+#custommenu ul.dropdown ul {
+    padding:0;
+    width: auto;
+}
+#custommenu ul.dropdown ul a {
+    padding: 4px 18px;
+}
+#custommenu ul.dropdown > li span a {
+    height: 16px;
+}
+ul.dropdown,
+ul.dropdown li,
+ul.dropdown ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+ul.dropdown {
+    position:relative;
+    top: 0;
+    z-index: 597;
+    float:left;
+    font-size: 13px;
+}
+ul.dropdown li {
+    float: left;
+    line-height: 1.3em;
+    vertical-align: middle;
+    background-color: transparent;
+    color: #FFF;
+    zoom: 1;
+}
+ul.dropdown li.hover,
+ul.dropdown li:hover {
+    position: relative;
+    z-index: 599;
+    cursor: default;
+}
+ul.dropdown ul {
+    visibility: hidden;
+    position: absolute;
+    top: 100%;
+    z-index: 598;
+    left: 0;
+    right: auto;
+    margin-top: -1px; /** this setting is important do not change **/
+    font-size: 100%;
+}
+ul.dropdown ul li {
+    float:none;
+    background-color: #3A4D28;
+    border-width: 1px;
+    border-style: solid;
+    border-color: #3A4D28 #697F55 #5A6D49; /** menu item border **/
+    padding: 0;
+}
+ul.dropdown ul ul {
+    top: 0;
+    right: auto;
+    left: 100%;
+    margin-top: 0;
+    border-top: none;
+    border-left: none;
+    font-weight: 400;
+}
+ul.dropdown li:hover > ul {
+    visibility: visible;
+}
+ul.dropdown span,
+ul.dropdown span a,
+ul.dropdown li.clickable-with-children > a {
+    width: auto;
+    padding: 2px 6px 4px 20px;
+    color: #FFF;
+}
+ul.dropdown ul span,
+ul.dropdown ul span a,
+ul.dropdown ul li.clickable-with-children > a {
+    background-color: #3A4D28;
+    background-image: url([[pix:theme|menu/nav-arrow-right]]);
+    background-position: 100% 50%;
+    background-repeat: no-repeat;
+    color: #FFF;
+}
+ul.dropdown ul ul span,
+ul.dropdown ul ul span a,
+ul.dropdown ul ul li.clickable-with-children > a {
+    background-color: #3A4D28;
+    background-image: url([[pix:theme|menu/nav-arrow-right]]);
+    background-position: 100% 50%;
+    background-repeat: no-repeat;
+    color: #FFF;
+}
+ul.dropdown a:link,
+ul.dropdown a:visited {
+    color: white;
+    text-decoration: none;
+}
+ul.dropdown a:hover {
+    border: 0 none;
+    background-color: #697F55;
+    color: #FFF;
+}
+ul.dropdown ul ul li {
+    background-color: #3A4D28;
+}
+ul.dropdown ul ul ul li {
+    background-color: #3A4D28;
+}
+ul.dropdown li a,
+ul.dropdown span,
+ul.dropdown span a {
+    border: 0 none;
+}
+ul.dropdown ul li a,
+ul.dropdown ul span,
+ul.dropdown ul span a {
+    border: 0;
+}
+ul.dropdown ul ul li a,
+ul.dropdown ul ul span,
+ul.dropdown ul ul span a {
+    border: 0 none;
+}
+ul.dropdown ul ul ul li a,
+ul.dropdown ul ul ul span,
+ul.dropdown ul ul ul span a {
+    border: 0 none;
+}
+ul.dropdown a,
+ul.dropdown span {
+    display: block;
+}
+ul.dropdown ul a {
+    width: 166px;
+    padding: 2px 0 4px 5px;
+}
+ul.dropdown ul a.open:hover {
+   background-color: #697F55;
+    color: #FFF;
+}
+ul.dropdown ul li:hover > span,
+ul.dropdown ul li:hover > span a {
+    background-color: #697F55;
+    background-image:url([[pix:theme|menu/nav-arrowover-right]]);
+    color:  #FFF;
+}
+ul.dropdown li.clickable-with-children:hover > a {
+    background-image:url([[pix:theme|menu/nav-arrowover-right]]);
+}
+ul.dropdown *.open,
+ul.dropdown li:hover > span,
+ul.dropdown li:hover > span a {
+    background-color: #697F55;
+    color: #FFF;
+}
+ul.dropdown ul ul *.open,
+ul.dropdown ul ul li:hover > span,
+ul.dropdown ul ul li:hover > span a {
+    background-color: #697F55;
+    background-image: url([[pix:theme|menu/nav-arrowover-right]]);
+    color: #FFF;
+}
+
+/* Custom menu, in RTL mode
+---------------------------*/
+.dir-rtl #custommenu ul.dropdown {
+    float: right;
+}
+.dir-rtl #custommenu ul.dropdown ul{
+    right: 0;
+    left: auto;
+}
+.dir-rtl #custommenu ul.dropdown ul ul {
+    right: 203px;
+    left: auto;
+}
+.dir-rtl #custommenu ul.dropdown ul span,
+.dir-rtl #custommenu ul.dropdown ul span a,
+.dir-rtl #custommenu ul.dropdown ul li.clickable-with-children > a {
+    background-image: url([[pix:theme|menu/nav-arrow-left]]);
+    background-position: 0 50%;
+    background-repeat: no-repeat;
+}
+.dir-rtl #custommenu ul.dropdown ul li:hover > span,
+.dir-rtl #custommenu ul.dropdown ul li:hover > span a {
+    background-image: url([[pix:theme|menu/nav-arrowover-left]]);
+}
+.dir-rtl #custommenu ul.dropdown li.clickable-with-children:hover > a {
+    background-image: url([[pix:theme|menu/nav-arrowover-left]]);
+}
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