Merge branch 'MDL-34794_M23' of git://github.com/raymondAntonio/moodle into MOODLE_23...
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 8 Oct 2012 22:08:26 +0000 (00:08 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 8 Oct 2012 22:08:26 +0000 (00:08 +0200)
77 files changed:
admin/cli/install.php
admin/settings/users.php
admin/tool/xmldb/lang/en/tool_xmldb.php
course/lib.php
course/tests/courselib_test.php
enrol/database/lib.php
enrol/locallib.php
enrol/manual/manage.php
grade/report/user/lib.php
group/lib.php
install/lang/es_mx/install.php
install/lang/fi/install.php
install/lang/uk/install.php [new file with mode: 0644]
lang/en/admin.php
lib/grade/grade_category.php
lib/grade/tests/grade_category_test.php
lib/javascript-static.js
lib/modinfolib.php
lib/outputcomponents.php
lib/pagelib.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/tests/outputcomponents_test.php
lib/yui/dragdrop/dragdrop.js
mod/assign/gradingoptionsform.php
mod/assign/lib.php
mod/book/lang/en/book.php
mod/book/tool/importhtml/index.php
mod/book/view.php
mod/data/lib.php
mod/data/tests/fixtures/test_data_records.csv
mod/data/tests/search_test.php
mod/data/view.php
mod/forum/lib.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/quiz/cronlib.php
mod/quiz/locallib.php
mod/quiz/module.js
mod/quiz/styles.css
mod/quiz/tests/locallib_test.php
mod/resource/lib.php
mod/scorm/lib.php
mod/upgrade.txt
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
repository/upload/lang/en/repository_upload.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]
theme/formal_white/lang/en/theme_formal_white.php
user/lib.php
user/selector/module.js
user/tests/externallib_test.php [new file with mode: 0644]
version.php

index 79ff0b7..38aa06e 100644 (file)
@@ -240,18 +240,8 @@ echo get_string('cliinstallheader', 'install', $CFG->target_release)."\n";
 if ($interactive) {
     cli_separator();
     $languages = get_string_manager()->get_list_of_translations();
-    // format the langs nicely - 3 per line
-    $c = 0;
-    $langlist = '';
-    foreach ($languages as $key=>$lang) {
-        $c++;
-        $length = iconv_strlen($lang, 'UTF-8');
-        $padded = $lang.str_repeat(' ', 38-$length);
-        $langlist .= $padded;
-        if ($c % 3 == 0) {
-            $langlist .= "\n";
-        }
-    }
+    // Do not put the langs into columns because it is not compatible with RTL.
+    $langlist = implode("\n", $languages);
     $default = $CFG->lang;
     cli_heading(get_string('availablelangs', 'install'));
     echo $langlist."\n";
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 7b384b2..c1475be 100644 (file)
@@ -111,10 +111,13 @@ $string['checkindexes'] = 'Check indexes';
 $string['check_indexes'] = 'Look for missing DB indexes';
 $string['checkoraclesemantics'] = 'Check semantics';
 $string['check_oracle_semantics'] = 'Look for incorrect length semantics';
+$string['duplicateindexname'] = 'Duplicate index name';
 $string['incorrectfieldname'] = 'Incorrect name';
 $string['index'] = 'Index';
 $string['indexes'] = 'Indexes';
+$string['indexnameempty'] = 'Index name is empty';
 $string['integerincorrectlength'] = 'Incorrect length for integer field';
+$string['incorrectindexname'] = 'Incorrect index name';
 $string['incorrectkeyname'] = 'Incorrect key name';
 $string['incorrecttablename'] = 'Incorrect table name';
 $string['key'] = 'Key';
index 955b58c..ef150d0 100644 (file)
@@ -531,7 +531,7 @@ function print_mnet_log($hostid, $course, $user=0, $date=0, $order="l.time ASC",
 
 function print_log_csv($course, $user, $date, $order='l.time DESC', $modname,
                         $modid, $modaction, $groupid) {
-    global $DB;
+    global $DB, $CFG;
 
     $text = get_string('course')."\t".get_string('time')."\t".get_string('ip_address')."\t".
             get_string('fullnameuser')."\t".get_string('action')."\t".get_string('info');
@@ -600,6 +600,8 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname,
         $firstField = format_string($courses[$log->course], true, array('context' => $coursecontext));
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action, $log->info);
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);\r
+        $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action.' ('.$actionurl.')', $log->info);
         $text = implode("\t", $row);
         echo $text." \n";
     }
@@ -710,7 +712,8 @@ function print_log_xls($course, $user, $date, $order='l.time DESC', $modname,
         $myxls->write($row, 2, $log->ip, '');
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $myxls->write($row, 3, $fullname, '');
-        $myxls->write($row, 4, $log->module.' '.$log->action, '');
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);
+        $myxls->write($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')', '');
         $myxls->write($row, 5, $log->info, '');
 
         $row++;
@@ -823,7 +826,8 @@ function print_log_ods($course, $user, $date, $order='l.time DESC', $modname,
         $myxls->write_string($row, 2, $log->ip);
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $myxls->write_string($row, 3, $fullname);
-        $myxls->write_string($row, 4, $log->module.' '.$log->action);
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);
+        $myxls->write_string($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')');
         $myxls->write_string($row, 5, $log->info);
 
         $row++;
@@ -1572,7 +1576,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
                     // Display link itself
                     echo '<a ' . $linkcss . $mod->extra . $onclick .
                             ' href="' . $url . '"><img src="' . $mod->get_icon_url() .
-                            '" class="activityicon" alt="" /> ' .
+                            '" class="activityicon" alt="' . $modulename . '" /> ' .
                             $accesstext . '<span class="instancename">' .
                             $instancename . $altname . '</span></a>';
 
index 42b8057..acf5963 100644 (file)
@@ -36,7 +36,7 @@ class courselib_testcase extends advanced_testcase {
         $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
         $oldsections = array();
         $sections = array();
-        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
+        foreach ($DB->get_records('course_sections', array('course'=>$course->id), 'id') as $section) {
             $oldsections[$section->section] = $section->id;
             $sections[$section->id] = $section->section;
         }
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 f7ea7e2..f458a91 100644 (file)
@@ -64,7 +64,7 @@ $PAGE->set_heading($course->fullname);
 navigation_node::override_active_url(new moodle_url('/enrol/users.php', array('id'=>$course->id)));
 
 // Create the user selector objects.
-$options = array('enrolid' => $enrolid);
+$options = array('enrolid' => $enrolid, 'accesscontext' => $context);
 
 $potentialuserselector = new enrol_manual_potential_participant('addselect', $options);
 $currentuserselector = new enrol_manual_current_participant('removeselect', $options);
index 915aefd..cd2e951 100644 (file)
@@ -341,23 +341,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 91a4a09..7de314e 100644 (file)
@@ -85,7 +85,7 @@ $string['phpversionhelp'] = '<p>Moodle requiere al menos una versión de PHP 4.3
 (En caso de 5.0.x podría también revertir a la versión 4.4.x)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Si está viendo esta página es porque ha podido ejecutar el paquete <strong>{$a->packname} {$a->packversion}</strong> en su computadora. !Enhorabuena!';
-$string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora principalmente:';
+$string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora principalmente:';
 $string['welcomep40'] = 'El paquete también incluye <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'El uso de todas las aplicaciones del paquete está gobernado por sus respectivas
     licencias. El programa <strong>{$a->installername}</strong> es
index cffb9b0..57673d4 100644 (file)
@@ -82,9 +82,7 @@ $string['pathsunsecuredataroot'] = 'Dataroot-sijainti on turvallinen';
 $string['pathswrongadmindir'] = 'Admin-hakemistoa ei ole';
 $string['phpextension'] = '{$a} PHP-lisäosa';
 $string['phpversion'] = 'PHP versio';
-$string['phpversionhelp'] = '<p>Moodle vaatii vähintään PHP version 4.1.0.</p>
-<p>Käytät parhaillaan versiota {$a}</p>
-<p>Sinun täytyy päivittää PHP tai siirtää isäntä uudemman PHP version kanssa!</p>';
+$string['phpversionhelp'] = '<p>Moodle vaatii vähintään PHP-version 4.3.0 tai 5.1.0 (5.0.x sisältää monia tunnettuja ongelmia).</p> <p>Käytössäsi on versio {$a}</p> <p>Sinun pitää päivittää PHP tai siirtää palvelimelle jossa on uudempi PHP.<br /> (Jos käytössäsi on 5.0.x voit myös päivittää alaspäin 4.4.x -versioon)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Näet tämän sivun koska olet onnistuneesti asentanut ja käynnistänyt <strong>{$a->packname} {$a->packversion}</strong> paketin tietokoneellasi.
 Onnittelut!';
diff --git a/install/lang/uk/install.php b/install/lang/uk/install.php
new file mode 100644 (file)
index 0000000..c132e32
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['databasehost'] = 'Сервер бази даних';
+$string['databasename'] = 'Ім’я бази даних';
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 95b99f8..3fa19ec 100644 (file)
@@ -911,7 +911,10 @@ class grade_category extends grade_object {
 
                 $i = 1;
                 while ($originalindex+$i < count($grade_keys)) {
+
                     $possibleitemid = $grade_keys[$originalindex+$i];
+                    $i++;
+
                     if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
                         // The next grade item has a different grade. Stop looking.
                         break;
@@ -928,8 +931,6 @@ class grade_category extends grade_object {
                         $founditemid = $possibleitemid;
                         // Continue searching to see if there is an even higher grademax...
                     }
-
-                    $i++;
                 }
 
                 // Now drop whatever grade item we have found
index 727e698..e4281a0 100644 (file)
@@ -516,6 +516,21 @@ class grade_category_testcase extends grade_base_testcase {
         $this->assertEquals(count($grades), 1);
         $this->assertEquals($grades[$this->grade_items[2]->id], 6);
 
+        // MDL-35667 - There was an infinite loop if several items had the same grade and at least one was extra credit
+        $category = new grade_category();
+        $category->droplow     = 1;
+        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // simple weighted mean
+        $items[$this->grade_items[1]->id]->aggregationcoef = 1; // Mark grade item 1 as "extra credit"
+        $grades = array($this->grade_items[0]->id=>1, // 1 out of 110. Should be excluded from aggregation.
+                        $this->grade_items[1]->id=>1, // 1 out of 100. Extra credit. Should be retained.
+                        $this->grade_items[2]->id=>1, // 1 out of 6. Should be retained.
+                        $this->grade_items[4]->id=>1);// 1 out of 100. Should be retained.
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 3);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 1);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 1);
+        $this->assertEquals($grades[$this->grade_items[4]->id], 1);
+
     }
 
     /**
index 24ac3fa..4189c37 100644 (file)
@@ -1190,9 +1190,12 @@ function openpopup(event, args) {
         }
     }
 
-    // Cleans window name because IE does not support funky ones.
+    // Make sure the name argument is set and valid.
     var nameregex = /[^a-z0-9_]/i;
-    if (args.name.match(nameregex)) {
+    if (typeof args.name !== 'string') {
+        args.name = '_blank';
+    } else if (args.name.match(nameregex)) {
+        // Cleans window name because IE does not support funky ones.
         args.name = args.name.replace(nameregex, '_');
         if (M.cfg.developerdebug) {
             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
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 b580259..3534ef8 100644 (file)
@@ -544,6 +544,9 @@ class phpunit_util {
     public static function reset_all_data($logchanges = false) {
         global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
 
+        // Release memory and indirectly call destroy() methods to release resource handles, etc.
+        gc_collect_cycles();
+
         // reset global $DB in case somebody mocked it
         $DB = self::get_global_backup('DB');
 
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 f8b664d..0772f18 100644 (file)
@@ -167,6 +167,10 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             // this.lastdroptarget (ghost node we use for indicating where to drop)
             e.drag = e.target;
             e.drop = this.lastdroptarget;
+            // Check that drag object belongs to correct group
+            if (!this.in_group(e.drag)) {
+                return;
+            }
             // Check that drop object belong to correct group
             if (!e.drop || !e.drop.inGroup(this.groups)) {
                 return;
index 3e6e54b..b24f4fa 100644 (file)
@@ -44,16 +44,17 @@ class mod_assign_grading_options_form extends moodleform {
     function definition() {
         $mform = $this->_form;
         $instance = $this->_customdata;
+        $dirtyclass = array('class'=>'ignoredirty');
 
         $mform->addElement('header', 'general', get_string('gradingoptions', 'assign'));
         // visible elements
         $options = array(-1=>get_string('all'),10=>'10', 20=>'20', 50=>'50', 100=>'100');
-        $mform->addElement('select', 'perpage', get_string('assignmentsperpage', 'assign'), $options);
+        $mform->addElement('select', 'perpage', get_string('assignmentsperpage', 'assign'), $options, $dirtyclass);
         $options = array('' => get_string('filternone', 'assign'),
                          ASSIGN_FILTER_SUBMITTED => get_string('filtersubmitted', 'assign'),
                          ASSIGN_FILTER_REQUIRE_GRADING => get_string('filterrequiregrading', 'assign'));
         if ($instance['submissionsenabled']) {
-            $mform->addElement('select', 'filter', get_string('filter', 'assign'), $options);
+            $mform->addElement('select', 'filter', get_string('filter', 'assign'), $options, $dirtyclass);
         }
 
         // quickgrading
index e370467..92490ec 100644 (file)
@@ -823,7 +823,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 470e18e..e15de10 100644 (file)
@@ -47,6 +47,7 @@ $string['editingchapter'] = 'Editing chapter';
 $string['chaptertitle'] = 'Chapter title';
 $string['content'] = 'Content';
 $string['subchapter'] = 'Subchapter';
+$string['nocontent'] = 'No content has been added to this book yet.';
 $string['numbering'] = 'Chapter formatting';
 $string['numbering_help'] = '* None - Chapter and subchapter titles have no formatting
 * Numbers - Chapters and subchapter titles are numbered 1, 1.1, 1.2, 2, ...
index eaae261..25ee6f7 100644 (file)
@@ -60,9 +60,9 @@ $mform = new booktool_importhtml_form(null, array('id'=>$id, 'chapterid'=>$chapt
 // If data submitted, then process and store.
 if ($mform->is_cancelled()) {
     if (empty($chapter->id)) {
-        redirect("/mod/book/view.php?id=$cm->id");
+        redirect($CFG->wwwroot."/mod/book/view.php?id=$cm->id");
     } else {
-        redirect("/mod/book/view.php?id=$cm->id&chapterid=$chapter->id");
+        redirect($CFG->wwwroot."/mod/book/view.php?id=$cm->id&chapterid=$chapter->id");
     }
 
 } else if ($data = $mform->get_data()) {
index 91b83b8..6b9365d 100644 (file)
@@ -87,13 +87,16 @@ if ($chapterid == '0') { // Go to first chapter if no given.
     }
 }
 
-if (!$chapterid or !$chapter = $DB->get_record('book_chapters', array('id'=>$chapterid, 'bookid'=>$book->id))) {
-    print_error('errorchapter', 'mod_book', new moodle_url('/course/view.php', array('id'=>$course->id)));
-}
+$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
 
-// chapter is hidden for students
-if ($chapter->hidden and !$viewhidden) {
-    print_error('errorchapter', 'mod_book', new moodle_url('/course/view.php', array('id'=>$course->id)));
+// No content in the book.
+if (!$chapterid) {
+    $PAGE->set_url('/mod/book/view.php', array('id' => $id));
+    notice(get_string('nocontent', 'mod_book'), $courseurl->out(false));
+}
+// Chapter doesnt exist or it is hidden for students
+if ((!$chapter = $DB->get_record('book_chapters', array('id' => $chapterid, 'bookid' => $book->id))) or ($chapter->hidden and !$viewhidden)) {
+    print_error('errorchapter', 'mod_book', $courseurl);
 }
 
 $PAGE->set_url('/mod/book/view.php', array('id'=>$id, 'chapterid'=>$chapterid));
index fc26f78..583ea73 100644 (file)
@@ -3408,21 +3408,28 @@ function data_page_type_list($pagetype, $parentcontext, $currentcontext) {
 /**
  * Get all of the record ids from a database activity.
  *
- * @param int $dataid      The dataid of the database module.
- * @return array $idarray  An array of record ids
+ * @param int    $dataid      The dataid of the database module.
+ * @param object $selectdata  Contains an additional sql statement for the
+ *                            where clause for group and approval fields.
+ * @param array  $params      Parameters that coincide with the sql statement.
+ * @return array $idarray     An array of record ids
  */
-function data_get_all_recordids($dataid) {
+function data_get_all_recordids($dataid, $selectdata = '', $params = null) {
     global $DB;
-    $initsql = 'SELECT c.recordid
-                  FROM {data_fields} f,
-                       {data_content} c
-                 WHERE f.dataid = :dataid
-                   AND f.id = c.fieldid
-              GROUP BY c.recordid';
-    $initrecord = $DB->get_recordset_sql($initsql, array('dataid' => $dataid));
+    $initsql = 'SELECT r.id
+                  FROM {data_records} r
+                 WHERE r.dataid = :dataid';
+    if ($selectdata != '') {
+        $initsql .= $selectdata;
+        $params = array_merge(array('dataid' => $dataid), $params);
+    } else {
+        $params = array('dataid' => $dataid);
+    }
+    $initsql .= ' GROUP BY r.id';
+    $initrecord = $DB->get_recordset_sql($initsql, $params);
     $idarray = array();
     foreach ($initrecord as $data) {
-        $idarray[] = $data->recordid;
+        $idarray[] = $data->id;
     }
     // Close the record set and free up resources.
     $initrecord->close();
@@ -3567,8 +3574,7 @@ function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $so
     } else {
         list($insql, $inparam) = $DB->get_in_or_equal(array('-1'), SQL_PARAMS_NAMED);
     }
-    $nestfromsql .= ' AND c.recordid ' . $insql . $groupsql;
-    $nestfromsql = "$nestfromsql $selectdata";
+    $nestfromsql .= ' AND c.recordid ' . $insql . $selectdata . $groupsql;
     $sqlselect['sql'] = "$nestselectsql $nestfromsql $sortorder";
     $sqlselect['params'] = $inparam;
     return $sqlselect;
index 5ffa5bf..f740429 100644 (file)
 id,userid,groupid,dataid,timecreated,timemodified,approved
 1,1,0,1,1234567891,1234567892,1
-2,2,0,1,1234567891,1234567892,1
-3,3,0,1,1234567891,1234567892,1
-4,4,0,1,1234567891,1234567892,1
-5,5,0,1,1234567891,1234567892,1
-6,6,0,1,1234567891,1234567892,1
+2,2,1,1,1234567891,1234567892,1
+3,3,2,1,1234567891,1234567892,1
+4,4,1,1,1234567891,1234567892,1
+5,5,1,1,1234567891,1234567892,1
+6,6,2,1,1234567891,1234567892,1
 7,7,0,1,1234567891,1234567892,1
-8,8,0,1,1234567891,1234567892,1
-9,9,0,1,1234567891,1234567892,1
-10,10,0,1,1234567891,1234567892,1
-11,11,0,1,1234567891,1234567892,1
-12,12,0,1,1234567891,1234567892,1
-13,13,0,1,1234567891,1234567892,1
+8,8,2,1,1234567891,1234567892,1
+9,9,1,1,1234567891,1234567892,1
+10,10,1,1,1234567891,1234567892,1
+11,11,1,1,1234567891,1234567892,1
+12,12,2,1,1234567891,1234567892,1
+13,13,2,1,1234567891,1234567892,1
 14,14,0,1,1234567891,1234567892,1
-15,15,0,1,1234567891,1234567892,1
+15,15,2,1,1234567891,1234567892,1
 16,16,0,1,1234567891,1234567892,1
-17,17,0,1,1234567891,1234567892,1
-18,18,0,1,1234567891,1234567892,1
+17,17,1,1,1234567891,1234567892,1
+18,18,2,1,1234567891,1234567892,1
 19,19,0,1,1234567891,1234567892,1
-20,20,0,1,1234567891,1234567892,1
+20,20,1,1,1234567891,1234567892,1
 21,21,0,1,1234567891,1234567892,1
-22,22,0,1,1234567891,1234567892,1
+22,22,2,1,1234567891,1234567892,1
 23,23,0,1,1234567891,1234567892,1
-24,24,0,1,1234567891,1234567892,1
-25,25,0,1,1234567891,1234567892,1
+24,24,2,1,1234567891,1234567892,1
+25,25,2,1,1234567891,1234567892,1
 26,26,0,1,1234567891,1234567892,1
-27,27,0,1,1234567891,1234567892,1
-28,28,0,1,1234567891,1234567892,1
+27,27,1,1,1234567891,1234567892,1
+28,28,2,1,1234567891,1234567892,1
 29,29,0,1,1234567891,1234567892,1
-30,30,0,1,1234567891,1234567892,1
+30,30,2,1,1234567891,1234567892,1
 31,31,0,1,1234567891,1234567892,1
-32,32,0,1,1234567891,1234567892,1
-33,33,0,1,1234567891,1234567892,1
-34,34,0,1,1234567891,1234567892,1
-35,35,0,1,1234567891,1234567892,1
+32,32,1,1,1234567891,1234567892,1
+33,33,1,1,1234567891,1234567892,1
+34,34,2,1,1234567891,1234567892,1
+35,35,1,1,1234567891,1234567892,1
 36,36,0,1,1234567891,1234567892,1
 37,37,0,1,1234567891,1234567892,1
-38,38,0,1,1234567891,1234567892,1
-39,39,0,1,1234567891,1234567892,1
-40,40,0,1,1234567891,1234567892,1
+38,38,2,1,1234567891,1234567892,1
+39,39,1,1,1234567891,1234567892,1
+40,40,1,1,1234567891,1234567892,1
 41,41,0,1,1234567891,1234567892,1
-42,42,0,1,1234567891,1234567892,1
+42,42,1,1,1234567891,1234567892,1
 43,43,0,1,1234567891,1234567892,1
-44,44,0,1,1234567891,1234567892,1
+44,44,1,1,1234567891,1234567892,1
 45,45,0,1,1234567891,1234567892,1
 46,46,0,1,1234567891,1234567892,1
-47,47,0,1,1234567891,1234567892,1
+47,47,1,1,1234567891,1234567892,1
 48,48,0,1,1234567891,1234567892,1
 49,49,0,1,1234567891,1234567892,1
-50,50,0,1,1234567891,1234567892,1
+50,50,1,1,1234567891,1234567892,1
 51,51,0,1,1234567891,1234567892,1
 52,52,0,1,1234567891,1234567892,1
-53,53,0,1,1234567891,1234567892,1
-54,54,0,1,1234567891,1234567892,1
+53,53,1,1,1234567891,1234567892,1
+54,54,1,1,1234567891,1234567892,1
 55,55,0,1,1234567891,1234567892,1
-56,56,0,1,1234567891,1234567892,1
-57,57,0,1,1234567891,1234567892,1
-58,58,0,1,1234567891,1234567892,1
-59,59,0,1,1234567891,1234567892,1
-60,60,0,1,1234567891,1234567892,1
+56,56,2,1,1234567891,1234567892,1
+57,57,2,1,1234567891,1234567892,1
+58,58,2,1,1234567891,1234567892,1
+59,59,1,1,1234567891,1234567892,1
+60,60,1,1,1234567891,1234567892,1
 61,61,0,1,1234567891,1234567892,1
-62,62,0,1,1234567891,1234567892,1
+62,62,2,1,1234567891,1234567892,1
 63,63,0,1,1234567891,1234567892,1
 64,64,0,1,1234567891,1234567892,1
-65,65,0,1,1234567891,1234567892,1
-66,66,0,1,1234567891,1234567892,1
+65,65,1,1,1234567891,1234567892,1
+66,66,1,1,1234567891,1234567892,1
 67,67,0,1,1234567891,1234567892,1
 68,68,0,1,1234567891,1234567892,1
-69,69,0,1,1234567891,1234567892,1
-70,70,0,1,1234567891,1234567892,1
+69,69,2,1,1234567891,1234567892,1
+70,70,2,1,1234567891,1234567892,1
 71,71,0,1,1234567891,1234567892,1
-72,72,0,1,1234567891,1234567892,1
-73,73,0,1,1234567891,1234567892,1
+72,72,1,1,1234567891,1234567892,1
+73,73,1,1,1234567891,1234567892,1
 74,74,0,1,1234567891,1234567892,1
 75,75,0,1,1234567891,1234567892,1
-76,76,0,1,1234567891,1234567892,1
-77,77,0,1,1234567891,1234567892,1
+76,76,2,1,1234567891,1234567892,1
+77,77,2,1,1234567891,1234567892,1
 78,78,0,1,1234567891,1234567892,1
-79,79,0,1,1234567891,1234567892,1
-80,80,0,1,1234567891,1234567892,1
+79,79,1,1,1234567891,1234567892,1
+80,80,1,1,1234567891,1234567892,1
 81,81,0,1,1234567891,1234567892,1
-82,82,0,1,1234567891,1234567892,1
-83,83,0,1,1234567891,1234567892,1
-84,84,0,1,1234567891,1234567892,1
-85,85,0,1,1234567891,1234567892,1
+82,82,1,1,1234567891,1234567892,1
+83,83,1,1,1234567891,1234567892,1
+84,84,1,1,1234567891,1234567892,1
+85,85,1,1,1234567891,1234567892,1
 86,86,0,1,1234567891,1234567892,1
 87,87,0,1,1234567891,1234567892,1
 88,88,0,1,1234567891,1234567892,1
-89,89,0,1,1234567891,1234567892,1
-90,90,0,1,1234567891,1234567892,1
-91,91,0,1,1234567891,1234567892,1
-92,92,0,1,1234567891,1234567892,1
-93,93,0,1,1234567891,1234567892,1
-94,94,0,1,1234567891,1234567892,1
-95,95,0,1,1234567891,1234567892,1
-96,96,0,1,1234567891,1234567892,1
-97,97,0,1,1234567891,1234567892,1
-98,98,0,1,1234567891,1234567892,1
-99,99,0,1,1234567891,1234567892,1
-100,100,0,1,1234567891,1234567892,1
+89,89,1,1,1234567891,1234567892,1
+90,90,1,1,1234567891,1234567892,0
+91,91,2,1,1234567891,1234567892,0
+92,92,0,1,1234567891,1234567892,0
+93,93,2,1,1234567891,1234567892,0
+94,94,1,1,1234567891,1234567892,0
+95,95,1,1,1234567891,1234567892,0
+96,96,1,1,1234567891,1234567892,0
+97,97,0,1,1234567891,1234567892,0
+98,98,1,1,1234567891,1234567892,0
+99,99,2,1,1234567891,1234567892,0
+100,100,0,1,1234567891,1234567892,0
index 150bc5d..2980643 100644 (file)
@@ -70,6 +70,11 @@ class data_advanced_search_sql_test extends advanced_testcase {
      */
     public $datarecordcount = 100;
 
+    /**
+     * @var int $groupdatarecordcount  The number of records in the database in groups 0 and 1.
+     */
+    public $groupdatarecordcount = 75;
+
     /**
      * @var array $datarecordset   Expected record IDs.
      */
@@ -80,6 +85,11 @@ class data_advanced_search_sql_test extends advanced_testcase {
      */
     public $finalrecord = array();
 
+    /**
+     * @var int $approvedatarecordcount  The number of approved records in the database.
+     */
+    public $approvedatarecordcount = 89;
+
     /**
      * Set up function. In this instance we are setting up database
      * records to be used in the unit tests.
@@ -169,6 +179,13 @@ class data_advanced_search_sql_test extends advanced_testcase {
      * Test 4: data_get_advanced_search_sql provides an array which contains an sql string to be used for displaying records
      * to the user when they use the advanced search criteria and the parameters that go with the sql statement. This test
      * takes that information and does a search on the database, returning a record.
+     *
+     * Test 5: Returning to data_get_all_recordids(). Here we are ensuring that the total amount of record ids is reduced to
+     * match the group conditions that are provided. There are 25 entries which relate to group 2. They are removed
+     * from the total so we should only have 75 records total.
+     *
+     * Test 6: data_get_all_recordids() again. This time we are testing approved database records. We only want to
+     * display the records that have been approved. In this record set we have 89 approved records.
      */
     function test_advanced_search_sql_section() {
         global $DB;
@@ -193,5 +210,16 @@ class data_advanced_search_sql_test extends advanced_testcase {
         $allparams = array_merge($html['params'], array('dataid' => $this->recorddata->id));
         $records = $DB->get_records_sql($html['sql'], $allparams);
         $this->assertEquals($records, $this->finalrecord);
+
+        // Test 5
+        $groupsql = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
+        $params = array('currentgroup' => 1);
+        $recordids = data_get_all_recordids($this->recorddata->id, $groupsql, $params);
+        $this->assertEquals($this->groupdatarecordcount, count($recordids));
+
+        // Test 6
+        $approvesql = ' AND r.approved=1 ';
+        $recordids = data_get_all_recordids($this->recorddata->id, $approvesql, $params);
+        $this->assertEquals($this->approvedatarecordcount, count($recordids));
     }
 }
index 3e9146e..1b2d85b 100644 (file)
     groups_print_activity_menu($cm, $returnurl);
     $currentgroup = groups_get_activity_group($cm);
     $groupmode = groups_get_activity_groupmode($cm);
+    // If a student is not part of a group and seperate groups is enabled, we don't
+    // want them seeing all records.
+    if ($currentgroup == 0 && $groupmode == 1 && !has_capability('mod/data:manageentries', $context)) {
+        $canviewallrecords = false;
+    } else {
+        $canviewallrecords = true;
+    }
 
     // detect entries not approved yet and show hint instead of not found error
     if ($record and $data->approval and !$record->approved and $record->userid != $USER->id and !has_capability('mod/data:manageentries', $context)) {
@@ -465,7 +472,13 @@ if ($showactivity) {
             $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
             $params['currentgroup'] = $currentgroup;
         } else {
-            $groupselect = ' ';
+            if ($canviewallrecords) {
+                $groupselect = ' ';
+            } else {
+                // If separate groups are enabled and the user isn't in a group or
+                // a teacher, manager, admin etc, then just show them entries for 'All participants'.
+                $groupselect = " AND r.groupid = 0";
+            }
         }
 
         // Init some variables to be used by advanced search
@@ -590,10 +603,19 @@ if ($showactivity) {
         $sqlmax     = "SELECT $count FROM $tables $where $groupselect $approveselect"; // number of all recoirds user may see
         $allparams  = array_merge($params, $advparams);
 
-        $recordids = data_get_all_recordids($data->id);
+        // Provide initial sql statements and parameters to reduce the number of total records.
+        $selectdata = $groupselect . $approveselect;
+        $initialparams = array();
+        if ($currentgroup) {
+            $initialparams['currentgroup'] = $params['currentgroup'];
+        }
+        if (!$approvecap && $data->approval && isloggedin()) {
+            $initialparams['myid1'] = $params['myid1'];
+        }
+
+        $recordids = data_get_all_recordids($data->id, $selectdata, $initialparams);
         $newrecordids = data_get_advance_search_ids($recordids, $search_array, $data->id);
         $totalcount = count($newrecordids);
-        $selectdata = $groupselect . $approveselect;
 
         if (!empty($advanced)) {
             $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
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 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 d07442b..74cef7b 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;}
@@ -113,6 +116,7 @@ table.quizattemptsummary .noreviewmessage {color: gray;}
 #page-mod-quiz-view .generalbox#feedback {width:70%;margin-left:auto;margin-right:auto;padding-bottom:15px;}
 #page-mod-quiz-view .generalbox#feedback h2 {margin: 0;}
 #page-mod-quiz-view .generalbox#feedback h3 {text-align: left;}
+#page-mod-quiz-view.dir-rtl .generalbox#feedback h3 {text-align: center;}
 #page-mod-quiz-view .generalbox#feedback .overriddennotice {text-align: center;font-size: 0.7em;}
 .quizstartbuttondiv.quizsecuremoderequired input { display: none; }
 .jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; }
@@ -135,6 +139,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 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 72411c8..6eac247 100644 (file)
@@ -21,6 +21,9 @@ optional - no changes needed:
   xxx_dndupload_register() and xxx_dndupload_handle($uploadinfo) see:
   http://docs.moodle.org/dev/Implementing_Course_drag_and_drop_upload_support_in_a_module
 
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
+
 === 2.2 ===
 
 required changes in code:
@@ -29,12 +32,20 @@ required changes in code:
 * textlib->asort() replaced by specialized collatorlib::asort()
 * use new make_temp_directory() and make_cache_directory()
 
+optional - no changes needed:
+
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
 
 === 2.1 ===
 
 required changes in code:
 * add new support for basic restore from 1.9
 
+optional - no changes needed:
+
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
 
 === 2.0 ===
 
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 1d5664d..1a0210a 100644 (file)
@@ -34,4 +34,4 @@ $string['upload_error_no_file'] = 'No file was uploaded.';
 $string['upload_error_no_tmp_dir'] = 'PHP is missing a temporary folder.';
 $string['upload_error_cant_write'] = 'Failed to write file to disk.';
 $string['upload_error_extension'] = 'A PHP extension stopped the file upload.';
-$string['upload_error_invalid_file'] = 'The file \'{$a}\' has no data in it - did you try to upload a folder?';
\ No newline at end of file
+$string['upload_error_invalid_file'] = 'The file \'{$a}\' is either empty or a folder. To upload folders zip them first.';
\ No newline at end of file
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 cb80bc2..0b4746e 100644 (file)
@@ -67,7 +67,7 @@ $string['ctmo_onfrontpageonly'] = 'in the front page only'; // ctmo == credits t
 $string['customcss'] = 'Custom CSS';
 $string['customcssdesc'] = 'Any CSS you enter here will be added to every page allowing your to easily customise this theme.';
 $string['customlogourl'] = 'Custom logo';
-$string['customlogourldesc'] = 'Change the logo for this theme by entering the URL to an image you wish to use (i.e. http://www.yoursite.local/mylogo.png). As a reference the default logo is 200px wide, 50px high and a transparent png will work best.';
+$string['customlogourldesc'] = 'Change the logo for this theme by entering the full or relatve URL to an image you wish to use (i.e. http://www.yoursite.tld/mylogo.png or ../path/to/your/logo.png). As a reference the default logo is 200px wide, 50px high and a transparent png will work best.';
 $string['displayheading'] = 'Display page heading';
 $string['displaylogo'] = 'Display logo';
 $string['fontsizereference'] = 'Font size reference';
@@ -77,7 +77,7 @@ $string['footnotedesc'] = 'The content from this textarea will be displayed in t
 $string['framemargin'] = 'Frame margin';
 $string['framemargindesc'] = 'Room between the frame and the edge of the browser window. (This setting will be ignored if "{$a}" is requested).';
 $string['frontpagelogourl'] = 'Custom front page logo';
-$string['frontpagelogourldesc'] = 'Change the logo that is displayed on the front page of your site by entering the URL to the image you wish to use (i.e. http://www.yoursite.local/myfrontpagelogo.png). This setting overrides the custom logo setting. As a reference the default logo is 300px wide, 80px high and a transparent png will work best.';
+$string['frontpagelogourldesc'] = 'Change the logo that is displayed on the front page of your site by entering the full or relatve URL to the image you wish to use (i.e. http://www.yoursite.tld/myfrontpagelogo.png or ../path/to/your/logo.png). This setting overrides the custom logo setting. As a reference the default logo is 300px wide, 80px high and a transparent png will work best.';
 $string['headerbgc'] = 'Header background colour';
 $string['headerbgcdesc'] = 'This sets the blocks header background colour for the theme.';
 $string['headercontent'] = 'Header content';
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
index 48fc6b2..a5b0b9a 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012062502.04;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012062502.05;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes
 
-$release  = '2.3.2+ (Build: 20120927)'; // Human-friendly version name
+$release  = '2.3.2+ (Build: 20121005)'; // Human-friendly version name
 
 $branch   = '23';                       // this version's branch
 $maturity = MATURITY_STABLE;            // this version's maturity level