Merge branch 'MDL-38119' of git://github.com/timhunt/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 5 Mar 2013 15:45:01 +0000 (16:45 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 5 Mar 2013 15:45:01 +0000 (16:45 +0100)
151 files changed:
admin/settings/appearance.php
admin/tool/behat/cli/init.php [new file with mode: 0644]
admin/tool/behat/cli/util.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/locallib.php
admin/tool/behat/renderer.php
admin/tool/behat/tests/behat/basic_actions.feature
admin/tool/behat/tests/behat/manipulate_forms.feature
admin/tool/behat/tests/tool_behat_test.php
admin/tool/replace/index.php
admin/tool/replace/lang/en/tool_replace.php
backup/moodle2/backup_qtype_plugin.class.php
backup/moodle2/backup_stepslib.php
backup/upgrade.txt
backup/util/dbops/backup_structure_dbops.class.php
backup/util/structure/backup_nested_element.class.php
blocks/course_overview/renderer.php
cache/README.md
cache/classes/factory.php
cache/classes/helper.php
cache/classes/loaders.php
cache/lib.php
cache/renderer.php
cache/stores/file/tests/file_test.php [moved from question/format/blackboard/version.php with 56% similarity]
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php [new file with mode: 0644]
cache/stores/memcached/tests/memcached_test.php [new file with mode: 0644]
cache/stores/mongodb/lib.php
cache/stores/mongodb/tests/mongodb_test.php [new file with mode: 0644]
cache/stores/session/lib.php
cache/stores/session/tests/session_test.php [new file with mode: 0644]
cache/stores/static/lib.php
cache/stores/static/tests/static_test.php [new file with mode: 0644]
cache/testperformance.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/tests/fixtures/stores.php [new file with mode: 0644]
calendar/managesubscriptions.php
config-dist.php
course/format/topics/format.js
course/format/weeks/format.js
course/manage.php
course/togglecompletion.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php [new file with mode: 0644]
grade/edit/outcome/import.php
grade/report/grader/module.js
lang/en/admin.php
lang/en/cache.php
lang/en/question.php
lang/en/repository.php
lib/adminlib.php
lib/behat/behat_base.php
lib/behat/classes/behat_command.php
lib/behat/classes/util.php
lib/behat/form_field/behat_form_editor.php
lib/behat/form_field/behat_form_field.php
lib/behat/lib.php
lib/db/install.xml
lib/db/upgrade.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/filemanager.js
lib/formslib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrequirementslib.php
lib/pear/Crypt/CHAP.php
lib/pluginlib.php
lib/setuplib.php
lib/simplepie/moodle_simplepie.php
lib/statslib.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_transformations.php
lib/tests/filelib_test.php
lib/tests/htmlwriter_test.php
lib/tests/setuplib_test.php
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js [moved from lib/yui/tooltip/tooltip.js with 97% similarity]
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js [new file with mode: 0644]
lib/yui/notification/notification.js
lib/yui/src/tooltip/build.json [new file with mode: 0644]
lib/yui/src/tooltip/js/tooltip.js [new file with mode: 0644]
lib/yui/src/tooltip/meta/tooltip.json [new file with mode: 0644]
lib/zend/Zend/XmlRpc/Request.php
lib/zend/readme_moodle.txt
login/change_password.php
login/index.php
mod/assign/lib.php
mod/assignment/lang/en/assignment.php
mod/assignment/type/upload/assignment.class.php
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/subscribe.php
mod/label/lang/en/label.php
mod/label/lib.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/quiz/edit.php
mod/quiz/styles.css
mod/scorm/backup/moodle2/backup_scorm_stepslib.php
mod/workshop/form/assessment_form.php
mod/workshop/lib.php
mod/workshop/renderer.php
mod/workshop/styles.css
question/behaviour/manualgraded/tests/walkthrough_test.php
question/format/blackboard/format.php [deleted file]
question/format/blackboard/lang/en/qformat_blackboard.php [deleted file]
question/format/blackboard/tests/blackboardformat_test.php [deleted file]
question/format/blackboard/tests/fixtures/sample_blackboard.dat [deleted file]
question/format/blackboard_six/lang/en/qformat_blackboard_six.php
question/type/edit_question_form.php
question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php
question/type/match/edit_match_form.php
question/type/match/lang/en/qtype_match.php
question/type/numerical/edit_numerical_form.php
repository/coursefiles/lib.php
repository/equella/lib.php
repository/filepicker.js
repository/filepicker.php
repository/filesystem/lib.php
repository/flickr_public/lib.php
repository/lib.php
repository/local/lib.php
repository/manage_instances.php
repository/merlot/lib.php
repository/recent/lib.php
repository/s3/lib.php
repository/tests/repository_test.php
repository/upgrade.txt
repository/upload/lib.php
repository/url/lib.php
repository/user/lib.php
repository/webdav/lib.php
repository/wikimedia/lib.php
repository/youtube/lib.php
theme/base/style/admin.css
theme/base/style/core.css
theme/mymobile/javascript/custom.js
theme/mymobile/style/core.css
theme/yui_combo.php
theme/yui_image.php
user/index.php
user/view.php
version.php

index f3f63c9..a11f019 100644 (file)
@@ -114,6 +114,13 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('navshowcategories', new lang_string('navshowcategories', 'admin'), new lang_string('confignavshowcategories', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('navshowmycoursecategories', new lang_string('navshowmycoursecategories', 'admin'), new lang_string('navshowmycoursecategories_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('navshowallcourses', new lang_string('navshowallcourses', 'admin'), new lang_string('confignavshowallcourses', 'admin'), 0));
+    $sortoptions = array(
+        'sortorder' => new lang_string('sort_sortorder', 'admin'),
+        'fullname' => new lang_string('sort_fullname', 'admin'),
+        'shortname' => new lang_string('sort_shortname', 'admin'),
+        'idnumber' => new lang_string('sort_idnumber', 'admin'),
+    );
+    $temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
     $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 0));
diff --git a/admin/tool/behat/cli/init.php b/admin/tool/behat/cli/init.php
new file mode 100644 (file)
index 0000000..8162b0e
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI script to set up all the behat test environment.
+ *
+ * @package    tool_behat
+ * @copyright  2013 David MonllaĆ³
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die(); // No access from web!
+}
+
+// Is not really necessary but adding it as is a CLI_SCRIPT.
+define('CLI_SCRIPT', true);
+
+// Basic functions.
+require_once(__DIR__ . '/../../../../lib/clilib.php');
+require_once(__DIR__ . '/../../../../lib/behat/lib.php');
+
+// Changing the cwd to admin/tool/behat/cli.
+chdir(__DIR__);
+$output = null;
+exec("php util.php --diag", $output, $code);
+if ($code == 0) {
+    echo "Behat test environment already installed\n";
+
+} else if ($code == BEHAT_EXITCODE_INSTALL) {
+    // Behat and dependencies are installed and we need to install the test site.
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else if ($code == BEHAT_EXITCODE_REINSTALL) {
+    // Test site data is outdated.
+    passthru("php util.php --drop", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else if ($code == BEHAT_EXITCODE_COMPOSER) {
+    // Missing Behat dependencies.
+
+    // Changing to moodle dirroot to run composer related commands at project level.
+    chdir(__DIR__ . '/../../../..');
+    if (!file_exists(__DIR__ . '/../../../../composer.phar')) {
+        passthru("curl http://getcomposer.org/install | php", $code);
+        if ($code != 0) {
+            exit($code);
+        }
+    }
+
+    passthru("php composer.phar install --dev", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+    // Returning to admin/tool/behat/cli.
+    chdir(__DIR__);
+    passthru("php util.php --install", $code);
+    if ($code != 0) {
+        exit($code);
+    }
+
+} else {
+    // Generic error, we just output it.
+    echo implode("\n", $output)."\n";
+    exit($code);
+}
+
+// Enable editing mode according to config.php vars.
+passthru("php util.php --enable", $code);
+if ($code != 0) {
+    exit($code);
+}
+
+exit(0);
index 8cba367..e4e3d6e 100644 (file)
@@ -43,6 +43,7 @@ list($options, $unrecognized) = cli_get_params(
         'drop'    => false,
         'enable'  => false,
         'disable' => false,
+        'diag'    => false
     ),
     array(
         'h' => 'help'
@@ -59,6 +60,7 @@ Options:
 --drop     Drops the database tables and the dataroot contents
 --enable   Enables test environment and updates tests list
 --disable  Disables test environment
+--diag     Get behat test environment status code
 
 -h, --help     Print out this help
 
@@ -183,6 +185,9 @@ if ($options['install']) {
 } else if ($options['disable']) {
     behat_util::stop_test_mode();
     mtrace("Acceptance tests environment disabled");
+} else if ($options['diag']) {
+    $code = behat_util::get_behat_status();
+    exit($code);
 } else {
     echo $help;
 }
index 8bb1f1d..6c4f296 100644 (file)
@@ -38,4 +38,7 @@ $string['stepsdefinitionstype'] = 'Type';
 $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected ones';
 $string['viewsteps'] = 'Filter';
 $string['wheninfo'] = 'When. Actions that provokes an event';
-$string['wrongbehatsetup'] = 'Something is wrong with the setup, ensure you ran the composer installer and vendor/bin/behat file has execution permissions';
+$string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
+<li>You ran "curl http://getcomposer.org/installer | php"</li>
+<li>You ran "php composer.phar install --dev"</li>
+<li>vendor/bin/behat file has execution permissions</li></ul>';
index 33a5fd8..83c3a8d 100644 (file)
@@ -44,13 +44,13 @@ class tool_behat {
      * @param string $type
      * @param string $component
      * @param string $filter
-     * @return string
+     * @return array System steps or empty array if case there are no steps
      */
     public static function stepsdefinitions($type, $component, $filter) {
 
         // We don't require the test environment to be enabled to list the steps definitions
         // so test writers can more easily set up the environment.
-        behat_command::check_behat_setup();
+        behat_command::behat_setup_problem();
 
         // The loaded steps depends on the component specified.
         behat_config_manager::update_config_file($component, false);
@@ -70,15 +70,7 @@ class tool_behat {
         $options = ' --config="'.behat_config_manager::get_steps_list_config_filepath(). '" '.$filteroption;
         list($steps, $code) = behat_command::run($options);
 
-        if ($steps) {
-            $stepshtml = implode('', $steps);
-        }
-
-        if (empty($stepshtml)) {
-            $stepshtml = get_string('nostepsdefinitions', 'tool_behat');
-        }
-
-        return $stepshtml;
+        return $steps;
     }
 
 }
index 1a70fce..0df7e3a 100644 (file)
@@ -37,9 +37,9 @@ require_once($CFG->libdir . '/behat/classes/behat_command.php');
 class tool_behat_renderer extends plugin_renderer_base {
 
     /**
-     * Renders the list of available steps according to the submitted filters
+     * Renders the list of available steps according to the submitted filters.
      *
-     * @param string     $stepsdefinitions HTML from behat with the available steps
+     * @param mixed $stepsdefinitions Available steps array.
      * @param moodleform $form
      * @return string HTML code
      */
@@ -82,6 +82,30 @@ class tool_behat_renderer extends plugin_renderer_base {
         $html .= ob_get_contents();
         ob_end_clean();
 
+        if (empty($stepsdefinitions)) {
+            $stepsdefinitions = get_string('nostepsdefinitions', 'tool_behat');
+        } else {
+
+            $stepsdefinitions = implode('', $stepsdefinitions);
+
+            // Replace text selector type arguments with a user-friendly select.
+            $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR_STRING)/',
+                function ($matches) {
+                    return html_writer::select(behat_command::$allowedtextselectors, uniqid());
+                },
+                $stepsdefinitions
+            );
+
+            // Replace selector type arguments with a user-friendly select.
+            $stepsdefinitions = preg_replace_callback('/(SELECTOR_STRING)/',
+                function ($matches) {
+                    return html_writer::select(behat_command::$allowedselectors, uniqid());
+                },
+                $stepsdefinitions
+            );
+
+        }
+
         // Steps definitions.
         $html .= html_writer::tag('div', $stepsdefinitions, array('class' => 'steps-definitions'));
 
index 8adc991..d76edc8 100644 (file)
@@ -18,12 +18,32 @@ Feature: Page contents assertions
     When I follow "Overview"
     And I wait until the page is ready
     And I wait "2" seconds
-    And I hover ".region-content .generaltable td span"
+    And I hover ".region-content .generaltable td span" "css_element"
     Then I should see "I'm the description"
     And I should see "Filter groups by"
     And I should not see "Filter groupssss by"
-    And I should see "Group members" in the ".region-content table th.c1" element
-    And I should not see "Group membersssss" in the ".region-content table th.c1" element
+    And I should see "Group members" in the ".region-content table th.c1" "css_element"
+    And I should not see "Group membersssss" in the ".region-content table th.c1" "css_element"
     And I follow "Groups"
-    And the element "#groupeditform #showcreateorphangroupform" should be enabled
-    And the element "#groupeditform #showeditgroupsettingsform" should be disabled
+    And the "#groupeditform #showcreateorphangroupform" "css_element" should be enabled
+    And the "#groupeditform #showeditgroupsettingsform" "css_element" should be disabled
+
+  @javascript
+  Scenario: Locators inside specific DOM nodes using CSS selectors
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    And I follow "Course 1"
+    When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
+    Then I should not see "Question bank"
+    And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element"
+
+  @javascript
+  Scenario: Locators inside specific DOM nodes using XPath
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    When I click on "Move this to the dock" "button" in the "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+    Then I should not see "Turn editing on"
index 9db0740..5878098 100644 (file)
@@ -18,3 +18,17 @@ Feature: Forms manipulation
     And I uncheck "Unmask"
     And the "Unmask" checkbox should not be checked
     And I press "Update profile"
+
+  @javascript
+  Scenario: Expand all fieldsets and advanced elements
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I follow "Edit settings"
+    When I expand all fieldsets
+    Then I should see "Group mode"
+    And I should see "Your word for 'Course creator'"
+    And I should not see "Show more..."
+    And I should see "Show less..."
index 66e5aaf..c8fa488 100644 (file)
@@ -52,7 +52,6 @@ class testable_behat_config_manager extends behat_config_manager {
     /**
      * Allow access to protected method
      * @see parent::get_config_file_contents()
-     * @param string $prefix
      * @param array $features
      * @param array $stepsdefinitions
      * @return string
index 11e1068..67d945e 100644 (file)
@@ -49,6 +49,7 @@ if ($DB->get_dbfamily() !== 'mysql' and $DB->get_dbfamily() !== 'postgres') {
 
 if (!data_submitted() or !$search or !$replace or !confirm_sesskey() or !$sure) {   /// Print a form
     echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
+    echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
 
     echo $OUTPUT->box_start();
     echo '<div class="mdl-align">';
index 2193805..9103e89 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['disclaimer'] = 'I understand the risks of this operation:';
+$string['excludedtables'] = 'Several tables are not updated as part of the text replacement. This include configuration, log, events, and session tables.';
 $string['pageheader'] = 'Search and replace text throughout the whole database';
 $string['notifyfinished'] = '...finished';
 $string['notifyrebuilding'] = 'Rebuilding course cache...';
@@ -33,4 +34,4 @@ $string['pluginname'] = 'DB search and replace';
 $string['replacewith'] = 'Replace with this string:';
 $string['replacewithhelp'] = 'usually new server URL';
 $string['searchwholedb'] = 'Search whole database for:';
-$string['searchwholedbhelp'] = 'usually previous server URL';
\ No newline at end of file
+$string['searchwholedbhelp'] = 'usually previous server URL';
index c6839a7..d75f3d3 100644 (file)
@@ -58,12 +58,7 @@ abstract class backup_qtype_plugin extends backup_plugin {
         $answers->add_child($answer);
 
         // Set the sources
-        $answer->set_source_sql('
-                SELECT *
-                FROM {question_answers}
-                WHERE question = :question
-                ORDER BY id',
-                array('question' => backup::VAR_PARENTID));
+        $answer->set_source_table('question_answers', array('question' => backup::VAR_PARENTID), 'id ASC');
 
         // Aliases
         $answer->set_source_alias('answer', 'answertext');
@@ -92,12 +87,7 @@ abstract class backup_qtype_plugin extends backup_plugin {
         $units->add_child($unit);
 
         // Set the sources
-        $unit->set_source_sql('
-                SELECT *
-                FROM {question_numerical_units}
-                WHERE question = :question
-                ORDER BY id',
-                array('question' => backup::VAR_PARENTID));
+        $unit->set_source_table('question_numerical_units', array('question' => backup::VAR_PARENTID), 'id ASC');
 
         // don't need to annotate ids nor files
     }
index f2ba2af..6d59c4b 100644 (file)
@@ -232,20 +232,9 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
         // Set the sources
         $quba->set_source_table('question_usages',
                 array('id'                => '../' . $usageidname));
-        $qa->set_source_sql('
-                SELECT *
-                FROM {question_attempts}
-                WHERE questionusageid = :questionusageid
-                ORDER BY slot',
-                array('questionusageid'   => backup::VAR_PARENTID));
-        $step->set_source_sql('
-                SELECT *
-                FROM {question_attempt_steps}
-                WHERE questionattemptid = :questionattemptid
-                ORDER BY sequencenumber',
-                array('questionattemptid' => backup::VAR_PARENTID));
-        $variable->set_source_table('question_attempt_step_data',
-                array('attemptstepid'     => backup::VAR_PARENTID));
+        $qa->set_source_table('question_attempts', array('questionusageid' => backup::VAR_PARENTID), 'slot ASC');
+        $step->set_source_table('question_attempt_steps', array('questionattemptid' => backup::VAR_PARENTID), 'sequencenumber ASC');
+        $variable->set_source_table('question_attempt_step_data', array('attemptstepid' => backup::VAR_PARENTID));
 
         // Annotate ids
         $qa->annotate_ids('question', 'questionid');
@@ -569,7 +558,7 @@ class backup_enrolments_structure_step extends backup_structure_step {
         $userenrolments->add_child($enrolment);
 
         // Define sources - the instances are restored using the same sortorder, we do not need to store it in xml and deal with it afterwards.
-        $enrol->set_source_sql("SELECT * FROM {enrol} WHERE courseid = :courseid ORDER BY sortorder", array('courseid' => backup::VAR_COURSEID));
+        $enrol->set_source_table('enrol', array('courseid' => backup::VAR_COURSEID), 'sortorder ASC');
 
         // User enrolments only added only if users included
         if ($users) {
index cb02095..9028e74 100644 (file)
@@ -1,6 +1,13 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
+=== 2.5 ===
+
+* New optional param $sortby in backup set_source_table() allows to
+    specify the ORDER BY clause to be used. Previously it was required
+    to use the set_source_sql() more complex alternative in places
+    requiring ordering.
+
 === 2.4 ===
 
 * Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of
@@ -14,4 +21,4 @@ information provided here is intended especially for developers.
 === 2.2 ===
 
 * Since 2.2.4+ the backup file name schema has changed. The ID of the course will always be part of
-    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
\ No newline at end of file
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
index 00134da..29529f0 100644 (file)
@@ -48,7 +48,7 @@ abstract class backup_structure_dbops extends backup_dbops {
             return new backup_array_iterator($element->get_source_array());
 
         } else if ($element->get_source_table() !== null) { // It's one table, return recordset iterator
-            return $DB->get_recordset($element->get_source_table(), self::convert_params_to_values($params, $processor));
+            return $DB->get_recordset($element->get_source_table(), self::convert_params_to_values($params, $processor), $element->get_source_table_sortby());
 
         } else if ($element->get_source_sql() !== null) { // It's one sql, return recordset iterator
             return $DB->get_recordset_sql($element->get_source_sql(), self::convert_params_to_values($params, $processor));
index 8557ec8..32abd9a 100644 (file)
@@ -31,6 +31,7 @@ class backup_nested_element extends base_nested_element implements processable {
 
     protected $var_array; // To be used in case we pass one in-memory structure
     protected $table;     // Table (without prefix) to fetch records from
+    protected $tablesortby; // The field to sort by when using the table methods
     protected $sql;       // Raw SQL to fetch records from
     protected $params;    // Unprocessed params as specified in the set_source() call
     protected $procparams;// Processed (path resolved) params array
@@ -51,6 +52,7 @@ class backup_nested_element extends base_nested_element implements processable {
         parent::__construct($name, $attributes, $final_elements);
         $this->var_array = null;
         $this->table     = null;
+        $this->tablesortby = null;
         $this->sql       = null;
         $this->params    = null;
         $this->procparams= null;
@@ -191,13 +193,16 @@ class backup_nested_element extends base_nested_element implements processable {
         $this->var_array = $arr;
     }
 
-    public function set_source_table($table, $params) {
+    public function set_source_table($table, $params, $sortby = null) {
         if (!is_array($params)) { // Check we are passing array
             throw new base_element_struct_exception('setsourcerequiresarrayofparams');
         }
         // TODO: Only elements having final elements can set source
         $this->table = $table;
         $this->procparams = $this->convert_table_params($params);
+        if ($sortby) {
+            $this->tablesortby = $sortby;
+        }
     }
 
     public function set_source_sql($sql, $params) {
@@ -259,6 +264,10 @@ class backup_nested_element extends base_nested_element implements processable {
         return $this->table;
     }
 
+    public function get_source_table_sortby() {
+        return $this->tablesortby;
+    }
+
     public function get_source_sql() {
         return $this->sql;
     }
index b0b4507..689afb2 100644 (file)
@@ -87,13 +87,17 @@ class block_course_overview_renderer extends plugin_renderer_base {
             if ($ismovingcourse && ($course->id == $movingcourseid)) {
                 continue;
             }
+
+            $context = context_course::instance($course->id);
+            $fullname = format_string($course->fullname, true, array('context' => $context));
+
             $html .= $this->output->box_start('coursebox', "course-{$course->id}");
             $html .= html_writer::start_tag('div', array('class' => 'course_title'));
             // If user is editing, then add move icons.
             if ($userediting && !$ismovingcourse) {
                 $moveicon = html_writer::empty_tag('img',
                         array('src' => $this->pix_url('t/move')->out(false),
-                            'alt' => get_string('movecourse', 'block_course_overview', $course->fullname),
+                            'alt' => get_string('movecourse', 'block_course_overview', $fullname),
                             'title' => get_string('move')));
                 $moveurl = new moodle_url($this->page->url, array('sesskey' => sesskey(), 'movecourse' => 1, 'courseid' => $course->id));
                 $moveurl = html_writer::link($moveurl, $moveicon);
@@ -101,11 +105,10 @@ class block_course_overview_renderer extends plugin_renderer_base {
 
             }
 
-            $attributes = array('title' => s($course->fullname));
+            $attributes = array('title' => str_replace('&amp;', '&', $fullname));
             if ($course->id > 0) {
                 $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
-                $coursefullname = format_string($course->fullname, true, $course->id);
-                $link = html_writer::link($courseurl, $coursefullname, $attributes);
+                $link = html_writer::link($courseurl, $fullname, $attributes);
                 $html .= $this->output->heading($link, 2, 'title');
             } else {
                 $html .= $this->output->heading(html_writer::link(
@@ -135,7 +138,7 @@ class block_course_overview_renderer extends plugin_renderer_base {
                             array('sesskey' => sesskey(), 'moveto' => $courseordernumber, 'courseid' => $movingcourseid));
                 $a = new stdClass();
                 $a->movingcoursename = $courses[$movingcourseid]->fullname;
-                $a->currentcoursename = $course->fullname;
+                $a->currentcoursename = $fullname;
                 $movehereicon = html_writer::empty_tag('img',
                         array('src' => $this->output->pix_url('movehere'),
                             'alt' => get_string('moveafterhere', 'block_course_overview', $a),
index c04c7e7..86218be 100644 (file)
@@ -226,3 +226,14 @@ The first method is designed to be used when you have a single known definition
 The second method is a lot more intensive for the system. There are defined invalidation events that definitions can "subscribe" to (through the definitions invalidationevents option).
 When you invalidate by event the cache API finds all of the definitions that subscribe to the event, it then loads the stores for each of those definitions and purges the keys from each store.
 This is obviously a recursive, and therefore, intense process.
+
+### Unit tests
+Both the cache API and the cache stores have unit tests.
+Please be aware that several of the cache stores require configuration in order to be able operate in the unit tests.
+Tests for stores requiring configuration that havn't been configured will be skipped.
+All configuration is done in your sites config.php through definitions.
+The following snippet illustates how to configure the three core cache stores that require configuration.
+
+    define('TEST_CACHESTORE_MEMCACHE_TESTSERVERS', '127.0.0.1:11211');
+    define('TEST_CACHESTORE_MEMCACHED_TESTSERVERS', '127.0.0.1:11211');
+    define('TEST_CACHESTORE_MONGODB_TESTSERVER', 'mongodb://localhost:27017');
\ No newline at end of file
index 24c7c1e..43f075b 100644 (file)
@@ -75,6 +75,12 @@ class cache_factory {
      */
     protected $cachesfromparams = array();
 
+    /**
+     * An array of stores organised by definitions.
+     * @var array
+     */
+    protected $definitionstores = array();
+
     /**
      * An array of instantiated stores.
      * @var array
@@ -272,9 +278,27 @@ class cache_factory {
         // order to address the issues.
         $store = $this->stores[$name]->create_clone($details);
         $store->initialise($definition);
+        $definitionid = $definition->get_id();
+        if (!isset($this->definitionstores[$definitionid])) {
+            $this->definitionstores[$definitionid] = array();
+        }
+        $this->definitionstores[$definitionid][] = $store;
         return $store;
     }
 
+    /**
+     * Returns an array of cache stores that have been initialised for use in definitions.
+     * @param cache_definition $definition
+     * @return array
+     */
+    public function get_store_instances_in_use(cache_definition $definition) {
+        $id = $definition->get_id();
+        if (!isset($this->definitionstores[$id])) {
+            return array();
+        }
+        return $this->definitionstores[$id];
+    }
+
     /**
      * Creates a cache config instance with the ability to write if required.
      *
index a8f3a26..20e2f6d 100644 (file)
@@ -312,15 +312,18 @@ class cache_helper {
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
-                // Create the cache.
-                $cache = $factory->create_cache($definition);
-                // Initialise, in case of a store.
-                if ($cache instanceof cache_store) {
-                    $cache->initialise($definition);
+                // Check if this definition would result in a persistent loader being in use.
+                if ($definition->should_be_persistent()) {
+                    // There may be a persistent cache loader. Lets purge that first so that any persistent data is removed.
+                    $cache = $factory->create_cache_from_definition($definition->get_component(), $definition->get_area());
+                    $cache->purge();
+                }
+                // Get all of the store instances that are in use for this store.
+                $stores = $factory->get_store_instances_in_use($definition);
+                foreach ($stores as $store) {
+                    // Purge each store individually.
+                    $store->purge();
                 }
-                // Purge the cache.
-                $cache->purge();
-
                 // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
                 if ($invalidationeventset === false) {
                     // Get the event invalidation cache.
index 4835360..6402e2c 100644 (file)
@@ -1094,10 +1094,11 @@ class cache_application extends cache implements cache_loader_with_locking {
             $cache = cache::make('core', 'eventinvalidation');
             $events = $cache->get_many($definition->get_invalidation_events());
             $todelete = array();
+            $purgeall = false;
             // Iterate the returned data for the events.
             foreach ($events as $event => $keys) {
                 if ($keys === false) {
-                    // There are no keys.
+                    // No data to be invalidated yet.
                     continue;
                 }
                 // Look at each key and check the timestamp.
@@ -1105,11 +1106,18 @@ class cache_application extends cache implements cache_loader_with_locking {
                     // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
                     // invalidation and now)then we need to invaliate the key.
                     if ($timestamp >= $lastinvalidation) {
-                        $todelete[] = $key;
+                        if ($key === 'purged') {
+                            $purgeall = true;
+                            break;
+                        } else {
+                            $todelete[] = $key;
+                        }
                     }
                 }
             }
-            if (!empty($todelete)) {
+            if ($purgeall) {
+                $this->purge();
+            } else if (!empty($todelete)) {
                 $todelete = array_unique($todelete);
                 $this->delete_many($todelete);
             }
@@ -1435,6 +1443,7 @@ class cache_session extends cache {
             $cache = cache::make('core', 'eventinvalidation');
             $events = $cache->get_many($definition->get_invalidation_events());
             $todelete = array();
+            $purgeall = false;
             // Iterate the returned data for the events.
             foreach ($events as $event => $keys) {
                 if ($keys === false) {
@@ -1446,11 +1455,18 @@ class cache_session extends cache {
                     // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
                     // invalidation and now)then we need to invaliate the key.
                     if ($timestamp >= $lastinvalidation) {
-                        $todelete[] = $key;
+                        if ($key === 'purged') {
+                            $purgeall = true;
+                            break;
+                        } else {
+                            $todelete[] = $key;
+                        }
                     }
                 }
             }
-            if (!empty($todelete)) {
+            if ($purgeall) {
+                $this->purge();
+            } else if (!empty($todelete)) {
                 $todelete = array_unique($todelete);
                 $this->delete_many($todelete);
             }
index 001cd43..1ee4100 100644 (file)
@@ -148,4 +148,81 @@ class cache_exception extends moodle_exception {
         // This may appear like a useless override but you will notice that we have set a MUCH more useful default for $module.
         parent::__construct($errorcode, $module, $link, $a, $debuginfo);
     }
+}
+
+/**
+ * An array of cacheable objects.
+ *
+ * This class allows a developer to create an array of cacheable objects and store that.
+ * The cache API doesn't check items within an array to see whether they are cacheable. Such a check would be very costly to both
+ * arrays using cacheable object and those that don't.
+ * Instead the developer must explicitly use a cacheable_object_array instance.
+ *
+ * The following is one example of how this class can be used.
+ * <code>
+ * $data = array();
+ * $data[] = new cacheable_object('one');
+ * $data[] = new cacheable_object('two');
+ * $data[] = new cacheable_object('three');
+ * $cache->set(new cacheable_object_array($data));
+ * </code>
+ * Another example would be
+ * <code>
+ * $data = new cacheable_object_array();
+ * $data[] = new cacheable_object('one');
+ * $data[] = new cacheable_object('two');
+ * $data[] = new cacheable_object('three');
+ * $cache->set($data);
+ * </code>
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cacheable_object_array extends ArrayObject implements cacheable_object {
+
+    /**
+     * Constructs a new array object instance.
+     * @param array $items
+     */
+    final public function __construct(array $items = array()) {
+        parent::__construct($items, ArrayObject::STD_PROP_LIST);
+    }
+
+    /**
+     * Returns the data to cache for this object.
+     *
+     * @return array An array of cache_cached_object instances.
+     * @throws coding_exception
+     */
+    final public function prepare_to_cache() {
+        $result = array();
+        foreach ($this as $key => $value) {
+            if ($value instanceof cacheable_object) {
+                $value = new cache_cached_object($value);
+            } else {
+                throw new coding_exception('Only cacheable_object instances can be added to a cacheable_array');
+            }
+            $result[$key] = $value;
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the cacheable_object_array that was originally sent to the cache.
+     *
+     * @param array $data
+     * @return cacheable_object_array
+     * @throws coding_exception
+     */
+    final static public function wake_from_cache($data) {
+        if (!is_array($data)) {
+            throw new coding_exception('Invalid data type when reviving cacheable_array data');
+        }
+        $result = array();
+        foreach ($data as $key => $value) {
+            $result[$key] = $value->restore_object();
+        }
+        $class = __CLASS__;
+        return new $class($result);
+    }
 }
\ No newline at end of file
index 7e55df8..dae5479 100644 (file)
@@ -93,15 +93,25 @@ class core_cache_renderer extends plugin_renderer_base {
                 $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
             }
 
+            $isready = $store['isready'] && $store['requirementsmet'];
+            $readycell = new html_table_cell;
+            if ($isready) {
+                $readycell->text = $this->output->pix_icon('i/valid', '1');
+            }
+
             $storename = $store['name'];
             if (!empty($store['default'])) {
                 $storename = get_string('store_'.$store['name'], 'cache');
             }
+            if (!$isready && (int)$store['mappings'] > 0) {
+                $readycell->text = $this->output->help_icon('storerequiresattention', 'cache');
+                $readycell->attributes['class'] = 'store-requires-attention';
+            }
 
             $row = new html_table_row(array(
                 $storename,
                 get_string('pluginname', 'cachestore_'.$store['plugin']),
-                ($store['isready'] && $store['requirementsmet']) ? $this->output->pix_icon('i/valid', '1') : '',
+                $readycell,
                 $store['mappings'],
                 join(', ', $modes),
                 join(', ', $supports),
similarity index 56%
rename from question/format/blackboard/version.php
rename to cache/stores/file/tests/file_test.php
index 0c61840..be321f2 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Version information for the calculated question type.
+ * File unit tests
  *
- * @package    qformat_blackboard
- * @copyright  2011 The Open University
+ * @package    cachestore_file
+ * @copyright  2013 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->component = 'qformat_blackboard';
-$plugin->version   = 2012112900;
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/file/lib.php');
 
-$plugin->requires  = 2012112900;
-
-$plugin->maturity  = MATURITY_STABLE;
+/**
+ * File unit test class.
+ *
+ * @package    cachestore_file
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_file_test extends cachestore_tests {
+    /**
+     * Returns the file class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_file';
+    }
+}
\ No newline at end of file
index c457cc0..906b355 100644 (file)
@@ -117,7 +117,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
             $this->connection->addServer($server[0], $server[1], true, $server[2]);
             // Test the connection to this server.
         }
-        $this->isready = @$this->connection->set("ping", 'ping', MEMCACHE_COMPRESSED, 1);
+        $this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
     }
 
     /**
@@ -191,6 +191,20 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         return self::MODE_APPLICATION + self::MODE_SESSION;
     }
 
+    /**
+     * Parses the given key to make it work for this memcache backend.
+     *
+     * @param string $key The raw key.
+     * @return string The resulting key.
+     */
+    protected function parse_key($key) {
+        if (strlen($key) > 245) {
+            $key = '_sha1_'.sha1($key);
+        }
+        $key = 'mdl_'.$key;
+        return $key;
+    }
+
     /**
      * Retrieves an item from the cache store given its key.
      *
@@ -198,7 +212,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return mixed The data that was associated with the key, or false if the key did not exist.
      */
     public function get($key) {
-        return $this->connection->get($key);
+        return $this->connection->get($this->parse_key($key));
     }
 
     /**
@@ -211,16 +225,23 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      *      be set to false.
      */
     public function get_many($keys) {
-        $result = $this->connection->get($keys);
+        $mkeys = array();
+        foreach ($keys as $key) {
+            $mkeys[$key] = $this->parse_key($key);
+        }
+        $result = $this->connection->get($mkeys);
         if (!is_array($result)) {
             $result = array();
         }
-        foreach ($keys as $key) {
-            if (!array_key_exists($key, $result)) {
-                $result[$key] = false;
+        $return = array();
+        foreach ($mkeys as $key => $mkey) {
+            if (!array_key_exists($mkey, $result)) {
+                $return[$key] = false;
+            } else {
+                $return[$key] = $result[$mkey];
             }
         }
-        return $result;
+        return $return;
     }
 
     /**
@@ -231,7 +252,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool True if the operation was a success false otherwise.
      */
     public function set($key, $data) {
-        return $this->connection->set($key, $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
+        return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
     }
 
     /**
@@ -245,7 +266,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
     public function set_many(array $keyvaluearray) {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
-            if ($this->connection->set($pair['key'], $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
+            if ($this->connection->set($this->parse_key($pair['key']), $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
                 $count++;
             }
         }
@@ -259,7 +280,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool Returns true if the operation was a success, false otherwise.
      */
     public function delete($key) {
-        return $this->connection->delete($key);
+        return $this->connection->delete($this->parse_key($key));
     }
 
     /**
diff --git a/cache/stores/memcache/tests/memcache_test.php b/cache/stores/memcache/tests/memcache_test.php
new file mode 100644 (file)
index 0000000..023e6cc
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Memcache unit tests.
+ *
+ * If you wish to use these unit tests all you need to do is add the following definition to
+ * your config.php file.
+ *
+ * define('TEST_CACHESTORE_MEMCACHE_TESTSERVERS', '127.0.0.1:11211');
+ *
+ * @package    cachestore_memcache
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/memcache/lib.php');
+
+/**
+ * Memcache unit test class.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcache_test extends cachestore_tests {
+    /**
+     * Prepare to run tests.
+     */
+    public function setUp() {
+        if (defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
+            set_config('testservers', TEST_CACHESTORE_MEMCACHE_TESTSERVERS, 'cachestore_memcache');
+            $this->resetAfterTest();
+        }
+        parent::setUp();
+    }
+    /**
+     * Returns the memcache class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_memcache';
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/memcached/tests/memcached_test.php b/cache/stores/memcached/tests/memcached_test.php
new file mode 100644 (file)
index 0000000..f0896ca
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Memcached unit tests.
+ *
+ * If you wish to use these unit tests all you need to do is add the following definition to
+ * your config.php file.
+ *
+ * define('TEST_CACHESTORE_MEMCACHED_TESTSERVERS', '127.0.0.1:11211');
+ *
+ * @package    cachestore_memcached
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/memcached/lib.php');
+
+/**
+ * Memcached unit test class.
+ *
+ * @package    cachestore_memcached
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcached_test extends cachestore_tests {
+    /**
+     * Prepare to run tests.
+     */
+    public function setUp() {
+        if (defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
+            set_config('testservers', TEST_CACHESTORE_MEMCACHED_TESTSERVERS, 'cachestore_memcached');
+            $this->resetAfterTest();
+        }
+        parent::setUp();
+    }
+    /**
+     * Returns the memcached class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_memcached';
+    }
+}
\ No newline at end of file
index 7a5e6ec..a6fd6a2 100644 (file)
@@ -274,11 +274,7 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         $cursor = $this->collection->find($query);
         $results = array();
         foreach ($cursor as $result) {
-            if (array_key_exists('key', $result)) {
-                $id = $result[$key];
-            } else {
-                $id = (string)$result['key'];
-            }
+            $id = (string)$result['key'];
             $results[$id] = unserialize($result['data']);
         }
         foreach ($keys as $key) {
@@ -307,11 +303,22 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         $record['data'] = serialize($data);
         $options = array(
             'upsert' => true,
-            'safe' => $this->usesafe
+            'safe' => $this->usesafe,
+            'w' => $this->usesafe ? 1 : 0
         );
         $this->delete($key);
         $result = $this->collection->insert($record, $options);
-        return $result;
+        if ($result === true) {
+            // Safe mode is off.
+            return true;
+        } else if (is_array($result)) {
+            if (empty($result['ok']) || isset($result['err'])) {
+                return false;
+            }
+            return true;
+        }
+        // Who knows?
+        return false;
     }
 
     /**
@@ -326,11 +333,11 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
             $result = $this->set($pair['key'], $pair['value']);
-            if ($result === true || (is_array($result)) && !empty($result['ok'])) {
+            if ($result === true) {
                  $count++;
             }
         }
-        return;
+        return $count;
     }
 
     /**
@@ -349,13 +356,25 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         }
         $options = array(
             'justOne' => false,
-            'safe' => $this->usesafe
+            'safe' => $this->usesafe,
+            'w' => $this->usesafe ? 1 : 0
         );
         $result = $this->collection->remove($criteria, $options);
-        if ($result === false || (is_array($result) && !array_key_exists('ok', $result)) || $result === 0) {
-            return false;
+
+        if ($result === true) {
+            // Safe mode.
+            return true;
+        } else if (is_array($result)) {
+            if (empty($result['ok']) || isset($result['err'])) {
+                return false;
+            } else if (empty($result['n'])) {
+                // Nothing was removed.
+                return false;
+            }
+            return true;
         }
-        return !empty($result['ok']);
+        // Who knows?
+        return false;
     }
 
     /**
@@ -496,7 +515,6 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         if (empty($config->testserver)) {
             return false;
         }
-
         $configuration = array();
         $configuration['server'] = $config->testserver;
         if (!empty($config->testreplicaset)) {
@@ -511,9 +529,7 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         if (!empty($config->testdatabase)) {
             $configuration['database'] = $config->testdatabase;
         }
-        if (!empty($config->testusesafe)) {
-            $configuration['usesafe'] = $config->testusesafe;
-        }
+        $configuration['usesafe'] = 1;
         if (!empty($config->testextendedmode)) {
             $configuration['extendedmode'] = (bool)$config->testextendedmode;
         }
diff --git a/cache/stores/mongodb/tests/mongodb_test.php b/cache/stores/mongodb/tests/mongodb_test.php
new file mode 100644 (file)
index 0000000..b1a3390
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This mongodb 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/>.
+
+/**
+ * MongoDB unit tests.
+ *
+ * If you wish to use these unit tests all you need to do is add the following definition to
+ * your config.php file.
+ *
+ * define('TEST_CACHESTORE_MONGODB_TESTSERVER', 'mongodb://localhost:27017');
+ *
+ * @package    cachestore_mongodb
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/mongodb/lib.php');
+
+/**
+ * MongoDB unit test class.
+ *
+ * @package    cachestore_mongodb
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_mongodb_test extends cachestore_tests {
+    /**
+     * Prepare to run tests.
+     */
+    public function setUp() {
+        if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
+            set_config('testserver', TEST_CACHESTORE_MONGODB_TESTSERVER, 'cachestore_mongodb');
+            $this->resetAfterTest();
+        }
+        parent::setUp();
+    }
+    /**
+     * Returns the MongoDB class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_mongodb';
+    }
+}
\ No newline at end of file
index 8cb42f0..441bd70 100644 (file)
@@ -335,8 +335,9 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
      * @return bool Returns true if the operation was a success, false otherwise.
      */
     public function delete($key) {
+        $result = isset($this->store[$key]);
         unset($this->store[$key]);
-        return true;
+        return $result;
     }
 
     /**
@@ -348,8 +349,10 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
     public function delete_many(array $keys) {
         $count = 0;
         foreach ($keys as $key) {
+            if (isset($this->store[$key])) {
+                $count++;
+            }
             unset($this->store[$key]);
-            $count++;
         }
         return $count;
     }
diff --git a/cache/stores/session/tests/session_test.php b/cache/stores/session/tests/session_test.php
new file mode 100644 (file)
index 0000000..bec0289
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This session 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/>.
+
+/**
+ * Session unit tests
+ *
+ * @package    cachestore_session
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/session/lib.php');
+
+/**
+ * Session unit test class.
+ *
+ * @package    cachestore_session
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_session_test extends cachestore_tests {
+    /**
+     * Returns the session class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_session';
+    }
+}
\ No newline at end of file
index 4ac32de..cc74f74 100644 (file)
@@ -331,8 +331,9 @@ class cachestore_static extends static_data_store implements cache_is_key_aware
      * @return bool Returns true if the operation was a success, false otherwise.
      */
     public function delete($key) {
+        $result = isset($this->store[$key]);
         unset($this->store[$key]);
-        return true;
+        return $result;
     }
 
     /**
@@ -344,8 +345,10 @@ class cachestore_static extends static_data_store implements cache_is_key_aware
     public function delete_many(array $keys) {
         $count = 0;
         foreach ($keys as $key) {
+            if (isset($this->store[$key])) {
+                $count++;
+            }
             unset($this->store[$key]);
-            $count++;
         }
         return $count;
     }
diff --git a/cache/stores/static/tests/static_test.php b/cache/stores/static/tests/static_test.php
new file mode 100644 (file)
index 0000000..b1ebc19
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This static 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/>.
+
+/**
+ * Static unit tests
+ *
+ * @package    cachestore_static
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the necessary evils.
+global $CFG;
+require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
+require_once($CFG->dirroot.'/cache/stores/static/lib.php');
+
+/**
+ * Static unit test class.
+ *
+ * @package    cachestore_static
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_static_test extends cachestore_tests {
+    /**
+     * Returns the static class name
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_static';
+    }
+}
\ No newline at end of file
index a4704dc..301bee1 100644 (file)
@@ -55,6 +55,7 @@ $strinvalidplugin = new lang_string('invalidplugin', 'cache');
 $strunsupportedmode = new lang_string('unsupportedmode', 'cache');
 $struntestable = new lang_string('untestable', 'cache');
 $strtested = new lang_string('tested', 'cache');
+$strnotready = new lang_string('storenotready', 'cache');
 
 foreach (get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin => $path) {
 
@@ -74,6 +75,8 @@ foreach (get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin =>
         $store = $class::initialise_test_instance($application);
         if ($store === false) {
             $applicationtable->data[] = array($plugin, $struntestable, '-', '-', '-', '-');
+        } else if (!$store->is_ready()) {
+            $applicationtable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
         } else {
             $result = array($plugin, $strtested, 0, 0, 0);
             $start = microtime(true);
@@ -110,6 +113,8 @@ foreach (get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin =>
         $store = $class::initialise_test_instance($session);
         if ($store === false) {
             $sessiontable->data[] = array($plugin, $struntestable, '-', '-', '-', '-');
+        } else if (!$store->is_ready()) {
+            $sessiontable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
         } else {
             $result = array($plugin, $strtested, 0, 0, 0);
             $start = microtime(true);
@@ -146,6 +151,8 @@ foreach (get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin =>
         $store = $class::initialise_test_instance($request);
         if ($store === false) {
             $requesttable->data[] = array($plugin, $struntestable, '-', '-', '-', '-');
+        } else if (!$store->is_ready()) {
+            $requesttable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
         } else {
             $result = array($plugin, $strtested, 0, 0, 0);
             $start = microtime(true);
index 7f402d5..e7fe836 100644 (file)
@@ -231,6 +231,23 @@ class cache_phpunit_tests extends advanced_testcase {
         $this->assertEquals('red_ptc_wfc', $result->property1);
         $this->assertEquals('blue_ptc_wfc', $result->property2);
 
+        // Test array of objects
+        $specobject = new cache_phpunit_dummy_object('red', 'blue');
+        $data = new cacheable_object_array(array(
+            clone($specobject),
+            clone($specobject),
+            clone($specobject))
+        );
+        $this->assertTrue($cache->set($key, $data));
+        $result = $cache->get($key);
+        $this->assertInstanceOf('cacheable_object_array', $result);
+        $this->assertCount(3, $data);
+        foreach ($result as $item) {
+            $this->assertInstanceOf('cache_phpunit_dummy_object', $item);
+            $this->assertEquals('red_ptc_wfc', $item->property1);
+            $this->assertEquals('blue_ptc_wfc', $item->property2);
+        }
+
         // Test set many.
         $cache->set_many(array('key1' => 'data1', 'key2' => 'data2'));
         $this->assertEquals('data1', $cache->get('key1'));
@@ -627,6 +644,15 @@ class cache_phpunit_tests extends advanced_testcase {
                 'crazyevent'
             )
         ));
+        $instance->phpunit_add_definition('phpunit/eventpurgetestpersistent', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetestpersistent',
+            'persistent' => true,
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
         $cache = cache::make('phpunit', 'eventpurgetest');
 
         $this->assertTrue($cache->set('testkey1', 'test data 1'));
@@ -640,6 +666,71 @@ class cache_phpunit_tests extends advanced_testcase {
         // Check things have been removed.
         $this->assertFalse($cache->get('testkey1'));
         $this->assertFalse($cache->get('testkey2'));
+
+        // Now test the persistent cache.
+        $cache = cache::make('phpunit', 'eventpurgetestpersistent');
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
+    }
+
+    /**
+     * Tests session cache event purge
+     */
+    public function test_session_event_purge() {
+        $instance = cache_config_phpunittest::instance();
+        $instance->phpunit_add_definition('phpunit/eventpurgetest', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetest',
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
+        $instance->phpunit_add_definition('phpunit/eventpurgetestpersistent', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetestpersistent',
+            'persistent' => true,
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
+        $cache = cache::make('phpunit', 'eventpurgetest');
+
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
+
+        // Now test the persistent cache.
+        $cache = cache::make('phpunit', 'eventpurgetestpersistent');
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
     }
 
     /**
index 471521b..42ce92b 100644 (file)
@@ -46,7 +46,7 @@ class cache_config_phpunittest extends cache_config_writer {
                 case cache_store::MODE_APPLICATION:
                     $properties['overrideclass'] = 'cache_phpunit_application';
                     break;
-                case cache_store::MDOE_SESSION:
+                case cache_store::MODE_SESSION:
                     $properties['overrideclass'] = 'cache_phpunit_session';
                     break;
                 case cache_store::MODE_REQUEST:
diff --git a/cache/tests/fixtures/stores.php b/cache/tests/fixtures/stores.php
new file mode 100644 (file)
index 0000000..2927b4b
--- /dev/null
@@ -0,0 +1,137 @@
+<?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/>.
+
+/**
+ * Cache store test fixtures.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * An abstract class to make writing unit tests for cache stores very easy.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class cachestore_tests extends advanced_testcase {
+
+    /**
+     * Returns the class name for the store.
+     *
+     * @return string
+     */
+    abstract protected function get_class_name();
+
+    /**
+     * Run the unit tests for the store.
+     */
+    public function test_test_instance() {
+        $class = $this->get_class_name();
+        if (!class_exists($class) || !method_exists($class, 'initialise_test_instance') || !$class::are_requirements_met()) {
+            $this->markTestSkipped('Could not test '.$class.'. Requirements are not met.');
+        }
+
+        $modes = $class::get_supported_modes();
+        if ($modes & cache_store::MODE_APPLICATION) {
+            $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+            $instance = $class::initialise_test_instance($definition);
+            if (!$instance) {
+                $this->markTestSkipped('Could not test '.$class.'. No test instance configured for application caches.');
+            } else {
+                $this->run_tests($instance);
+            }
+        }
+        if ($modes & cache_store::MODE_SESSION) {
+            $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+            $instance = $class::initialise_test_instance($definition);
+            if (!$instance) {
+                $this->markTestSkipped('Could not test '.$class.'. No test instance configured for session caches.');
+            } else {
+                $this->run_tests($instance);
+            }
+        }
+        if ($modes & cache_store::MODE_REQUEST) {
+            $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+            $instance = $class::initialise_test_instance($definition);
+            if (!$instance) {
+                $this->markTestSkipped('Could not test '.$class.'. No test instance configured for request caches.');
+            } else {
+                $this->run_tests($instance);
+            }
+        }
+    }
+
+    /**
+     * Test the store for basic functionality.
+     */
+    public function run_tests(cache_store $instance) {
+
+        // Test set.
+        $this->assertTrue($instance->set('test1', 'test1'));
+        $this->assertTrue($instance->set('test2', 'test2'));
+
+        // Test get.
+        $this->assertEquals('test1', $instance->get('test1'));
+        $this->assertEquals('test2', $instance->get('test2'));
+
+        // Test delete.
+        $this->assertTrue($instance->delete('test1'));
+        $this->assertFalse($instance->delete('test3'));
+        $this->assertFalse($instance->get('test1'));
+        $this->assertEquals('test2', $instance->get('test2'));
+        $this->assertTrue($instance->set('test1', 'test1'));
+
+        // Test purge.
+        $this->assertTrue($instance->purge());
+        $this->assertFalse($instance->get('test1'));
+        $this->assertFalse($instance->get('test2'));
+
+        // Test set_many.
+        $outcome = $instance->set_many(array(
+            array('key' => 'many1', 'value' => 'many1'),
+            array('key' => 'many2', 'value' => 'many2'),
+            array('key' => 'many3', 'value' => 'many3'),
+            array('key' => 'many4', 'value' => 'many4'),
+            array('key' => 'many5', 'value' => 'many5')
+        ));
+        $this->assertEquals(5, $outcome);
+        $this->assertEquals('many1', $instance->get('many1'));
+        $this->assertEquals('many5', $instance->get('many5'));
+        $this->assertFalse($instance->get('many6'));
+
+        // Test get_many.
+        $result = $instance->get_many(array('many1', 'many3', 'many5', 'many6'));
+        $this->assertInternalType('array', $result);
+        $this->assertCount(4, $result);
+        $this->assertEquals(array(
+            'many1' => 'many1',
+            'many3' => 'many3',
+            'many5' => 'many5',
+            'many6' => false,
+        ), $result);
+
+        // Test delete_many.
+        $this->assertEquals(3, $instance->delete_many(array('many2', 'many3', 'many4')));
+        $this->assertEquals(2, $instance->delete_many(array('many1', 'many5', 'many6')));
+    }
+}
\ No newline at end of file
index 07265bc..21782aa 100644 (file)
@@ -115,6 +115,14 @@ $PAGE->set_button(calendar_preferences_button($course));
 $renderer = $PAGE->get_renderer('core_calendar');
 
 echo $OUTPUT->header();
+
+// Filter subscriptions which user can't edit.
+foreach($subscriptions as $subscription) {
+    if (!calendar_can_edit_subscription($subscription)) {
+        unset($subscriptions[$subscription->id]);
+    }
+}
+
 // Display a table of subscriptions.
 echo $renderer->subscription_details($courseid, $subscriptions, $importresults);
 // Display the add subscription form.
index 40f8577..c9ce761 100644 (file)
@@ -468,6 +468,9 @@ $CFG->admin = 'admin';
 // Prevent theme caching
 // $CFG->themerev = -1; // NOT FOR PRODUCTION SERVERS!
 //
+// Prevent JS caching
+// $CFG->jsrev = -1; // NOT FOR PRODUCTION SERVERS!
+//
 // Prevent core_string_manager on-disk cache
 // $CFG->langstringcache = false; // NOT FOR PRODUCTION SERVERS!
 //
index 172d1d9..94bc371 100644 (file)
@@ -56,6 +56,9 @@ M.course.format.swap_sections = function(Y, node1, node2) {
 M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
     var CSS = {
         SECTIONNAME : 'sectionname'
+    },
+    SELECTORS = {
+        SECTIONLEFTSIDE : '.left .section-handle img'
     };
 
     if (response.action == 'move') {
@@ -65,9 +68,20 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             sectionto = sectionfrom;
             sectionfrom = temp;
         }
-        // Update titles in all affected sections.
+
+        // Update titles and move icons in all affected sections.
+        var ele, str, stridx, newstr;
+
         for (var i = sectionfrom; i <= sectionto; i++) {
+            // Update section title.
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+            // Update move icon.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
+            str = ele.getAttribute('alt');
+            stridx = str.lastIndexOf(' ');
+            newstr = str.substr(0, stridx +1) + i;
+            ele.setAttribute('alt', newstr);
+            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
         }
     }
 }
index 6433702..1c36994 100644 (file)
@@ -56,6 +56,9 @@ M.course.format.swap_sections = function(Y, node1, node2) {
 M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
     var CSS = {
         SECTIONNAME : 'sectionname'
+    },
+    SELECTORS = {
+        SECTIONLEFTSIDE : '.left .section-handle img'
     };
 
     if (response.action == 'move') {
@@ -65,9 +68,20 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             sectionto = sectionfrom;
             sectionfrom = temp;
         }
-        // Update titles in all affected sections.
+
+        // Update titles and move icons in all affected sections.
+        var ele, str, stridx, newstr;
+
         for (var i = sectionfrom; i <= sectionto; i++) {
+            // Update section title.
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+            // Update move icon.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
+            str = ele.getAttribute('alt');
+            stridx = str.lastIndexOf(' ');
+            newstr = str.substr(0, stridx +1) + i;
+            ele.setAttribute('alt', newstr);
+            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
         }
     }
 }
index 177e922..0110db1 100644 (file)
@@ -373,7 +373,12 @@ if ($canmanage) {
 
     // Print button for creating new categories.
     $url = new moodle_url('/course/editcategory.php', array('parent' => $id));
-    echo $OUTPUT->single_button($url, get_string('addsubcategory'), 'get');
+    if ($id) {
+        $title = get_string('addsubcategory');
+    } else {
+        $title = get_string('addnewcategory');
+    }
+    echo $OUTPUT->single_button($url, $title, 'get');
     echo $OUTPUT->container_end();
 }
 
index e1e0c17..53992db 100644 (file)
@@ -138,14 +138,17 @@ if (isguestuser() or !confirm_sesskey()) {
     print_error('error');
 }
 
-// Now change state
+// Set up completion object and check it is enabled.
 $completion = new completion_info($course);
 if (!$completion->is_enabled()) {
     throw new moodle_exception('completionnotenabled', 'completion');
-} elseif (!$completion->is_tracked_user($USER->id)) {
-    throw new moodle_exception('nottracked', 'completion');
 }
 
+// NOTE: All users are allowed to toggle their completion state, including
+// users for whom completion information is not directly tracked. (I.e. even
+// if you are a teacher, or admin who is not enrolled, you can still toggle
+// your own completion state. You just don't appear on the reports.)
+
 // Check completion state is manual
 if($cm->completion != COMPLETION_TRACKING_MANUAL) {
     error_or_ajax('cannotmanualctrack', $fromajax);
index b312c9e..b7b68b2 100644 (file)
@@ -96,7 +96,7 @@ class filter_emoticon extends moodle_text_filter {
      */
     protected function load_global_config() {
         if (is_null(self::$globalconfig)) {
-            self::$globalconfig = get_config('filter_emoticon');
+            self::$globalconfig = get_config(get_class($this));
         }
     }
 
diff --git a/filter/emoticon/tests/filter_test.php b/filter/emoticon/tests/filter_test.php
new file mode 100644 (file)
index 0000000..ddd73e1
--- /dev/null
@@ -0,0 +1,74 @@
+<?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/>.
+
+/**
+ * Skype icons filter phpunit tests
+ *
+ * @package    filter_emoticon
+ * @category   test
+ * @copyright  2013 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/emoticon/filter.php'); // Include the code to test.
+
+/**
+ * Skype icons filter testcase.
+ */
+class filter_emoticon_testcase extends advanced_testcase {
+
+    /**
+     * Verify configured target formats are observed. Just that.
+     */
+    public function test_filter_emoticon_formats() {
+
+        $this->resetAfterTest(true); // We are modifying the config.
+
+        $filter = new testable_filter_emoticon();
+
+        // Verify texts not matching target formats aren't filtered.
+        $expected = '(grr)';
+        $options = array('originalformat' => FORMAT_MOODLE); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
+        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+
+        $options = array('originalformat' => FORMAT_MARKDOWN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
+        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+
+        $options = array('originalformat' => FORMAT_PLAIN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
+        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+
+        // And texts matching target formats are filtered.
+        $expected = '<img class="emoticon" alt="angry" title="angry"'.
+                    ' src="http://www.example.com/moodle/theme/image.php/_s/standard/core/1/s/angry" />';
+        $options = array('originalformat' => FORMAT_HTML); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
+        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+    }
+}
+
+/**
+ * Subclass for easier testing.
+ */
+class testable_filter_emoticon extends filter_emoticon {
+    public function __construct() {
+        // Use this context for filtering.
+        $this->context = context_system::instance();
+        // Define FORMAT_HTML as only one filtering in DB.
+        set_config('formats', implode(',', array(FORMAT_HTML)), get_class($this));
+    }
+}
index c10a858..6e21e46 100644 (file)
@@ -30,7 +30,7 @@ require_once('import_outcomes_form.php');
 
 $courseid = optional_param('courseid', 0, PARAM_INT);
 $action   = optional_param('action', '', PARAM_ALPHA);
-$scope    = optional_param('scope', 'global', PARAM_ALPHA);
+$scope    = optional_param('scope', 'custom', PARAM_ALPHA);
 
 $PAGE->set_url('/grade/edit/outcome/import.php', array('courseid' => $courseid));
 
index 54af548..8d5f090 100644 (file)
@@ -741,9 +741,11 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
     this.editfeedback = ajax.showquickfeedback;
     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
 
-    for(var i = 0; i < this.report.grades.length; i++) {
-        if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
-            this.oldgrade = this.report.grades[i]['grade'];
+    if (this.report.grades) {
+        for (var i = 0; i < this.report.grades.length; i++) {
+            if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
+                this.oldgrade = this.report.grades[i]['grade'];
+            }
         }
     }
 
index 53f68e2..3d36a0a 100644 (file)
@@ -747,6 +747,8 @@ $string['navshowallcourses'] = 'Show all courses';
 $string['navshowcategories'] = 'Show course categories';
 $string['navshowmycoursecategories'] = 'Show my course categories';
 $string['navshowmycoursecategories_help'] = 'If enabled courses in the users my courses branch will be shown in categories.';
+$string['navsortmycoursessort'] = 'Sort my courses';
+$string['navsortmycoursessort_help'] = 'Determines how the courses displayed within the My courses branch on the navigation will be sorted. By default sortorder it used.';
 $string['neverdeleteruns'] = 'Never delete runs';
 $string['nobookmarksforuser'] = 'You do not have any bookmarks.';
 $string['nodatabase'] = 'No database';
@@ -971,6 +973,10 @@ $string['sitesectionhelp'] = 'If selected, a topic section will be displayed on
 $string['slasharguments'] = 'Use slash arguments';
 $string['smartpix'] = 'Smart pix search';
 $string['soaprecommended'] = 'Installing the optional soap extension is useful for web services and some contrib modules.';
+$string['sort_fullname'] = 'Fullname';
+$string['sort_idnumber'] = 'ID Number';
+$string['sort_shortname'] = 'Shortname';
+$string['sort_sortorder'] = 'Sort order';
 $string['spellengine'] = 'Spell engine';
 $string['spelllanguagelist'] = 'Spell language list';
 $string['splrequired'] = 'The SPL PHP extension is now required by Moodle.';
index 64d772b..4d6b995 100644 (file)
@@ -105,6 +105,9 @@ $string['storenamealreadyused'] = 'You must choose a unique name for this store.
 $string['storenameinvalid'] = 'Invalid store name. You can only use a-z A-Z 0-9 -_ and spaces.';
 $string['storeperformance'] = 'Cache store performance reporting - {$a} unique requests per operation.';
 $string['storeready'] = 'Ready';
+$string['storenotready'] = 'Store not ready';
+$string['storerequiresattention'] = 'Requires attention.';
+$string['storerequiresattention_help'] = 'This store instance is not ready to be used but has mappings. Fixing this issue will improve performance on your system. Please check that the store backend is ready to be used and that any PHP requirements are met.';
 $string['storeresults_application'] = 'Store requests when used as an application cache.';
 $string['storeresults_request'] = 'Store requests when used as a request cache.';
 $string['storeresults_session'] = 'Store requests when used as a session cache.';
index 540e9d1..ebb617a 100644 (file)
@@ -90,6 +90,7 @@ affected will continue to work in all existing quizzes until you remove them fro
 $string['cwrqpfsnoprob'] = 'No question categories in your site are affected by the \'Random questions selecting questions from sub categories\' issue.';
 $string['defaultfor'] = 'Default for {$a}';
 $string['defaultinfofor'] = 'The default category for questions shared in context \'{$a}\'.';
+$string['defaultmarkmustbepositive'] = 'The default mark must be positive.';
 $string['deletebehaviourareyousure'] = 'Delete behaviour {$a}: are you sure?';
 $string['deletebehaviourareyousuremessage'] = 'You are about to completely delete the question behaviour {$a}. This will completely delete everything in the database associated with this question behaviour. Are you SURE you want to continue?';
 $string['deletecoursecategorywithquestions'] = 'There are questions in the question bank associated with this course category. If you proceed, they will be deleted. You may wish to move them first, using the question bank interface.';
index 25713be..5ee707f 100644 (file)
@@ -158,7 +158,7 @@ $string['nofilesattached'] = 'No files attached';
 $string['nofilesavailable'] = 'No files available';
 $string['nomorefiles'] = 'No more attachments allowed';
 $string['nopathselected'] = 'No destination path select yet (double click tree node to select)';
-$string['nopermissiontoaccess'] = 'No permission to access this repository';
+$string['nopermissiontoaccess'] = 'No permission to access this repository.';
 $string['noresult'] = 'No search result';
 $string['norepositoriesavailable'] = 'Sorry, none of your current repositories can return files in the required format.';
 $string['norepositoriesexternalavailable'] = 'Sorry, none of your current repositories can return external files.';
index fe4dc93..1782445 100644 (file)
@@ -6668,7 +6668,7 @@ function db_replace($search, $replace) {
     global $DB, $CFG, $OUTPUT;
 
     // TODO: this is horrible hack, we should do whitelisting and each plugin should be responsible for proper replacing...
-    $skiptables = array('config', 'config_plugins', 'config_log', 'upgrade_log',
+    $skiptables = array('config', 'config_plugins', 'config_log', 'upgrade_log', 'log',
                         'filter_config', 'sessions', 'events_queue', 'repository_instance_config',
                         'block_instances', '');
 
index d703a77..fb79e35 100644 (file)
@@ -59,21 +59,41 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      * @param string $path
      * @return string
      */
-    protected function locatePath($path) {
-        $startUrl = rtrim($this->getMinkParameter('base_url'), '/') . '/';
-        return 0 !== strpos($path, 'http') ? $startUrl . ltrim($path, '/') : $path;
+    protected function locate_path($path) {
+        $starturl = rtrim($this->getMinkParameter('base_url'), '/') . '/';
+        return 0 !== strpos($path, 'http') ? $starturl . ltrim($path, '/') : $path;
     }
 
     /**
-     * Adapter to Behat\Mink\Element\Element::find() using the spin() method.
+     * Returns the first matching element.
      *
      * @link http://mink.behat.org/#traverse-the-page-selectors
-     * @param Exception $exception Otherwise we throw expcetion with generic info
      * @param string $selector The selector type (css, xpath, named...)
      * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
+     * @param Exception $exception Otherwise we throw exception with generic info
+     * @param NodeElement $node Spins around certain DOM node instead of the whole page
      * @return NodeElement
      */
-    protected function find($selector, $locator, $exception = false) {
+    protected function find($selector, $locator, $exception = false, $node = false) {
+
+        // Returns the first match.
+        $items = $this->find_all($selector, $locator, $exception, $node);
+        return count($items) ? reset($items) : null;
+    }
+
+    /**
+     * Returns all matching elements.
+     *
+     * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method.
+     *
+     * @link http://mink.behat.org/#traverse-the-page-selectors
+     * @param string $selector The selector type (css, xpath, named...)
+     * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
+     * @param Exception $exception Otherwise we throw expcetion with generic info
+     * @param NodeElement $node Spins around certain DOM node instead of the whole page
+     * @return array NodeElements list
+     */
+    protected function find_all($selector, $locator, $exception = false, $node = false) {
 
         // Generic info.
         if (!$exception) {
@@ -90,15 +110,44 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
         }
 
+        $params = array('selector' => $selector, 'locator' => $locator);
+        // Pushing $node if required.
+        if ($node) {
+            $params['node'] = $node;
+        }
+
         // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
         return $this->spin(
             function($context, $args) {
-                return $context->getSession()->getPage()->find($args[0], $args[1]);
+
+                // If no DOM node provided look in all the page.
+                if (empty($args['node'])) {
+                    return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
+                }
+
+                // For nodes contained in other nodes we can not use the basic named selectors
+                // as they include unions and they would look for matches in the DOM root.
+                $elementxpath = $context->getSession()->getSelectorsHandler()->selectorToXpath($args['selector'], $args['locator']);
+
+                // Split the xpath in unions and prefix them with the container xpath.
+                $unions = explode('|', $elementxpath);
+                foreach ($unions as $key => $union) {
+                    $union = trim($union);
+
+                    // We are in the container node.
+                    if (strpos($union, '.') === 0) {
+                        $union = substr($union, 1);
+                    }
+                    $unions[$key] = $args['node']->getXpath() . $union;
+                }
+
+                // We can not use usual Element::find() as it prefixes with DOM root.
+                return $context->getSession()->getDriver()->find(implode('|', $unions));
             },
-            array($selector, $locator),
+            $params,
             self::TIMEOUT,
             $exception
-       );
+        );
     }
 
     /**
@@ -119,7 +168,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      *
      * @link http://mink.behat.org/#named-selectors
      * @throws coding_exception
-     * @param string $method The name of the called method
+     * @param string $name The name of the called method
      * @param mixed $arguments
      * @return NodeElement
      */
@@ -193,7 +242,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 if ($return = $lambda($this, $args)) {
                     return $return;
                 }
-            } catch(Exception $e) {
+            } catch (Exception $e) {
 
                 // We would use the first closure exception if no exception has been provided.
                 if (!$exception) {
@@ -216,4 +265,120 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         throw $exception;
     }
 
+    /**
+     * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
+     *
+     * Use behat_base::get_text_selector_node() for text-based selectors.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $selectortype
+     * @param string $element
+     * @return NodeElement
+     */
+    protected function get_selected_node($selectortype, $element) {
+
+        // Getting Mink selector and locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Returns the NodeElement.
+        return $this->find($selector, $locator);
+    }
+
+    /**
+     * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $selectortype
+     * @param string $element
+     * @return NodeElement
+     */
+    protected function get_text_selector_node($selectortype, $element) {
+
+        // Getting Mink selector and locator.
+        list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
+
+        // Returns the NodeElement.
+        return $this->find($selector, $locator);
+    }
+
+    /**
+     * Gets the requested element inside the specified container.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param mixed $selectortype The element selector type.
+     * @param mixed $element The element locator.
+     * @param mixed $containerselectortype The container selector type.
+     * @param mixed $containerelement The container locator.
+     * @return NodeElement
+     */
+    protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) {
+
+        // Gets the container, it will always be text based.
+        $containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
+
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Specific exception giving info about where can't we find the element.
+        $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
+        $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
+
+        // Looks for the requested node inside the container node.
+        return $this->find($selector, $locator, $exception, $containernode);
+    }
+
+    /**
+     * Transforms from step definition's argument style to Mink format.
+     *
+     * Mink has 3 different selectors css, xpath and named, where named
+     * selectors includes link, button, field... to simplify and group multiple
+     * steps in one we use the same interface, considering all link, buttons...
+     * at the same level as css selectors and xpath; this method makes the
+     * conversion from the arguments received by the steps to the selectors and locators
+     * required to interact with Mink.
+     *
+     * @throws ExpectationException
+     * @param string $selectortype It can be css, xpath or any of the named selectors.
+     * @param string $element The locator (or string) we are looking for.
+     * @return array Contains the selector and the locator expected by Mink.
+     */
+    protected function transform_selector($selectortype, $element) {
+
+        // Here we don't know if a $allowedtextselector is used.
+        if (!isset(behat_command::$allowedselectors[$selectortype])) {
+            throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
+        }
+
+        // CSS and XPath selectors locator is one single argument.
+        if ($selectortype == 'css_element' || $selectortype == 'xpath_element') {
+            $selector = str_replace('_element', '', $selectortype);
+            $locator = $element;
+        } else {
+            // Named selectors uses arrays as locators including the type of named selector.
+            $locator = array($selectortype, $this->getSession()->getSelectorsHandler()->xpathLiteral($element));
+            $selector = 'named';
+        }
+
+        return array($selector, $locator);
+    }
+
+    /**
+     * Transforms from step definition's argument style to Mink format.
+     *
+     * Delegates all the process to behat_base::transform_selector() checking
+     * the provided $selectortype.
+     *
+     * @throws ExpectationException
+     * @param string $selectortype It can be css, xpath or any of the named selectors.
+     * @param string $element The locator (or string) we are looking for.
+     * @return array Contains the selector and the locator expected by Mink.
+     */
+    protected function transform_text_selector($selectortype, $element) {
+
+        if ($selectortype != 'css_element' && $selectortype != 'xpath_element') {
+            throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession());
+        }
+
+        return $this->transform_selector($selectortype, $element);
+    }
+
 }
index 8bdb123..e399996 100644 (file)
@@ -42,6 +42,34 @@ class behat_command {
      */
     const DOCS_URL = 'http://docs.moodle.org/dev/Acceptance_testing';
 
+    /**
+     * @var Allowed types when using text selectors arguments.
+     */
+    public static $allowedtextselectors = array(
+        'css_element' => 'css_element',
+        'xpath_element' => 'xpath_element'
+    );
+
+    /**
+     * @var Allowed types when using selector arguments.
+     */
+    public static $allowedselectors = array(
+        'link' => 'link',
+        'button' => 'button',
+        'link_or_button' => 'link_or_button',
+        'select' => 'select',
+        'checkbox' => 'checkbox',
+        'radio' => 'radio',
+        'file' => 'file',
+        'optgroup' => 'optgroup',
+        'option' => 'option',
+        'table' => 'table',
+        'field' => 'field',
+        'fieldset' => 'fieldset',
+        'css_element' => 'css_element',
+        'xpath_element' => 'xpath_element'
+    );
+
     /**
      * Ensures the behat dir exists in moodledata
      * @return string Full path
@@ -101,9 +129,9 @@ class behat_command {
      * the behat help command to ensure it works as expected
      *
      * @param  bool $checkphp Extra check for the PHP version
-     * @return void
+     * @return int Error code or 0 if all ok
      */
-    public static function check_behat_setup($checkphp = false) {
+    public static function behat_setup_problem($checkphp = false) {
         global $CFG;
 
         // We don't check the PHP version if $CFG->behat_switchcompletely has been enabled.
@@ -112,31 +140,48 @@ class behat_command {
             behat_error(BEHAT_EXITCODE_REQUIREMENT, 'PHP 5.4 is required. See config-dist.php for possible alternatives');
         }
 
+        $clibehaterrorstr = "Behat dependencies not installed. Ensure you ran the composer installer. " . self::DOCS_URL . "#Installation\n";
+
         // Moodle setting.
         if (!self::are_behat_dependencies_installed()) {
 
-            $msg = get_string('wrongbehatsetup', 'tool_behat');
 
             // With HTML.
-            $docslink = self::DOCS_URL . '#Installation';
             if (!CLI_SCRIPT) {
+
+                $msg = get_string('wrongbehatsetup', 'tool_behat');
+                $docslink = self::DOCS_URL . '#Installation';
                 $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
+                $msg .= get_string('moreinfoin', 'tool_behat', $docslink);
+            } else {
+                $msg = $clibehaterrorstr;
             }
-            $msg .= '. ' . get_string('moreinfoin', 'tool_behat', $docslink);
-            notice($msg);
+
+            self::output_msg($msg);
+            return BEHAT_EXITCODE_COMPOSER;
         }
 
         // Behat test command.
         list($output, $code) = self::run(' --help');
 
         if ($code != 0) {
-            notice(get_string('wrongbehatsetup', 'tool_behat'));
+            // Returning composer error code to avoid conflicts with behat and moodle error codes.
+            if (!CLI_SCRIPT) {
+                $msg = get_string('wrongbehatsetup', 'tool_behat');
+            } else {
+                $msg = $clibehaterrorstr;
+            }
+            self::output_msg($msg);
+            return BEHAT_EXITCODE_COMPOSER;
         }
 
-        // Checking behat dataroot existence otherwise notice about admin/tool/behat/cli/util.php.
+        // Checking behat dataroot existence otherwise echo about admin/tool/behat/cli/util.php.
         if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
-            notice(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/util.php'));
+            self::output_msg(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/util.php'));
+            return BEHAT_EXITCODE_CONFIG;
         }
+
+        return 0;
     }
 
     /**
@@ -150,4 +195,22 @@ class behat_command {
         return true;
     }
 
+    /**
+     * Outputs a message.
+     *
+     * Used in CLI + web UI methods. Stops the
+     * execution in web.
+     *
+     * @param string $msg
+     * @return void
+     */
+    protected static function output_msg($msg) {
+
+        if (!CLI_SCRIPT) {
+            notice($msg);
+        } else {
+            echo $msg;
+        }
+    }
+
 }
index 5fe4aa3..05cd1db 100644 (file)
@@ -71,6 +71,7 @@ class behat_util extends testing_util {
         $options['adminuser'] = 'admin';
         $options['adminpass'] = 'admin';
         $options['fullname'] = 'Acceptance test site';
+        $options['shortame'] = 'Acceptance test site';
 
         install_cli_database($options, false);
 
@@ -176,7 +177,9 @@ class behat_util extends testing_util {
         }
 
         // Checks the behat set up and the PHP version.
-        behat_command::check_behat_setup(true);
+        if ($errorcode = behat_command::behat_setup_problem(true)) {
+            exit($errorcode);
+        }
 
         // Check that test environment is correctly set up.
         self::test_environment_problem();
@@ -196,6 +199,26 @@ class behat_util extends testing_util {
         }
     }
 
+    /**
+     * Returns the status of the behat test environment
+     *
+     * @return int Error code
+     */
+    public static function get_behat_status() {
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        // Checks the behat set up and the PHP version, returning an error code if something went wrong.
+        if ($errorcode = behat_command::behat_setup_problem(true)) {
+            return $errorcode;
+        }
+
+        // Check that test environment is correctly set up, stops execution.
+        self::test_environment_problem();
+    }
+
     /**
      * Disables test mode
      * @throws coding_exception
index 04e0b3c..22aea0b 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index 7c08d73..58f8875 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -41,7 +40,14 @@ use Behat\Mink\Session as Session,
  */
 class behat_form_field {
 
+    /**
+     * @var Session Behat session.
+     */
     protected $session;
+
+    /**
+     * @var NodeElement The field DOM node to interact with.
+     */
     protected $fieldnode;
 
     /**
index dda2deb..b1a747a 100644 (file)
@@ -32,6 +32,7 @@ define('BEHAT_EXITCODE_REQUIREMENT', 251);
 define('BEHAT_EXITCODE_PERMISSIONS', 252);
 define('BEHAT_EXITCODE_REINSTALL', 253);
 define('BEHAT_EXITCODE_INSTALL', 254);
+define('BEHAT_EXITCODE_COMPOSER', 255);
 
 /**
  * Exits with an error code
index 1602999..2ba5735 100644 (file)
       </KEYS>
       <INDEXES>
         <INDEX NAME="action" UNIQUE="false" FIELDS="action" COMMENT="insert/update/delete"/>
+        <INDEX NAME="timemodified" UNIQUE="false" FIELDS="timemodified"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="grade_import_newitem" COMMENT="temporary table for storing new grade_item names from grade import">
index 09ea47d..8e47e90 100644 (file)
@@ -1670,5 +1670,28 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013022600.00);
     }
 
+    // Add index to field "timemodified" for grade_grades_history table.
+    if ($oldversion < 2013030400.00) {
+        $table = new xmldb_table('grade_grades_history');
+        $field = new xmldb_field('timemodified');
+
+        if ($dbman->field_exists($table, $field)) {
+            $index = new xmldb_index('timemodified', XMLDB_INDEX_NOTUNIQUE, array('timemodified'));
+            if (!$dbman->index_exists($table, $index)) {
+                $dbman->add_index($table, $index);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013030400.00);
+    }
+
+    if ($oldversion < 2013030400.02) {
+        // Cleanup qformat blackboard settings.
+        unset_all_config_for_plugin('qformat_blackboard');
+
+        upgrade_main_savepoint(true, 2013030400.02);
+    }
+
     return true;
 }
index 3e54990..fc69751 100644 (file)
@@ -3078,6 +3078,12 @@ class curl {
         }
         curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
 
+        // Bypass proxy (for this request only) if required.
+        if (!empty($this->options['CURLOPT_URL']) &&
+                is_proxybypass($this->options['CURLOPT_URL'])) {
+            unset($this->options['CURLOPT_PROXY']);
+        }
+
         if ($this->debug){
             echo '<h1>Options</h1>';
             var_dump($this->options);
@@ -3436,6 +3442,45 @@ class curl {
     public function get_errno() {
         return $this->errno;
     }
+
+    /**
+     * When using a proxy, an additional HTTP response code may appear at
+     * the start of the header. For example, when using https over a proxy
+     * there may be 'HTTP/1.0 200 Connection Established'. Other codes are
+     * also possible and some may come with their own headers.
+     *
+     * If using the return value containing all headers, this function can be
+     * called to remove unwanted doubles.
+     *
+     * Note that it is not possible to distinguish this situation from valid
+     * data unless you know the actual response part (below the headers)
+     * will not be included in this string, or else will not 'look like' HTTP
+     * headers. As a result it is not safe to call this function for general
+     * data.
+     *
+     * @param string $input Input HTTP response
+     * @return string HTTP response with additional headers stripped if any
+     */
+    public static function strip_double_headers($input) {
+        // I have tried to make this regular expression as specific as possible
+        // to avoid any case where it does weird stuff if you happen to put
+        // HTTP/1.1 200 at the start of any line in your RSS file. This should
+        // also make it faster because it can abandon regex processing as soon
+        // as it hits something that doesn't look like an http header. The
+        // header definition is taken from RFC 822, except I didn't support
+        // folding which is never used in practice.
+        $crlf = "\r\n";
+        return preg_replace(
+                // HTTP version and status code (ignore value of code).
+                '~^HTTP/1\..*' . $crlf .
+                // Header name: character between 33 and 126 decimal, except colon.
+                // Colon. Header value: any character except \r and \n. CRLF.
+                '(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
+                // Headers are terminated by another CRLF (blank line).
+                $crlf .
+                // Second HTTP status code, this time must be 200.
+                '(HTTP/1.[01] 200 )~', '$1', $input);
+    }
 }
 
 /**
index 8a40431..acbd0cb 100644 (file)
@@ -334,6 +334,9 @@ class file_storage {
         } else if ($mode === 'thumb') {
             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
 
+        } else if ($mode === 'bigthumb') {
+            $data = generate_image_thumbnail($tmpfilepath, 250, 250);
+
         } else {
             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
         }
index 19080d9..917ad8c 100644 (file)
@@ -254,7 +254,7 @@ M.form_filemanager.init = function(Y, options) {
 
             this.msg_dlg.set('headerContent', header);
             this.msg_dlg_node.removeClass('fp-msg-info').removeClass('fp-msg-error').addClass('fp-msg-'+type)
-            this.msg_dlg_node.one('.fp-msg-text').setContent(msg);
+            this.msg_dlg_node.one('.fp-msg-text').setContent(Y.Escape.html(msg));
             this.msg_dlg.show();
         },
         is_disabled: function() {
@@ -325,7 +325,7 @@ M.form_filemanager.init = function(Y, options) {
                     }
                     this.mkdir_dialog.show();
                     Y.one('#fm-newname-'+scope.client_id).focus();
-                    Y.all('#fm-curpath-'+scope.client_id).setContent(this.currentpath)
+                    Y.all('#fm-curpath-'+scope.client_id).setContent(Y.Escape.html(this.currentpath))
                 }, this);
             } else {
                 this.filemanager.addClass('fm-nomkdir');
@@ -412,7 +412,7 @@ M.form_filemanager.init = function(Y, options) {
                     } else {
                         el.addClass('odd');
                     }
-                    el.one('.fp-path-folder-name').setContent(p[i].name).
+                    el.one('.fp-path-folder-name').setContent(Y.Escape.html(p[i].name)).
                         on('click', function(e, path) {
                             e.preventDefault();
                             if (!this.is_disabled()) {
@@ -602,7 +602,7 @@ M.form_filemanager.init = function(Y, options) {
             for (var i in licenses) {
                 var option = Y.Node.create('<option/>').
                     set('value', licenses[i].shortname).
-                    setContent(licenses[i].fullname);
+                    setContent(Y.Escape.html(licenses[i].fullname));
                 node.appendChild(option)
             }
         },
@@ -621,7 +621,7 @@ M.form_filemanager.init = function(Y, options) {
             node.setContent('');
             for (var i in list) {
                 node.appendChild(Y.Node.create('<option/>').
-                    set('value', list[i]).setContent(list[i]))
+                    set('value', list[i]).setContent(Y.Escape.html(list[i])));
             }
         },
         update_file: function(confirmed) {
@@ -923,7 +923,7 @@ M.form_filemanager.init = function(Y, options) {
                 if (selectnode.one('.fp-'+attrs[i])) {
                     var value = (node[attrs[i]+'_f']) ? node[attrs[i]+'_f'] : (node[attrs[i]] ? node[attrs[i]] : '');
                     selectnode.one('.fp-'+attrs[i]).addClassIf('fp-unknown', ''+value == '')
-                        .one('.fp-value').setContent(value);
+                        .one('.fp-value').setContent(Y.Escape.html(value));
                 }
             }
             // display thumbnail
@@ -948,7 +948,7 @@ M.form_filemanager.init = function(Y, options) {
                             selectnode.one('.fp-original').removeClass('fp-loading');
                             if (obj.original) {
                                 node.original = obj.original;
-                                selectnode.one('.fp-original .fp-value').setContent(node.original);
+                                selectnode.one('.fp-original .fp-value').setContent(Y.Escape.html(node.original));
                             } else {
                                 selectnode.one('.fp-original .fp-value').setContent(M.str.repository.unknownsource);
                             }
@@ -976,7 +976,7 @@ M.form_filemanager.init = function(Y, options) {
                                 for (var i in obj.references) {
                                     node.reflist += '<li>'+obj.references[i]+'</li>';
                                 }
-                                selectnode.one('.fp-reflist .fp-value').setContent(node.reflist);
+                                selectnode.one('.fp-reflist .fp-value').setContent(Y.Escape.html(node.reflist));
                             } else {
                                 selectnode.one('.fp-reflist .fp-value').setContent('');
                             }
index 92c16a2..c84c9b8 100644 (file)
@@ -1036,6 +1036,11 @@ abstract class moodleform {
                 }else {
                     $realelementname = $elementname."[$i]";
                 }
+                // This logic to calculate the element id is the same as in
+                // HTML_QuickForm_element::_generateId(). There was no way to
+                // avoid this duplication.
+                $realelementid = 'id_' . str_replace(array('qf_', '[', ']'), array('', '_', ''), $realelementname);
+                $realelementid = clean_param($realelementid, PARAM_ALPHANUMEXT);
                 foreach ($elementoptions as  $option => $params){
 
                     switch ($option){
@@ -1069,8 +1074,13 @@ abstract class moodleform {
                                 $mform->setType($elementname, $params);
                             }
                             break;
+
                         case 'expanded' :
-                            $mform->setExpanded($realelementname, $params);
+                            $mform->setExpanded($realelementid, $params);
+                            break;
+
+                        case 'advanced' :
+                            $mform->setAdvanced($realelementname, $params);
                             break;
                     }
                 }
@@ -1364,23 +1374,23 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      * Use this method to indicate that the fieldset should be shown as expanded.
      * The method is applicable to header elements only.
      *
-     * @param string $headerName header element name
+     * @param string $headerid header element id
      * @param boolean $expanded default true sets the element to expanded. False makes the element collapsed.
      */
-    function setExpanded($headerName, $expanded=true){
-        if ($this->getElementType('mform_isexpanded_'.$headerName)===false) {
+    function setExpanded($headerid, $expanded=true){
+        if ($this->getElementType('mform_isexpanded_' . $headerid)===false) {
             // see if we the form has been submitted already
-            $formexpanded = optional_param('mform_isexpanded_'.$headerName, -1, PARAM_INT);
+            $formexpanded = optional_param('mform_isexpanded_' . $headerid, -1, PARAM_INT);
             if (!$expanded && $formexpanded != -1) {
                 // override expanded state with the form variable
                 $expanded = $formexpanded;
             }
             // create the form element for storing expanded state
-            $this->addElement('hidden', 'mform_isexpanded_'.$headerName);
-            $this->setType('mform_isexpanded_'.$headerName, PARAM_INT);
-            $this->setConstant('mform_isexpanded_' . $headerName, (int)$expanded);
+            $this->addElement('hidden', 'mform_isexpanded_' . $headerid);
+            $this->setType('mform_isexpanded_' . $headerid, PARAM_INT);
+            $this->setConstant('mform_isexpanded_' . $headerid, (int)$expanded);
         }
-        $this->_collapsibleElements[$headerName] = !$expanded;
+        $this->_collapsibleElements[$headerid] = !$expanded;
     }
 
     /**
@@ -1390,19 +1400,19 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      * @param string $headerName header element name.
      * @param boolean $showmore default false sets the advanced elements to be hidden.
      */
-    function addAdvancedStatusElement($headerName, $showmore=false){
+    function addAdvancedStatusElement($headerid, $showmore=false){
         // Add extra hidden element to store advanced items state for each section.
-        if ($this->getElementType('mform_showmore_' . $headerName) === false) {
+        if ($this->getElementType('mform_showmore_' . $headerid) === false) {
             // See if we the form has been submitted already.
-            $formshowmore = optional_param('mform_showmore_' . $headerName, -1, PARAM_INT);
+            $formshowmore = optional_param('mform_showmore_' . $headerid, -1, PARAM_INT);
             if (!$showmore && $formshowmore != -1) {
                 // Override showmore state with the form variable.
                 $showmore = $formshowmore;
             }
             // Create the form element for storing advanced items state.
-            $this->addElement('hidden', 'mform_showmore_' . $headerName);
-            $this->setType('mform_showmore_' . $headerName, PARAM_INT);
-            $this->setConstant('mform_showmore_' . $headerName, (int)$showmore);
+            $this->addElement('hidden', 'mform_showmore_' . $headerid);
+            $this->setType('mform_showmore_' . $headerid, PARAM_INT);
+            $this->setConstant('mform_showmore_' . $headerid, (int)$showmore);
         }
     }
 
@@ -1483,9 +1493,10 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
 
                 // if closing header and any contained element was advanced then mark it as advanced
                 if ($element->getType()=='header' || in_array($element->getName(), $stopFields)){
-                    if ($anyAdvanced && !is_null($lastHeader)){
+                    if ($anyAdvanced && !is_null($lastHeader)) {
+                        $lastHeader->_generateId();
                         $this->setAdvanced($lastHeader->getName());
-                        $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
+                        $this->addAdvancedStatusElement($lastHeader->getAttribute('id'), $anyError);
                     }
                     $lastHeaderAdvanced = false;
                     unset($lastHeader);
@@ -1509,7 +1520,8 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
             // the last header may not be closed yet...
             if ($anyAdvanced && !is_null($lastHeader)){
                 $this->setAdvanced($lastHeader->getName());
-                $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
+                $lastHeader->_generateId();
+                $this->addAdvancedStatusElement($lastHeader->getAttribute('id'), $anyError);
             }
             $renderer->setAdvancedElements($this->_advancedElements);
         }
@@ -1534,13 +1546,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                     $element =& $this->_elements[$elementIndex];
                     if ($element->getType()=='header') {
                         if (!is_null($lastHeader)) {
+                            $lastHeader->_generateId();
                             // Check if we had any required elements or
                             // we are at the top header that should be expanded by default.
                             if ($anyRequiredOrError || $headercounter === 1) {
-                                $this->setExpanded($lastHeader->getName());
-                            } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                                $this->setExpanded($lastHeader->getAttribute('id'));
+                            } else if (!isset($this->_collapsibleElements[$lastHeader->getAttribute('id')])) {
                                 // Define element as collapsed by default.
-                                $this->setExpanded($lastHeader->getName(), false);
+                                $this->setExpanded($lastHeader->getAttribute('id'), false);
                             }
                         }
                         $headercounter++;
@@ -1552,11 +1565,12 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                 }
                 // Process very last header.
                 if (!is_null($lastHeader)){
+                    $lastHeader->_generateId();
                     // Check if we had any required elements or
                     // we are at the top header that should be expanded by default.
                     if ($anyRequiredOrError || $headercounter === 1) {
                         $this->setExpanded($lastHeader->getName());
-                    } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                    } elseif (!isset($this->_collapsibleElements[$lastHeader->getAttribute('id')])) {
                         // Define element as collapsed by default.
                         $this->setExpanded($lastHeader->getName(), false);
                     }
@@ -2419,7 +2433,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             );
             $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
         }
-        if (count($this->_collapsibleElements)) {
+        if (!empty($this->_collapsibleElements)) {
             $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
         }
         if (!empty($this->_advancedElements)){
@@ -2555,10 +2569,10 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
     function renderHeader(&$header) {
         global $PAGE;
 
+        $header->_generateId();
         $name = $header->getName();
 
-        $id = empty($name) ? '' : ' id="' . $name . '"';
-        $id = preg_replace(array('/\]/', '/\[/'), array('', '_'), $id);
+        $id = empty($name) ? '' : ' id="' . $header->getAttribute('id') . '"';
         if (is_null($header->_text)) {
             $header_html = '';
         } elseif (!empty($name) && isset($this->_templates[$name])) {
@@ -2574,9 +2588,9 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
 
         // Define collapsible classes for fieldsets.
         $fieldsetclasses = array('clearfix');
-        if (isset($this->_collapsibleElements[$name])) {
+        if (isset($this->_collapsibleElements[$header->getAttribute('id')])) {
             $fieldsetclasses[] = 'collapsible';
-            if ($this->_collapsibleElements[$name]) {
+            if ($this->_collapsibleElements[$header->getAttribute('id')]) {
                 $fieldsetclasses[] = 'collapsed';
             }
         }
index 3a341cb..a31ac11 100644 (file)
@@ -672,6 +672,20 @@ class navigation_node implements renderable {
             $this->parent->make_inactive();
         }
     }
+
+    /**
+     * Hides the node and any children it has.
+     *
+     * @since 2.5
+     */
+    public function hide() {
+        $this->display = false;
+        if ($this->has_children()) {
+            foreach ($this->children as $child) {
+                $child->hide();
+            }
+        }
+    }
 }
 
 /**
@@ -1062,7 +1076,7 @@ class global_navigation extends navigation_node {
         $this->rootnodes['site'] = $this->add_course($SITE);
         $this->rootnodes['myprofile'] = $this->add(get_string('myprofile'), null, self::TYPE_USER, null, 'myprofile');
         $this->rootnodes['currentcourse'] = $this->add(get_string('currentcourse'), null, self::TYPE_ROOTNODE, null, 'currentcourse');
-        $this->rootnodes['mycourses'] = $this->add(get_string('mycourses'), new moodle_url('/my'), self::TYPE_ROOTNODE, null, 'mycourses');
+        $this->rootnodes['mycourses'] = $this->add(get_string('mycourses'), new moodle_url('/my/'), self::TYPE_ROOTNODE, null, 'mycourses');
         $this->rootnodes['courses'] = $this->add(get_string('courses'), new moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses');
         $this->rootnodes['users'] = $this->add(get_string('users'), null, self::TYPE_ROOTNODE, null, 'users');
 
@@ -1089,6 +1103,10 @@ class global_navigation extends navigation_node {
             $this->rootnodes['courses']->isexpandable = true;
         }
 
+        if ($this->rootnodes['mycourses']->isactive) {
+            $this->load_courses_enrolled();
+        }
+
         $canviewcourseprofile = true;
 
         // Next load context specific content into the navigation
@@ -2349,7 +2367,7 @@ class global_navigation extends navigation_node {
         $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id);
         $coursenode->nodetype = self::NODETYPE_BRANCH;
         $coursenode->hidden = (!$course->visible);
-        $coursenode->title($fullname);
+        $coursenode->title(str_replace('&amp;', '&', $fullname));
         if (!$forcegeneric) {
             $this->addedcourses[$course->id] = $coursenode;
         }
@@ -2392,16 +2410,19 @@ class global_navigation extends navigation_node {
             $participants = $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), self::TYPE_CONTAINER, get_string('participants'), 'participants');
             $currentgroup = groups_get_course_group($course, true);
             if ($course->id == $SITE->id) {
+                $filtervar = 'courseid';
                 $filterselect = '';
             } else if ($course->id && !$currentgroup) {
+                $filtervar = 'courseid';
                 $filterselect = $course->id;
             } else {
+                $filtervar = 'groupid';
                 $filterselect = $currentgroup;
             }
             $filterselect = clean_param($filterselect, PARAM_INT);
             if (($CFG->bloglevel == BLOG_GLOBAL_LEVEL or ($CFG->bloglevel == BLOG_SITE_LEVEL and (isloggedin() and !isguestuser())))
                and has_capability('moodle/blog:view', context_system::instance())) {
-                $blogsurls = new moodle_url('/blog/index.php', array('courseid' => $filterselect));
+                $blogsurls = new moodle_url('/blog/index.php', array($filtervar => $filterselect));
                 $participants->add(get_string('blogscourse','blog'), $blogsurls->out());
             }
             if (!empty($CFG->enablenotes) && (has_capability('moodle/notes:manage', $this->page->context) || has_capability('moodle/notes:view', $this->page->context))) {
@@ -2492,18 +2513,18 @@ class global_navigation extends navigation_node {
     public function set_expansion_limit($type) {
         global $SITE;
         $nodes = $this->find_all_of_type($type);
-        foreach ($nodes as &$node) {
+        foreach ($nodes as $node) {
             // We need to generate the full site node
             if ($type == self::TYPE_COURSE && $node->key == $SITE->id) {
                 continue;
             }
-            foreach ($node->children as &$child) {
+            foreach ($node->children as $child) {
                 // We still want to show course reports and participants containers
                 // or there will be navigation missing.
                 if ($type == self::TYPE_COURSE && $child->type === self::TYPE_CONTAINER) {
                     continue;
                 }
-                $child->display = false;
+                $child->hide();
             }
         }
         return true;
@@ -2556,6 +2577,47 @@ class global_navigation extends navigation_node {
         }
         return parent::find($key, $type);
     }
+
+    /**
+     * They've expanded the 'my courses' branch.
+     */
+    protected function load_courses_enrolled() {
+        global $CFG, $DB;
+        $sortorder = 'visible DESC';
+        // Append the chosen sortorder.
+        $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
+        $courses = enrol_get_my_courses(null, $sortorder);
+        if ($this->show_my_categories(true)) {
+            // OK Actually we are loading categories. We only want to load categories that have a parent of 0.
+            // In order to make sure we load everything required we must first find the categories that are not
+            // base categories and work out the bottom category in thier path.
+            $categoryids = array();
+            foreach ($courses as $course) {
+                $categoryids[] = $course->category;
+            }
+            $categoryids = array_unique($categoryids);
+            list($sql, $params) = $DB->get_in_or_equal($categoryids);
+            $categories = $DB->get_recordset_select('course_categories', 'id '.$sql.' AND parent <> 0', $params, 'sortorder, id', 'id, path');
+            foreach ($categories as $category) {
+                $bits = explode('/', trim($category->path,'/'));
+                $categoryids[] = array_shift($bits);
+            }
+            $categoryids = array_unique($categoryids);
+            $categories->close();
+
+            // Now we load the base categories.
+            list($sql, $params) = $DB->get_in_or_equal($categoryids);
+            $categories = $DB->get_recordset_select('course_categories', 'id '.$sql.' AND parent = 0', $params, 'sortorder, id');
+            foreach ($categories as $category) {
+                $this->add_category($category, $this->rootnodes['mycourses']);
+            }
+            $categories->close();
+        } else {
+            foreach ($courses as $course) {
+                $this->add_course($course, false, self::COURSE_MY);
+            }
+        }
+    }
 }
 
 /**
@@ -2682,44 +2744,6 @@ class global_navigation_for_ajax extends global_navigation {
         return $this->expandable;
     }
 
-    /**
-     * They've expanded the 'my courses' branch.
-     */
-    protected function load_courses_enrolled() {
-        global $DB;
-        $courses = enrol_get_my_courses();
-        if ($this->show_my_categories(true)) {
-            // OK Actually we are loading categories. We only want to load categories that have a parent of 0.
-            // In order to make sure we load everything required we must first find the categories that are not
-            // base categories and work out the bottom category in thier path.
-            $categoryids = array();
-            foreach ($courses as $course) {
-                $categoryids[] = $course->category;
-            }
-            $categoryids = array_unique($categoryids);
-            list($sql, $params) = $DB->get_in_or_equal($categoryids);
-            $categories = $DB->get_recordset_select('course_categories', 'id '.$sql.' AND parent <> 0', $params, 'sortorder, id', 'id, path');
-            foreach ($categories as $category) {
-                $bits = explode('/', trim($category->path,'/'));
-                $categoryids[] = array_shift($bits);
-            }
-            $categoryids = array_unique($categoryids);
-            $categories->close();
-
-            // Now we load the base categories.
-            list($sql, $params) = $DB->get_in_or_equal($categoryids);
-            $categories = $DB->get_recordset_select('course_categories', 'id '.$sql.' AND parent = 0', $params, 'sortorder, id');
-            foreach ($categories as $category) {
-                $this->add_category($category, $this->rootnodes['mycourses'], self::TYPE_MY_CATEGORY);
-            }
-            $categories->close();
-        } else {
-            foreach ($courses as $course) {
-                $this->add_course($course, false, self::COURSE_MY);
-            }
-        }
-    }
-
     /**
      * They've expanded the general 'courses' branch.
      */
@@ -3955,14 +3979,14 @@ class settings_navigation extends navigation_node {
             $reportfunction($reporttab, $user, $course);
         }
         $anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
-        if ($anyreport || ($course->showreports && $iscurrentuser && $forceforcontext)) {
+        if ($anyreport || ($course->showreports && $currentuser && $forceforcontext)) {
             // Add grade hardcoded grade report if necessary.
             $gradeaccess = false;
             if (has_capability('moodle/grade:viewall', $coursecontext)) {
                 // Can view all course grades.
                 $gradeaccess = true;
             } else if ($course->showgrades) {
-                if ($iscurrentuser && has_capability('moodle/grade:view', $coursecontext)) {
+                if ($currentuser && has_capability('moodle/grade:view', $coursecontext)) {
                     // Can view own grades.
                     $gradeaccess = true;
                 } else if (has_capability('moodle/grade:viewall', $usercontext)) {
index 74f0d37..814f8dd 100644 (file)
@@ -1733,6 +1733,97 @@ class html_writer {
 
         return $label;
     }
+
+    /**
+     * Combines a class parameter with other attributes. Aids in code reduction
+     * because the class parameter is very frequently used.
+     *
+     * If the class attribute is specified both in the attributes and in the
+     * class parameter, the two values are combined with a space between.
+     *
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     * @return array Attributes (or null if still none)
+     */
+    private static function add_class($class = '', array $attributes = null) {
+        if ($class !== '') {
+            $classattribute = array('class' => $class);
+            if ($attributes) {
+                if (array_key_exists('class', $attributes)) {
+                    $attributes['class'] = trim($attributes['class'] . ' ' . $class);
+                } else {
+                    $attributes = $classattribute + $attributes;
+                }
+            } else {
+                $attributes = $classattribute;
+            }
+        }
+        return $attributes;
+    }
+
+    /**
+     * Creates a <div> tag. (Shortcut function.)
+     *
+     * @param string $content HTML content of tag
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     * @return string HTML code for div
+     */
+    public static function div($content, $class = '', array $attributes = null) {
+        return self::tag('div', $content, self::add_class($class, $attributes));
+    }
+
+    /**
+     * Starts a <div> tag. (Shortcut function.)
+     *
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     * @return string HTML code for open div tag
+     */
+    public static function start_div($class = '', array $attributes = null) {
+        return self::start_tag('div', self::add_class($class, $attributes));
+    }
+
+    /**
+     * Ends a <div> tag. (Shortcut function.)
+     *
+     * @return string HTML code for close div tag
+     */
+    public static function end_div() {
+        return self::end_tag('div');
+    }
+
+    /**
+     * Creates a <span> tag. (Shortcut function.)
+     *
+     * @param string $content HTML content of tag
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     * @return string HTML code for span
+     */
+    public static function span($content, $class = '', array $attributes = null) {
+        return self::tag('span', $content, self::add_class($class, $attributes));
+    }
+
+    /**
+     * Starts a <span> tag. (Shortcut function.)
+     *
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     * @return string HTML code for open span tag
+     */
+    public static function start_span($class = '', array $attributes = null) {
+        return self::start_tag('span', self::add_class($class, $attributes));
+    }
+
+    /**
+     * Ends a <span> tag. (Shortcut function.)
+     *
+     * @return string HTML code for close span tag
+     */
+    public static function end_span() {
+        return self::end_tag('span');
+    }
 }
 
 /**
index 484ac3c..95a21a7 100644 (file)
@@ -198,7 +198,6 @@ class page_requirements_manager {
                 'base' => $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep.'moodle/'.$jsrev.'/',
                 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep,
                 'combine' => $this->yui3loader->combine,
-                'filter' => '',
                 'ext' => false,
                 'root' => 'moodle/'.$jsrev.'/', // Add the rev to the root path so that we can control caching.
                 'patterns' => array(
@@ -395,7 +394,7 @@ class page_requirements_manager {
                 case 'core_filepicker':
                     $module = array('name'     => 'core_filepicker',
                                     'fullpath' => '/repository/filepicker.js',
-                                    'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form', 'yui2-treeview', 'panel', 'cookie', 'datatable', 'datatable-sort', 'resize-plugin', 'dd-plugin', 'moodle-core_filepicker'),
+                                    'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form', 'yui2-treeview', 'panel', 'cookie', 'datatable', 'datatable-sort', 'resize-plugin', 'dd-plugin', 'escape', 'moodle-core_filepicker'),
                                     'strings'  => array(array('lastmodified', 'moodle'), array('name', 'moodle'), array('type', 'repository'), array('size', 'repository'),
                                                         array('invalidjson', 'repository'), array('error', 'moodle'), array('info', 'moodle'),
                                                         array('nofilesattached', 'repository'), array('filepicker', 'repository'), array('logout', 'repository'),
@@ -1062,7 +1061,7 @@ class page_requirements_manager {
         // Note: in JavaScript just use "YUI().use('overlay', function(Y) { .... });",
         //       this needs to be done before including any other script.
         $js = "var M = {}; M.yui = {};
-var moodleConfigFn = function(me) {var p = me.path, b = me.name.replace(/^moodle-/,'').split('-', 3), n = b.pop();if (/(skin|core)/.test(n)) {n = b.pop();me.type = 'css';};me.path = b.join('-')+'/'+n+'/'+n+'.'+me.type;};
+var moodleConfigFn = function(me) {var p = me.path, b = me.name.replace(/^moodle-/,'').split('-', 3), n = b.pop();if (/(skin|core)/.test(n)) {n = b.pop();me.type = 'css';};me.path = b.join('-')+'/'+n+'/'+n+'-min.'+me.type;};
 var galleryConfigFn = function(me) {var p = me.path,v=M.yui.galleryversion,f;if(/-(skin|core)/.test(me.name)) {me.type = 'css';p = p.replace(/-(skin|core)/, '').replace(/\.js/, '.css').split('/'), f = p.pop().replace(/(\-(min|debug))/, '');if (/-skin/.test(me.name)) {p.splice(p.length,0,v,'assets','skins','sam', f);} else {p.splice(p.length,0,v,'assets', f);};} else {p = p.split('/'), f = p.pop();p.splice(p.length,0,v, f);};me.path = p.join('/');};
 var yui2in3ConfigFn = function(me) {if(/-skin|reset|fonts|grids|base/.test(me.name)){me.type='css';me.path=me.path.replace(/\.js/,'.css');me.path=me.path.replace(/\/yui2-skin/,'/assets/skins/sam/yui2-skin');}};\n";
         $js .= js_writer::set_variable('YUI_config', $this->YUI_config, false) . "\n";
index db33e98..561ec9f 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-Copyright (c) 2002-2003, Michael Bretterklieber <michael@bretterklieber.com>
+Copyright (c) 2002-2010, Michael Bretterklieber <michael@bretterklieber.com>
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
@@ -101,7 +101,6 @@ class Crypt_CHAP extends PEAR
     function generateChallenge($varname = 'challenge', $size = 8)
     {
         $this->$varname = '';
-        mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff);
         for ($i = 0; $i < $size; $i++) {
             $this->$varname .= pack('C', 1 + mt_rand() % 255);
         }
@@ -149,7 +148,7 @@ class Crypt_CHAP_MD5 extends Crypt_CHAP
  * Generate MS-CHAPv1 Packets. MS-CHAP doesen't use the plaintext password, it uses the
  * NT-HASH wich is stored in the SAM-Database or in the smbpasswd, if you are using samba.
  * The NT-HASH is MD4(str2unicode(plaintextpass)).
- * You need the mhash extension for this class.
+ * You need the hash extension for this class.
  *
  * @package Crypt_CHAP
  */
@@ -165,13 +164,13 @@ class Crypt_CHAP_MSv1 extends Crypt_CHAP
     /**
      * Constructor
      *
-     * Loads the mhash extension
+     * Loads the hash extension
      * @return void
      */
     function Crypt_CHAP_MSv1()
     {
         $this->Crypt_CHAP();
-        $this->loadExtension('mhash');
+        $this->loadExtension('hash');
     }
 
     /**
@@ -183,9 +182,9 @@ class Crypt_CHAP_MSv1 extends Crypt_CHAP
     function ntPasswordHash($password = null)
     {
         if (isset($password)) {
-            return mhash(MHASH_MD4, $this->str2unicode($password));
+            return pack('H*',hash('md4', $this->str2unicode($password)));
         } else {
-            return mhash(MHASH_MD4, $this->str2unicode($this->password));
+            return pack('H*',hash('md4', $this->str2unicode($this->password)));
         }
     }
 
@@ -432,7 +431,7 @@ class Crypt_CHAP_MSv2 extends Crypt_CHAP_MSv1
      */
     function ntPasswordHashHash($nthash)
     {
-        return mhash(MHASH_MD4, $nthash);
+        return pack('H*',hash('md4', $nthash));
     }
 
     /**
@@ -444,7 +443,7 @@ class Crypt_CHAP_MSv2 extends Crypt_CHAP_MSv1
      */
     function challengeHash()
     {
-        return substr(mhash(MHASH_SHA1, $this->peerChallenge . $this->authChallenge . $this->username), 0, 8);
+        return substr(pack('H*',hash('sha1', $this->peerChallenge . $this->authChallenge . $this->username)), 0, 8);
     }
 
     /**
index a626ab4..fdc95f6 100644 (file)
@@ -362,6 +362,7 @@ class plugin_manager {
     public static function is_deleted_standard_plugin($type, $name) {
         static $plugins = array(
             // do not add 1.9-2.2 plugin removals here
+            'qformat' => array('blackboard'),
         );
 
         if (!isset($plugins[$type])) {
@@ -508,7 +509,7 @@ class plugin_manager {
             ),
 
             'qformat' => array(
-                'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
+                'aiken', 'blackboard_six', 'examview', 'gift',
                 'learnwise', 'missingword', 'multianswer', 'webct',
                 'xhtml', 'xml'
             ),
index eefa460..129147a 100644 (file)
@@ -526,6 +526,22 @@ function get_exception_info($ex) {
         $debuginfo .= PHP_EOL.'$a contents: '.print_r($a, true);
     }
 
+    // Remove some absolute paths from message and debugging info.
+    $searches = array();
+    $replaces = array();
+    $cfgnames = array('tempdir', 'cachedir', 'themedir',
+        'langmenucachefile', 'langcacheroot', 'dataroot', 'dirroot');
+    foreach ($cfgnames as $cfgname) {
+        if (property_exists($CFG, $cfgname)) {
+            $searches[] = $CFG->$cfgname;
+            $replaces[] = "[$cfgname]";
+        }
+    }
+    if (!empty($searches)) {
+        $message   = str_replace($searches, $replaces, $message);
+        $debuginfo = str_replace($searches, $replaces, $debuginfo);
+    }
+
     // Be careful, no guarantee weblib.php is loaded.
     if (function_exists('clean_text')) {
         $message = clean_text($message);
index 54e2d07..5fe2755 100644 (file)
@@ -139,7 +139,7 @@ class moodle_simplepie_file extends SimplePie_File
             }
         }
 
-        $this->headers = $curl->get($url);
+        $this->headers = curl::strip_double_headers($curl->get($url));
 
         if ($curl->error) {
             $this->error = 'cURL Error: '.$curl->error;
index 3582c01..0cbf77f 100644 (file)
@@ -1442,6 +1442,18 @@ function stats_get_report_options($courseid,$mode) {
     return $reportoptions;
 }
 
+/**
+ * Fix missing entries in the statistics.
+ *
+ * This creates a dummy stat when nothing happened during a day/week/month.
+ *
+ * @param array $stats array of statistics.
+ * @param int $timeafter unused.
+ * @param string $timestr type of statistics to generate (dayly, weekly, monthly).
+ * @param boolean $line2
+ * @param boolean $line3
+ * @return array of fixed statistics.
+ */
 function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
 
     if (empty($stats)) {
@@ -1449,23 +1461,37 @@ function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
     }
 
     $timestr = str_replace('user_','',$timestr); // just in case.
-    $fun = 'stats_get_base_'.$timestr;
 
+    // Gets the current user base time.
+    $fun = 'stats_get_base_'.$timestr;
     $now = $fun();
 
-    $times = array();
-    // add something to timeafter since it is our absolute base
+    // Extract the ending time of the statistics.
     $actualtimes = array();
-    foreach ($stats as $statid=>$s) {
-        //normalize the times in stats - those might have been created in different timezone, DST etc.
-        $s->timeend = $fun($s->timeend + 60*60*5);
+    $actualtimeshour = null;
+    foreach ($stats as $statid => $s) {
+        // Normalise the month date to the 1st if for any reason it's set to later. But we ignore
+        // anything above or equal to 29 because sometimes we get the end of the month. Also, we will
+        // set the hours of the result to all of them, that way we prevent DST differences.
+        if ($timestr == 'monthly') {
+            $day = date('d', $s->timeend);
+            if (date('d', $s->timeend) > 1 && date('d', $s->timeend) < 29) {
+                $day = 1;
+            }
+            if (is_null($actualtimeshour)) {
+                $actualtimeshour = date('H', $s->timeend);
+            }
+            $s->timeend = mktime($actualtimeshour, 0, 0, date('m', $s->timeend), $day, date('Y', $s->timeend));
+        }
         $stats[$statid] = $s;
-
         $actualtimes[] = $s->timeend;
     }
 
-    $timeafter = array_pop(array_values($actualtimes));
+    $actualtimesvalues = array_values($actualtimes);
+    $timeafter = array_pop($actualtimesvalues);
 
+    // Generate a base timestamp for each possible month/week/day.
+    $times = array();
     while ($timeafter < $now) {
         $times[] = $timeafter;
         if ($timestr == 'daily') {
@@ -1473,12 +1499,25 @@ function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
         } else if ($timestr == 'weekly') {
             $timeafter = stats_get_next_week_start($timeafter);
         } else if ($timestr == 'monthly') {
-            $timeafter = stats_get_next_month_start($timeafter);
+            // We can't just simply +1 month because the 31st Jan + 1 month = 2nd of March.
+            $year = date('Y', $timeafter);
+            $month = date('m', $timeafter);
+            $day = date('d', $timeafter);
+            $dayofnextmonth = $day;
+            if ($day >= 29) {
+                $daysinmonth = date('n', mktime(0, 0, 0, $month+1, 1, $year));
+                if ($day > $daysinmonth) {
+                    $dayofnextmonth = $daysinmonth;
+                }
+            }
+            $timeafter = mktime($actualtimeshour, 0, 0, $month+1, $dayofnextmonth, $year);
         } else {
-            return $stats; // this will put us in a never ending loop.
+            // This will put us in a never ending loop.
+            return $stats;
         }
     }
 
+    // Add the base timestamp to the statistics if not present.
     foreach ($times as $count => $time) {
         if (!in_array($time,$actualtimes) && $count != count($times) -1) {
             $newobj = new StdClass;
@@ -1499,7 +1538,6 @@ function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
 
     usort($stats,"stats_compare_times");
     return $stats;
-
 }
 
 // helper function to sort arrays by $obj->timeend
index d409231..c3f0932 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -51,6 +50,9 @@ use Behat\Behat\Exception\PendingException as PendingException;
  */
 class behat_data_generators extends behat_base {
 
+    /**
+     * @var testing_data_generator
+     */
     protected $datagenerator;
 
     /**
@@ -215,7 +217,7 @@ class behat_data_generators extends behat_base {
     /**
      * Gets the user id from it's username.
      * @throws Exception
-     * @param string $idnumber
+     * @param string $username
      * @return int
      */
     protected function get_user_id($username) {
@@ -230,7 +232,7 @@ class behat_data_generators extends behat_base {
     /**
      * Gets the role id from it's shortname.
      * @throws Exception
-     * @param string $idnumber
+     * @param string $roleshortname
      * @return int
      */
     protected function get_role_id($roleshortname) {
index 260adae..5337c04 100644 (file)
@@ -50,6 +50,7 @@ class behat_forms extends behat_base {
      *
      * @When /^I press "(?P<button_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $button
      */
     public function press_button($button) {
 
@@ -67,6 +68,9 @@ class behat_forms extends behat_base {
      */
     public function i_fill_the_moodle_form_with(TableNode $data) {
 
+        // Expand all fields in case we have.
+        $this->expand_all_fields();
+
         $datahash = $data->getRowsHash();
 
         // The action depends on the field type.
@@ -85,11 +89,73 @@ class behat_forms extends behat_base {
         }
     }
 
+    /**
+     * Expands all moodleform's fields, including collapsed fieldsets and advanced fields if they are present.
+     * @Given /^I expand all fieldsets$/
+     */
+    public function i_expand_all_fieldsets() {
+        $this->expand_all_fields();
+    }
+
+    /**
+     * Expands all moodle form fieldsets if they exists.
+     *
+     * Externalized from i_expand_all_fields to call it from
+     * other form-related steps without having to use steps-group calls.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find_all
+     * @return void
+     */
+    protected function expand_all_fields() {
+
+        // behat_base::find() throws an exception if there are no elements, we should not fail a test because of this.
+        try {
+
+            // Expand fieldsets.
+            $fieldsets = $this->find_all('css', 'fieldset.collapsed.jsprocessed a.fheader');
+
+            // We are supposed to have fieldsets here, otherwise exception.
+
+            // Funny thing about this, with find_all() we specify a pattern and each element matching the pattern is added to the array
+            // with of xpaths with a [0], [1]... sufix, but when we click on an element it does not matches the specified xpath
+            // anymore (is not collapsed) so [1] becomes [0], that's why we always click on the first XPath match, will be always the next one.
+            $iterations = count($fieldsets);
+            for ($i = 0; $i < $iterations; $i++) {
+                $fieldsets[0]->click();
+            }
+
+        } catch (ElementNotFoundException $e) {
+            // We continue if there are not expanded fields.
+        }
+
+        // Different try & catch as we can have expanded fieldsets with advanced fields on them.
+        try {
+
+            // Show all fields.
+            $showmorestr = get_string('showmore', 'form');
+            $showmores = $this->find_all('xpath', "//a[contains(concat(' ', normalize-space(.), ' '), '" . $showmorestr . "')][contains(concat(' ', normalize-space(@class), ' '), ' morelesstoggler')]");
+
+            // We are supposed to have 'show more's here, otherwise exception.
+
+            // Same funny case, after clicking on the element the [1] showmore link becomes the [0].
+            $iterations = count($showmores);
+            for ($i = 0; $i < $iterations; $i++) {
+                $showmores[0]->click();
+            }
+
+        } catch (ElementNotFoundException $e) {
+            // We continue with the test.
+        }
+
+    }
+
     /**
      * Fills in form field with specified id|name|label|value.
      *
      * @When /^I fill in "(?P<field_string>(?:[^"]|\\")*)" with "(?P<value_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $field
+     * @param string $value
      */
     public function fill_field($field, $value) {
 
@@ -102,6 +168,8 @@ class behat_forms extends behat_base {
      *
      * @When /^I select "(?P<option_string>(?:[^"]|\\")*)" from "(?P<select_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $option
+     * @param string $select
      */
     public function select_option($option, $select) {
 
@@ -117,6 +185,7 @@ class behat_forms extends behat_base {
      *
      * @When /^I check "(?P<option_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $option
      */
     public function check_option($option) {
 
@@ -129,6 +198,7 @@ class behat_forms extends behat_base {
      *
      * @When /^I uncheck "(?P<option_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $option
      */
     public function uncheck_option($option) {
 
@@ -142,8 +212,8 @@ class behat_forms extends behat_base {
      * @Then /^the "(?P<field_string>(?:[^"]|\\")*)" field should match "(?P<value_string>(?:[^"]|\\")*)" value$/
      * @throws ExpectationException
      * @throws ElementNotFoundException Thrown by behat_base::find
-     * @param mixed $locator
-     * @param mixed $value
+     * @param string $locator
+     * @param string $value
      */
     public function the_field_should_match_value($locator, $value) {
 
@@ -166,6 +236,7 @@ class behat_forms extends behat_base {
      *
      * @Then /^the "(?P<checkbox_string>(?:[^"]|\\")*)" checkbox should be checked$/
      * @see Behat\MinkExtension\Context\MinkContext
+     * @param string $checkbox
      */
     public function assert_checkbox_checked($checkbox) {
         $this->assertSession()->checkboxChecked($checkbox);
@@ -176,6 +247,7 @@ class behat_forms extends behat_base {
      *
      * @Then /^the "(?P<checkbox_string>(?:[^"]|\\")*)" checkbox should not be checked$/
      * @see Behat\MinkExtension\Context\MinkContext
+     * @param string $checkbox
      */
     public function assert_checkbox_not_checked($checkbox) {
         $this->assertSession()->checkboxNotChecked($checkbox);
index 9a2c794..67d773e 100644 (file)
@@ -50,7 +50,7 @@ class behat_general extends behat_base {
      * @Given /^I am on homepage$/
      */
     public function i_am_on_homepage() {
-        $this->getSession()->visit($this->locatePath('/'));
+        $this->getSession()->visit($this->locate_path('/'));
     }
 
     /**
@@ -58,6 +58,7 @@ class behat_general extends behat_base {
      *
      * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $link
      */
     public function click_link($link) {
 
@@ -85,27 +86,54 @@ class behat_general extends behat_base {
     }
 
     /**
-     * Mouse over a CSS element.
+     * Generic mouse over action. Mouse over a element of the specified type.
      *
-     * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)"$/
-     * @throws ExpectationException Thrown by behat_base::find
-     * @param string $element
+     * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
+     * @param string $element Element we look for
+     * @param string $selectortype The type of what we look for
      */
-    public function i_hover($element) {
-
-        $exception = new ExpectationException(
-            'The hovered element "' . $element . '" was not found anywhere in the page', $this->getSession()
-        );
+    public function i_hover($element, $selectortype) {
 
-        $node = $this->find('css', $element, $exception);
+        // Gets the node based on the requested selector type and locator.
+        $node = $this->get_selected_node($selectortype, $element);
         $node->mouseOver();
     }
 
+    /**
+     * Generic click action. Click on the element of the specified type.
+     *
+     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
+     * @param string $element Element we look for
+     * @param string $selectortype The type of what we look for
+     */
+    public function i_click_on($element, $selectortype) {
+
+        // Gets the node based on the requested selector type and locator.
+        $node = $this->get_selected_node($selectortype, $element);
+        $node->click();
+    }
+
+    /**
+     * Click on the element of the specified type which is located inside the second element.
+     *
+     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
+     * @param string $element Element we look for
+     * @param string $selectortype The type of what we look for
+     * @param string $nodeelement Element we look in
+     * @param string $nodeselectortype The type of selector where we look in
+     */
+    public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {
+
+        $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
+        $node->click();
+    }
+
     /**
      * Checks, that page contains specified text.
      *
      * @see Behat\MinkExtension\Context\MinkContext
      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
+     * @param string $text
      */
     public function assert_page_contains_text($text) {
         $this->assertSession()->pageTextContains($text);
@@ -116,40 +144,54 @@ class behat_general extends behat_base {
      *
      * @see Behat\MinkExtension\Context\MinkContext
      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
+     * @param string $text
      */
     public function assert_page_not_contains_text($text) {
         $this->assertSession()->pageTextNotContains($text);
     }
 
     /**
-     * Checks, that element with specified CSS contains specified text.
+     * Checks, that element with specified CSS selector or XPath contains specified text.
      *
-     * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
+     * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
+     * @param string $text
+     * @param string $element Element we look in.
+     * @param string $selectortype The type of element where we are looking in.
      */
-    public function assert_element_contains_text($text, $element) {
-        $this->assertSession()->elementTextContains('css', $element, $text);
+    public function assert_element_contains_text($text, $element, $selectortype) {
+
+        // Transforming from steps definitions selector/locator format to Mink format.
+        list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
+        $this->assertSession()->elementTextContains($selector, $locator, $text);
     }
 
     /**
-     * Checks, that element with specified CSS doesn't contain specified text.
+     * Checks, that element with specified CSS selector or XPath doesn't contain specified text.
      *
-     * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
+     * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
+     * @param string $text
+     * @param string $element Element we look in.
+     * @param string $selectortype The type of element where we are looking in.
      */
-    public function assert_element_not_contains_text($text, $element) {
-        $this->assertSession()->elementTextNotContains('css', $element, $text);
+    public function assert_element_not_contains_text($text, $element, $selectortype) {
+
+        // Transforming from steps definitions selector/locator format to mink format.
+        list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
+        $this->assertSession()->elementTextNotContains($selector, $locator, $text);
     }
 
     /**
-     * Checks, that element with given CSS is disabled.
+     * Checks, that element of specified type is disabled.
      *
-     * @Then /^the element "(?P<element_string>(?:[^"]|\\")*)" should be disabled$/
+     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be disabled$/
      * @throws ExpectationException Thrown by behat_base::find
-     * @param string $element
+     * @param string $element Element we look in
+     * @param string $selectortype The type of element where we are looking in.
      */
-    public function the_element_should_be_disabled($element) {
+    public function the_element_should_be_disabled($element, $selectortype) {
 
-        $exception = new ExpectationException('There is no "' . $element . '" element', $this->getSession());
-        $node = $this->find('css', $element, $exception);
+        // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
+        $node = $this->get_selected_node($selectortype, $element);
 
         if (!$node->hasAttribute('disabled')) {
             throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
@@ -157,16 +199,17 @@ class behat_general extends behat_base {
     }
 
     /**
-     * Checks, that element with given CSS is enabled.
+     * Checks, that element of specified type is enabled.
      *
-     * @Then /^the element "(?P<element_string>(?:[^"]|\\")*)" should be enabled$/
+     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be enabled$/
      * @throws ExpectationException Thrown by behat_base::find
-     * @param string $element
+     * @param string $element Element we look on
+     * @param string $selectortype The type of where we look
      */
-    public function the_element_should_be_enabled($element) {
+    public function the_element_should_be_enabled($element, $selectortype) {
 
-        $exception = new ExpectationException('There is no "' . $element . '" element', $this->getSession());
-        $node = $this->find('css', $element, $exception);
+        // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
+        $node = $this->get_selected_node($selectortype, $element);
 
         if ($node->hasAttribute('disabled')) {
             throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
index 8724326..60b6768 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -113,8 +112,8 @@ class behat_hooks extends behat_base {
                php_sapi_name() != 'cli' ||
                !behat_util::is_test_mode_enabled() ||
                !behat_util::is_test_site() ||
-               !isset($CFG->originaldataroot))  {
-           throw new coding_exception('Behat only can modify the test database and the test dataroot!');
+               !isset($CFG->originaldataroot)) {
+            throw new coding_exception('Behat only can modify the test database and the test dataroot!');
         }
 
         behat_util::reset_database();
index 13f282d..7743d7b 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index 9f3b2b8..c684c8a 100644 (file)
@@ -77,7 +77,7 @@ class behat_transformations extends behat_base {
      * adding them in a different method for Behat API restrictions.
      *
      * @Transform /^table:(.*)/
-     * @param TableNode $table
+     * @param TableNode $tablenode
      * @return TableNode The transformed table
      */
     public function tablenode_transformations(TableNode $tablenode) {
index 53bc317..7365a44 100644 (file)
@@ -270,4 +270,101 @@ class filelib_testcase extends advanced_testcase {
         $this->assertEquals($contenthash, $fileref->get_contenthash());
         $this->assertEquals($filecontent, $fileref->get_content());
     }
+
+    /**
+     * Tests the strip_double_headers function in the curl class.
+     */
+    public function test_curl_strip_double_headers() {
+        // Example from issue tracker.
+        $mdl30648example = <<<EOF
+HTTP/1.0 407 Proxy Authentication Required
+Server: squid/2.7.STABLE9
+Date: Thu, 08 Dec 2011 14:44:33 GMT
+Content-Type: text/html
+Content-Length: 1275
+X-Squid-Error: ERR_CACHE_ACCESS_DENIED 0
+Proxy-Authenticate: Basic realm="Squid proxy-caching web server"
+X-Cache: MISS from homer.lancs.ac.uk
+X-Cache-Lookup: NONE from homer.lancs.ac.uk:3128
+Via: 1.0 homer.lancs.ac.uk:3128 (squid/2.7.STABLE9)
+Connection: close
+
+HTTP/1.0 200 OK
+Server: Apache
+X-Lb-Nocache: true
+Cache-Control: private, max-age=15
+ETag: "4d69af5d8ba873ea9192c489e151bd7b"
+Content-Type: text/html
+Date: Thu, 08 Dec 2011 14:44:53 GMT
+Set-Cookie: BBC-UID=c4de2e109c8df6a51de627cee11b214bd4fb6054a030222488317afb31b343360MoodleBot/1.0; expires=Mon, 07-Dec-15 14:44:53 GMT; path=/; domain=bbc.co.uk
+X-Cache-Action: MISS
+X-Cache-Age: 0
+Vary: Cookie,X-Country,X-Ip-is-uk-combined,X-Ip-is-advertise-combined,X-Ip_is_uk_combined,X-Ip_is_advertise_combined, X-GeoIP
+X-Cache: MISS from ww
+
+<html>...
+EOF;
+        $mdl30648expected = <<<EOF
+HTTP/1.0 200 OK
+Server: Apache
+X-Lb-Nocache: true
+Cache-Control: private, max-age=15
+ETag: "4d69af5d8ba873ea9192c489e151bd7b"
+Content-Type: text/html
+Date: Thu, 08 Dec 2011 14:44:53 GMT
+Set-Cookie: BBC-UID=c4de2e109c8df6a51de627cee11b214bd4fb6054a030222488317afb31b343360MoodleBot/1.0; expires=Mon, 07-Dec-15 14:44:53 GMT; path=/; domain=bbc.co.uk
+X-Cache-Action: MISS
+X-Cache-Age: 0
+Vary: Cookie,X-Country,X-Ip-is-uk-combined,X-Ip-is-advertise-combined,X-Ip_is_uk_combined,X-Ip_is_advertise_combined, X-GeoIP
+X-Cache: MISS from ww
+
+<html>...
+EOF;
+        // For HTTP, replace the \n with \r\n.
+        $mdl30648example = preg_replace("~(?!<\r)\n~", "\r\n", $mdl30648example);
+        $mdl30648expected = preg_replace("~(?!<\r)\n~", "\r\n", $mdl30648expected);
+
+        // Test stripping works OK.
+        $this->assertEquals($mdl30648expected, curl::strip_double_headers($mdl30648example));
+        // Test it does nothing to the 'plain' data.
+        $this->assertEquals($mdl30648expected, curl::strip_double_headers($mdl30648expected));
+
+        // Example from OU proxy.
+        $httpsexample = <<<EOF
+HTTP/1.0 200 Connection established
+
+HTTP/1.1 200 OK
+Date: Fri, 22 Feb 2013 17:14:23 GMT
+Server: Apache/2
+X-Powered-By: PHP/5.3.3-7+squeeze14
+Content-Type: text/xml
+Connection: close
+Content-Encoding: gzip
+Transfer-Encoding: chunked
+
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<rss version="2.0">...
+EOF;
+        $httpsexpected = <<<EOF
+HTTP/1.1 200 OK
+Date: Fri, 22 Feb 2013 17:14:23 GMT
+Server: Apache/2
+X-Powered-By: PHP/5.3.3-7+squeeze14
+Content-Type: text/xml
+Connection: close
+Content-Encoding: gzip
+Transfer-Encoding: chunked
+
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<rss version="2.0">...
+EOF;
+        // For HTTP, replace the \n with \r\n.
+        $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
+        $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
+
+        // Test stripping works OK.
+        $this->assertEquals($httpsexpected, curl::strip_double_headers($httpsexample));
+        // Test it does nothing to the 'plain' data.
+        $this->assertEquals($httpsexpected, curl::strip_double_headers($httpsexpected));
+    }
 }
index eaa82db..97e1285 100644 (file)
@@ -89,4 +89,84 @@ class html_writer_testcase extends basic_testcase {
         $this->assertEquals('<div class="score">0</div>',
             html_writer::nonempty_tag('div', '0', array('class' => 'score')));
     }
+
+    public function test_div() {
+        // All options.
+        $this->assertEquals('<div class="frog" id="kermit">ribbit</div>',
+                html_writer::div('ribbit', 'frog', array('id' => 'kermit')));
+        // Combine class from attributes and $class.
+        $this->assertEquals('<div class="amphibian frog">ribbit</div>',
+                html_writer::div('ribbit', 'frog', array('class' => 'amphibian')));
+        // Class only.
+        $this->assertEquals('<div class="frog">ribbit</div>',
+                html_writer::div('ribbit', 'frog'));
+        // Attributes only.
+        $this->assertEquals('<div id="kermit">ribbit</div>',
+                html_writer::div('ribbit', '', array('id' => 'kermit')));
+        // No options.
+        $this->assertEquals('<div>ribbit</div>',
+                html_writer::div('ribbit'));
+    }
+
+    public function test_start_div() {
+        // All options.
+        $this->assertEquals('<div class="frog" id="kermit">',
+                html_writer::start_div('frog', array('id' => 'kermit')));
+        // Combine class from attributes and $class.
+        $this->assertEquals('<div class="amphibian frog">',
+                html_writer::start_div('frog', array('class' => 'amphibian')));
+        // Class only.
+        $this->assertEquals('<div class="frog">',
+                html_writer::start_div('frog'));
+        // Attributes only.
+        $this->assertEquals('<div id="kermit">',
+                html_writer::start_div('', array('id' => 'kermit')));
+        // No options.
+        $this->assertEquals('<div>',
+                html_writer::start_div());
+    }
+
+    public function test_end_div() {
+        $this->assertEquals('</div>', html_writer::end_div());
+    }
+
+    public function test_span() {
+        // All options.
+        $this->assertEquals('<span class="frog" id="kermit">ribbit</span>',
+                html_writer::span('ribbit', 'frog', array('id' => 'kermit')));
+        // Combine class from attributes and $class.
+        $this->assertEquals('<span class="amphibian frog">ribbit</span>',
+                html_writer::span('ribbit', 'frog', array('class' => 'amphibian')));
+        // Class only.
+        $this->assertEquals('<span class="frog">ribbit</span>',
+                html_writer::span('ribbit', 'frog'));
+        // Attributes only.
+        $this->assertEquals('<span id="kermit">ribbit</span>',
+                html_writer::span('ribbit', '', array('id' => 'kermit')));
+        // No options.
+        $this->assertEquals('<span>ribbit</span>',
+                html_writer::span('ribbit'));
+    }
+
+    public function test_start_span() {
+        // All options.
+        $this->assertEquals('<span class="frog" id="kermit">',
+                html_writer::start_span('frog', array('id' => 'kermit')));
+        // Combine class from attributes and $class.
+        $this->assertEquals('<span class="amphibian frog">',
+                html_writer::start_span('frog', array('class' => 'amphibian')));
+        // Class only.
+        $this->assertEquals('<span class="frog">',
+                html_writer::start_span('frog'));
+        // Attributes only.
+        $this->assertEquals('<span id="kermit">',
+                html_writer::start_span('', array('id' => 'kermit')));
+        // No options.
+        $this->assertEquals('<span>',
+                html_writer::start_span());
+    }
+
+    public function test_end_span() {
+        $this->assertEquals('</span>', html_writer::end_span());
+    }
 }
index 30a257b..63431ee 100644 (file)
@@ -118,4 +118,28 @@ class core_setuplib_testcase extends basic_testcase {
             $this->assertTrue(is_web_crawler(), "$agent should be considered a search engine");
         }
     }
+
+    /**
+     * Test if get_exception_info() removes file system paths
+     */
+    public function test_exception_info_removes_serverpaths() {
+        global $CFG;
+
+        // This doesn't test them all possible ones, but these are set for unit tests.
+        $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir');
+
+        $fixture  = '';
+        $expected = '';
+        foreach ($cfgnames as $cfgname) {
+            if (!empty($CFG->$cfgname)) {
+                $fixture  .= $CFG->$cfgname.' ';
+                $expected .= "[$cfgname] ";
+            }
+        }
+        $exception     = new moodle_exception('generalexceptionmessage', 'error', '', $fixture, $fixture);
+        $exceptioninfo = get_exception_info($exception);
+
+        $this->assertContains($expected, $exceptioninfo->message, 'Exception message does not contain system paths');
+        $this->assertContains($expected, $exceptioninfo->debuginfo, 'Exception debug info does not contain system paths');
+    }
 }
diff --git a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-coverage.js b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-coverage.js
new file mode 100644 (file)
index 0000000..30a7119
Binary files /dev/null and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-coverage.js differ
similarity index 97%
rename from lib/yui/tooltip/tooltip.js
rename to lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js
index e2680e5..2d1ce55 100644 (file)
Binary files a/lib/yui/tooltip/tooltip.js and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js differ
diff --git a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js
new file mode 100644 (file)
index 0000000..e2a616c
Binary files /dev/null and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js differ
diff --git a/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js
new file mode 100644 (file)
index 0000000..5080704
Binary files /dev/null and b/lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js differ
index 83d4f4e..f7f3000 100644 (file)
@@ -77,6 +77,10 @@ Y.extend(DIALOGUE, Y.Panel, {
         this.after('visibleChange', this.visibilityChanged, this);
         this.render();
         this.show();
+
+        // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
+        // and allow setting of z-index in theme.
+        this.get('boundingBox').setStyle('zIndex', null);
     },
     visibilityChanged : function(e) {
         var titlebar;
diff --git a/lib/yui/src/tooltip/build.json b/lib/yui/src/tooltip/build.json
new file mode 100644 (file)
index 0000000..bb3ed16
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-tooltip",
+  "builds": {
+    "moodle-core-tooltip": {
+      "jsfiles": [
+        "tooltip.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/tooltip/js/tooltip.js b/lib/yui/src/tooltip/js/tooltip.js
new file mode 100644 (file)
index 0000000..10e9121
--- /dev/null
@@ -0,0 +1,427 @@
+/**
+ * Provides the base tooltip class.
+ *
+ * @module moodle-core-tooltip
+ */
+
+/**
+ * A base class for a tooltip.
+ *
+ * @param {Object} config Object literal specifying tooltip configuration properties.
+ * @class M.core.tooltip
+ * @constructor
+ * @extends M.core.dialogue
+ */
+function TOOLTIP(config) {
+    if (!config) {
+        config = {};
+    }
+
+    // Override the default options provided by the parent class.
+    if (typeof config.draggable === 'undefined') {
+        config.draggable = true;
+    }
+
+    if (typeof config.constrain === 'undefined') {
+        config.constrain = true;
+    }
+
+    if (typeof config.lightbox === 'undefined') {
+        config.lightbox = false;
+    }
+
+    TOOLTIP.superclass.constructor.apply(this, [config]);
+}
+
+var SELECTORS = {
+        CLOSEBUTTON: '.closebutton'
+    },
+
+    CSS = {
+        PANELTEXT: 'tooltiptext'
+    },
+    RESOURCES = {
+        WAITICON: {
+            pix: 'i/loading_small',
+            component: 'moodle'
+        }
+    },
+    ATTRS = {};
+
+/**
+ * Static property provides a string to identify the JavaScript class.
+ *
+ * @property NAME
+ * @type String
+ * @static
+ */
+TOOLTIP.NAME = 'moodle-core-tooltip';
+
+/**
+ * Static property used to define the CSS prefix applied to tooltip dialogues.
+ *
+ * @property CSS_PREFIX
+ * @type String
+ * @static
+ */
+TOOLTIP.CSS_PREFIX = 'moodle-dialogue';
+
+/**
+ * Static property used to define the default attribute configuration for the Tooltip.
+ *
+ * @property ATTRS
+ * @type String
+ * @static
+ */
+TOOLTIP.ATTRS = ATTRS;
+
+/**
+ * The initial value of the header region before the content finishes loading.
+ *
+ * @attribute initialheadertext
+ * @type String
+ * @default ''
+ * @writeOnce
+ */
+ATTRS.initialheadertext = {
+    value: ''
+};
+
+/**
+  * The initial value of the body region before the content finishes loading.
+  *
+  * The supplid string will be wrapped in a div with the CSS.PANELTEXT class and a standard Moodle spinner
+  * appended.
+  *
+  * @attribute initialbodytext
+  * @type String
+  * @default ''
+  * @writeOnce
+  */
+ATTRS.initialbodytext = {
+    value: '',
+    setter: function(content) {
+        var parentnode,
+            spinner;
+        parentnode = Y.Node.create('<div />')
+            .addClass(CSS.PANELTEXT);
+
+        spinner = Y.Node.create('<img />')
+            .setAttribute('src', M.util.image_url(RESOURCES.WAITICON.pix, RESOURCES.WAITICON.component))
+            .addClass('spinner');
+
+        if (content) {
+            // If we have been provided with content, add it to the parent and make
+            // the spinner appear correctly inline
+            parentnode.set('text', content);
+            spinner.addClass('iconsmall');
+        } else {
+            // If there is no loading message, just make the parent node a lightbox
+            parentnode.addClass('content-lightbox');
+        }
+
+        parentnode.append(spinner);
+        return parentnode;
+    }
+};
+
+/**
+ * The initial value of the footer region before the content finishes loading.
+ *
+ * If a value is supplied, it will be wrapped in a <div> first.
+ *
+ * @attribute initialfootertext
+ * @type String
+ * @default ''
+ * @writeOnce
+ */
+ATTRS.initialfootertext = {
+    value: null,
+    setter: function(content) {
+        if (content) {
+            return Y.Node.create('<div />')
+                .set('text', content);
+        }
+    }
+};
+
+/**
+ * The function which handles setting the content of the title region.
+ * The specified function will be called with a context of the tooltip instance.
+ *
+ * The default function will simply set the value of the title to object.heading as returned by the AJAX call.
+ *
+ * @attribute headerhandler
+ * @type Function|String|null
+ * @default set_header_content
+ */
+ATTRS.headerhandler = {
+    value: 'set_header_content'
+};
+
+/**
+ * The function which handles setting the content of the body region.
+ * The specified function will be called with a context of the tooltip instance.
+ *
+ * The default function will simply set the value of the body area to a div containing object.text as returned
+ * by the AJAX call.
+ *
+ * @attribute bodyhandler
+ * @type Function|String|null
+ * @default set_body_content
+ */
+ATTRS.bodyhandler = {
+    value: 'set_body_content'
+};
+
+/**
+ * The function which handles setting the content of the footer region.
+ * The specified function will be called with a context of the tooltip instance.
+ *
+ * By default, the footer is not set.
+ *
+ * @attribute footerhandler
+ * @type Function|String|null
+ * @default null
+ */
+ATTRS.footerhandler = {
+    value: null
+};
+
+/**
+ * Set the Y.Cache object to use.
+ *
+ * By default a new Y.Cache object will be created for each instance of the tooltip.
+ *
+ * In certain situations, where multiple tooltips may share the same cache, it may be preferable to
+ * seed this cache from the calling method.
+ *
+ * @attribute textcache
+ * @type Y.Cache|null
+ * @default null
+ */
+ATTRS.textcache = {
+    value: null
+};
+
+/**
+ * Set the default size of the Y.Cache object.
+ *
+ * This is only used if no textcache is specified.
+ *
+ * @attribute textcachesize
+ * @type Number
+ * @default 10
+ */
+ATTRS.textcachesize = {
+    value: 10
+};
+
+Y.extend(TOOLTIP, M.core.dialogue, {
+    // The bounding box.
+    bb: null,
+
+    // Any event listeners we may need to cancel later.
+    listenevents: [],
+
+    // Cache of objects we've already retrieved.
+    textcache: null,
+
+    // The align position. This differs for RTL languages so we calculate once and store.
+    alignpoints: [
+        Y.WidgetPositionAlign.TL,
+        Y.WidgetPositionAlign.RC
+    ],
+
+    initializer: function() {
+        // Set the initial values for the handlers.
+        // These cannot be set in the attributes section as context isn't present at that time.
+        if (!this.get('headerhandler')) {
+            this.set('headerhandler', this.set_header_content);
+        }
+        if (!this.get('bodyhandler')) {
+            this.set('bodyhandler', this.set_body_content);
+        }
+        if (!this.get('footerhandler')) {
+            this.set('footerhandler', function() {});
+        }
+
+        // Set up the dialogue with initial content.
+        this.setAttrs({
+            headerContent: this.get('initialheadertext'),
+            bodyContent: this.get('initialbodytext'),
+            footerContent: this.get('initialfootertext'),
+            zIndex: 150
+        });
+
+        // Hide and then render the dialogue.
+        this.hide();
+        this.render();
+
+        // Hook into a few useful areas.
+        this.bb = this.get('boundingBox');
+
+        // Change the alignment if this is an RTL language.
+        if (right_to_left()) {
+            this.alignpoints = [
+                Y.WidgetPositionAlign.TR,
+                Y.WidgetPositionAlign.LC
+            ];
+        }
+
+        // Set up the text cache if it's not set up already.
+        if (!this.get('textcache')) {
+            this.set('textcache', new Y.Cache({
+                // Set a reasonable maximum cache size to prevent memory growth.
+                max: this.get('textcachesize')
+            }));
+        }
+
+        // Disable the textcache when in developerdebug.
+        if (M.cfg.developerdebug) {
+            this.get('textcache').set('max', 0);
+        }
+
+        return this;
+    },
+
+    /**
+     * Display the tooltip for the clicked link.
+     *
+     * The anchor for the clicked link is used, additionally appending ajax=1 to the parameters.
+     *
+     * @method display_panel
+     * @param {EventFacade} e The event from the clicked link. This is used to determine the clicked URL.
+     */
+    display_panel: function(e) {
+        var clickedlink, thisevent, ajaxurl, config, cacheentry;
+
+        // Prevent the default click action and prevent the event triggering anything else.
+        e.preventDefault();
+        e.stopPropagation();
+
+        // Cancel any existing listeners and close the panel if it's already open.
+        this.cancel_events();
+
+        // Grab the clickedlink - this contains the URL we fetch and we align the panel to it.
+        clickedlink = e.target.ancestor('a', true);
+
+        // Align with the link that was clicked.
+        this.align(clickedlink, this.alignpoints);
+
+        // Reset the initial text to a spinner while we retrieve the text.
+        this.setAttrs({
+            headerContent: this.get('initialheadertext'),
+            bodyContent: this.get('initialbodytext'),
+            footerContent: this.get('initialfootertext')
+        });
+
+        // Now that initial setup has begun, show the panel.
+        this.show();
+
+        // Add some listen events to close on.
+        thisevent = this.bb.delegate('click', this.close_panel, SELECTORS.CLOSEBUTTON, this);
+        this.listenevents.push(thisevent);
+
+        thisevent = Y.one('body').on('key', this.close_panel, 'esc', this);
+        this.listenevents.push(thisevent);
+
+        // Listen for mousedownoutside events - clickoutside is broken on IE.
+        thisevent = this.bb.on('mousedownoutside', this.close_panel, this);
+        this.listenevents.push(thisevent);
+
+        ajaxurl = clickedlink.get('href');
+
+        cacheentry = this.get('textcache').retrieve(ajaxurl);
+        if (cacheentry) {
+            // The data from this help call was already cached so use that and avoid an AJAX call.
+            this._set_panel_contents(cacheentry.response);
+        } else {
+            // Retrieve the actual help text we should use.
+            config = {
+                method: 'get',
+                context: this,
+                sync: false,
+                data: {
+                    // We use a slightly different AJAX URL to the one on the anchor to allow non-JS fallback.
+                    ajax: 1
+                },
+                on: {
+                    complete: function(tid, response) {
+                        this._set_panel_contents(response.responseText, ajaxurl);
+                    }
+                }
+            };
+
+            Y.io(clickedlink.get('href'), config);
+        }
+    },
+
+    _set_panel_contents: function(response, ajaxurl) {
+        var responseobject;
+
+        // Attempt to parse the response into an object.
+        try {
+            responseobject = Y.JSON.parse(response);
+            if (responseobject.error) {
+                this.close_panel();
+                return new M.core.ajaxException(responseobject);
+            }
+        } catch (error) {
+            this.close_panel();
+            return new M.core.exception({
+                name: error.name,
+                message: "Unable to retrieve the requested content. The following error was returned: " + error.message
+            });
+        }
+
+        // Set the contents using various handlers.
+        // We must use Y.bind to ensure that the correct context is used when the default handlers are overridden.
+        Y.bind(this.get('headerhandler'), this, responseobject)();
+        Y.bind(this.get('bodyhandler'), this, responseobject)();
+        Y.bind(this.get('footerhandler'), this, responseobject)();
+
+        if (ajaxurl) {
+            // Ensure that this data is added to the cache.
+            this.get('textcache').add(ajaxurl, response);
+        }
+
+        this.get('buttons').header[0].focus();
+    },
+
+    set_header_content: function(responseobject) {
+        this.set('headerContent', responseobject.heading);
+    },
+
+    set_body_content: function(responseobject) {
+        var bodycontent = Y.Node.create('<div />')
+            .set('innerHTML', responseobject.text)
+            .setAttribute('role', 'alert')
+            .addClass(CSS.PANELTEXT);
+        this.set('bodyContent', bodycontent);
+    },
+
+    close_panel: function(e) {
+        // Hide the panel first.
+        this.hide();
+
+        // Cancel the listeners that we added in display_panel.
+        this.cancel_events();
+
+        // Prevent any default click that the close button may have.
+        if (e) {
+            e.preventDefault();
+        }
+    },
+
+    cancel_events: function() {
+        // Detach all listen events to prevent duplicate triggers.
+        var thisevent;
+        while (this.listenevents.length) {
+            thisevent = this.listenevents.shift();
+            thisevent.detach();
+        }
+    }
+});
+M.core = M.core || {};
+M.core.tooltip = M.core.tooltip = TOOLTIP;
diff --git a/lib/yui/src/tooltip/meta/tooltip.json b/lib/yui/src/tooltip/meta/tooltip.json
new file mode 100644 (file)
index 0000000..46ff22a
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "moodle-core-tooltip": {
+    "requires": [
+        "base",
+        "node",
+        "io-base",
+        "moodle-core-notification",
+        "json-parse",
+        "widget-position",
+        "widget-position-align",
+        "event-outside",
+        "cache"
+    ]
+  }
+}
index 5a05073..2a11a3a 100644 (file)
@@ -303,12 +303,15 @@ class Zend_XmlRpc_Request
             return false;
         }
 
+        // @see ZF-12293 - disable external entities for security purposes
+        $loadEntities = libxml_disable_entity_loader(true);
         try {
             $xml = new SimpleXMLElement($request);
         } catch (Exception $e) {
             // Not valid XML
             $this->_fault = new Zend_XmlRpc_Fault(631);
             $this->_fault->setEncoding($this->getEncoding());
+            libxml_disable_entity_loader($loadEntities);
             return false;
         }
 
@@ -317,6 +320,7 @@ class Zend_XmlRpc_Request
             // Missing method name
             $this->_fault = new Zend_XmlRpc_Fault(632);
             $this->_fault->setEncoding($this->getEncoding());
+            libxml_disable_entity_loader($loadEntities);
             return false;
         }
 
@@ -330,6 +334,7 @@ class Zend_XmlRpc_Request
                 if (!isset($param->value)) {
                     $this->_fault = new Zend_XmlRpc_Fault(633);
                     $this->_fault->setEncoding($this->getEncoding());
+                    libxml_disable_entity_loader($loadEntities);
                     return false;
                 }
 
@@ -340,6 +345,7 @@ class Zend_XmlRpc_Request
                 } catch (Exception $e) {
                     $this->_fault = new Zend_XmlRpc_Fault(636);
                     $this->_fault->setEncoding($this->getEncoding());
+                    libxml_disable_entity_loader($loadEntities);
                     return false;
                 }
             }
@@ -348,6 +354,7 @@ class Zend_XmlRpc_Request
             $this->_params = $argv;
         }
 
+        libxml_disable_entity_loader($loadEntities);
         $this->_xml = $request;
 
         return true;
index ef52efc..1df8e76 100644 (file)
@@ -9,4 +9,4 @@ Changes:
 * small fix to error reporting in reflection (MDL-21460, ZF-8980)
 * SOAP and XMLRPC servers overwrite the fault() functions
 * synced and renamed file to version in ZF 1.10.6 (MDL-30603, ZF-11080)
-
+* import security patch (MDL-34284, ZF2012-01, ZF-12293)
index 4318175..c46d776 100644 (file)
@@ -112,7 +112,7 @@ if ($mform->is_cancelled()) {
     }
 
     // Reset login lockout - we want to prevent any accidental confusion here.
-    login_unlock_account($user);
+    login_unlock_account($USER);
 
     // register success changing password
     unset_user_preference('auth_forcepasswordchange', $USER);
index fde60a4..a98af01 100644 (file)
@@ -202,7 +202,10 @@ if ($frm and isset($frm->username)) {                             // Login WITH
             // no wantsurl stored or external - go to homepage
             $urltogo = $CFG->wwwroot.'/';
             unset($SESSION->wantsurl);
+        }
 
+        // If the url to go to is the same as the site page, check for default homepage.
+        if ($urltogo == ($CFG->wwwroot . '/')) {
             $home_page = get_home_page();
             // Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my
             if ($home_page == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) {
index 04dbde7..aad2a58 100644 (file)
@@ -337,49 +337,8 @@ function assign_print_overview($courses, &$htmlarray) {
     // We do all possible database work here *outside* of the loop to ensure this scales.
     list($sqlassignmentids, $assignmentidparams) = $DB->get_in_or_equal($assignmentids);
 
-    // Build up and array of unmarked submissions indexed by assignment id/ userid
-    // for use where the user has grading rights on assignment.
-    $dbparams = array_merge(array(ASSIGN_SUBMISSION_STATUS_SUBMITTED), $assignmentidparams);
-    $rs = $DB->get_recordset_sql('SELECT
-                                      s.assignment as assignment,
-                                      s.userid as userid,
-                                      s.id as id,
-                                      s.status as status,
-                                      g.timemodified as timegraded
-                                  FROM {assign_submission} s
-                                  LEFT JOIN {assign_grades} g ON
-                                      s.userid = g.userid AND
-                                      s.assignment = g.assignment
-                                  WHERE
-                                      ( g.timemodified is NULL OR
-                                      s.timemodified > g.timemodified ) AND
-                                      s.timemodified IS NOT NULL AND
-                                      s.status = ? AND
-                                      s.assignment ' . $sqlassignmentids, $dbparams);
-
-    $unmarkedsubmissions = array();
-    foreach ($rs as $rd) {
-        $unmarkedsubmissions[$rd->assignment][$rd->userid] = $rd->id;
-    }
-    $rs->close();
-
-    // Get all user submissions, indexed by assignment id.
-    $dbparams = array_merge(array($USER->id, $USER->id), $assignmentidparams);
-    $mysubmissions = $DB->get_records_sql('SELECT
-                                               a.id AS assignment,
-                                               a.nosubmissions AS nosubmissions,
-                                               g.timemodified AS timemarked,
-                                               g.grader AS grader,
-                                               g.grade AS grade,
-                                               s.status AS status
-                                           FROM {assign} a
-                                           LEFT JOIN {assign_grades} g ON
-                                               g.assignment = a.id AND
-                                               g.userid = ?
-                                           LEFT JOIN {assign_submission} s ON
-                                               s.assignment = a.id AND
-                                               s.userid = ?
-                                           WHERE a.id ' . $sqlassignmentids, $dbparams);
+    $mysubmissions = null;
+    $unmarkedsubmissions = null;
 
     foreach ($assignments as $assignment) {
         // Do not show assignments that are not open.
@@ -415,6 +374,34 @@ function assign_print_overview($courses, &$htmlarray) {
         }
         $context = context_module::instance($assignment->coursemodule);
         if (has_capability('mod/assign:grade', $context)) {
+            if (!isset($unmarkedsubmissions)) {
+                // Build up and array of unmarked submissions indexed by assignment id/ userid
+                // for use where the user has grading rights on assignment.
+                $dbparams = array_merge(array(ASSIGN_SUBMISSION_STATUS_SUBMITTED), $assignmentidparams);
+                $rs = $DB->get_recordset_sql('SELECT
+                                                  s.assignment as assignment,
+                                                  s.userid as userid,
+                                                  s.id as id,
+                                                  s.status as status,
+                                                  g.timemodified as timegraded
+                                              FROM {assign_submission} s
+                                              LEFT JOIN {assign_grades} g ON
+                                                  s.userid = g.userid AND
+                                                  s.assignment = g.assignment
+                                              WHERE
+                                                  ( g.timemodified is NULL OR
+                                                  s.timemodified > g.timemodified ) AND
+                                                  s.timemodified IS NOT NULL AND
+                                                  s.status = ? AND
+                                                  s.assignment ' . $sqlassignmentids, $dbparams);
+
+                $unmarkedsubmissions = array();
+                foreach ($rs as $rd) {
+                    $unmarkedsubmissions[$rd->assignment][$rd->userid] = $rd->id;
+                }
+                $rs->close();
+            }
+
             // Count how many people can submit.
             $submissions = 0;
             if ($students = get_enrolled_users($context, 'mod/assign:view', 0, 'u.id')) {
@@ -435,6 +422,26 @@ function assign_print_overview($courses, &$htmlarray) {
             }
         }
         if (has_capability('mod/assign:submit', $context)) {
+            if (!isset($mysubmissions)) {
+                // Get all user submissions, indexed by assignment id.
+                $dbparams = array_merge(array($USER->id, $USER->id), $assignmentidparams);
+                $mysubmissions = $DB->get_records_sql('SELECT
+                                                           a.id AS assignment,
+                                                           a.nosubmissions AS nosubmissions,
+                                                           g.timemodified AS timemarked,
+                                                           g.grader AS grader,
+                                                           g.grade AS grade,
+                                                           s.status AS status
+                                                       FROM {assign} a
+                                                       LEFT JOIN {assign_grades} g ON
+                                                           g.assignment = a.id AND
+                                                           g.userid = ?
+                                                       LEFT JOIN {assign_submission} s ON
+                                                           s.assignment = a.id AND
+                                                           s.userid = ?
+                                                       WHERE a.id ' . $sqlassignmentids, $dbparams);
+            }
+
             $str .= '<div class="details">';
             $str .= get_string('mysubmission', 'assign');
             $submission = $mysubmissions[$assignment->id];
index 7aef1ae..7f6ed43 100644 (file)
@@ -58,6 +58,7 @@ $string['availabledate'] = 'Available from';
 $string['cannotdeletefiles'] = 'An error occurred and files could not be deleted';
 $string['cannotviewassignment'] = 'You can not view this assignment';
 $string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.';
+$string['closedassignment'] = 'The submission date for this assignment has been closed.';
 $string['comment'] = 'Comment';
 $string['commentinline'] = 'Comment inline';
 $string['commentinline_help'] = 'If enabled, the submission text will be copied into the feedback comment field during grading, making it easier to comment inline (using a different colour, perhaps) or to edit the original text.';
@@ -105,6 +106,7 @@ $string['feedbackfromteacher'] = 'Feedback from {$a}';
 $string['feedbackupdated'] = 'Submissions feedback updated for {$a} people';
 $string['finalize'] = 'Prevent submission updates';
 $string['finalizeerror'] = 'An error occurred and that submission could not be finalised';
+$string['futureaassignment'] = 'This assignment is not yet available.';
 $string['graded'] = 'Graded';
 $string['guestnosubmit'] = 'Sorry, guests are not allowed to submit an assignment. You have to log in/ register before you can submit your answer.';
 $string['guestnoupload'] = 'Sorry, guests are not allowed to upload';
index f9b93d4..667308a 100644 (file)
@@ -184,8 +184,11 @@ class assignment_upload extends assignment_base {
             echo '</form>';
             echo '</div>';
         } else if (!$this->isopen()) {
-            echo $OUTPUT->heading(get_string('nomoresubmissions','assignment'), 3);
-
+            if ($this->assignment->timeavailable < time()) {
+                echo $OUTPUT->heading(get_string('closedassignment','assignment'), 3);
+            } else {
+                echo $OUTPUT->heading(get_string('futureaassignment','assignment'), 3);
+            }
         } else if ($this->drafts_tracked() and $state = $this->is_finalized($submission)) {
             if ($state == ASSIGNMENT_STATUS_SUBMITTED) {
                 echo $OUTPUT->heading(get_string('submitedformarking','assignment'), 3);
@@ -1123,7 +1126,13 @@ class assignment_upload extends assignment_base {
      * @return bool                 Indicates if the submission was found to be complete
      */
     public function is_submitted_with_required_data($submission) {
-        return ($submission->timemodified AND $submission->data2);
+        if ($this->drafts_tracked()) {
+            $submitted = $submission->timemodified > 0 &&
+                         $submission->data2 == ASSIGNMENT_STATUS_SUBMITTED;
+        } else {
+            $submitted = $submission->numfiles > 0;
+        }
+        return $submitted;
     }
 }
 
index 77b2405..07aeacb 100644 (file)
@@ -63,12 +63,7 @@ class backup_choice_activity_structure_step extends backup_activity_structure_st
         // Define sources
         $choice->set_source_table('choice', array('id' => backup::VAR_ACTIVITYID));
 
-        $option->set_source_sql('
-            SELECT *
-            FROM {choice_options}
-            WHERE choiceid = ?
-            ORDER BY id',
-            array(backup::VAR_PARENTID));
+        $option->set_source_table('choice_options', array('choiceid' => backup::VAR_PARENTID), 'id ASC');
 
         // All the rest of elements only happen if we are including user info
         if ($userinfo) {
index 69258d0..2ad3580 100644 (file)
@@ -114,10 +114,7 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
                 array(backup::VAR_PARENTID));
 
             // Need posts ordered by id so parents are always before childs on restore
-            $post->set_source_sql("SELECT *
-                                     FROM {forum_posts}
-                                    WHERE discussion = :discussion
-                                 ORDER BY id", array('discussion' => backup::VAR_PARENTID));
+            $post->set_source_table('forum_posts', array('discussion' => backup::VAR_PARENTID), 'id ASC');
 
             $subscription->set_source_table('forum_subscriptions', array('forum' => backup::VAR_PARENTID));
 
index 149aa87..dc06db9 100644 (file)
@@ -212,6 +212,14 @@ function forum_update_instance($forum, $mform) {
 
     $DB->update_record('forum', $forum);
 
+    $modcontext = context_module::instance($forum->coursemodule);
+    if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
+        $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
+        foreach ($users as $user) {
+            forum_subscribe($user->id, $forum->id);
+        }
+    }
+
     forum_grade_item_update($forum);
 
     return true;
index 688c57b..35704e4 100644 (file)
@@ -112,6 +112,7 @@ class mod_forum_mod_form extends moodleform_mod {
             $choices[50] = '50';
             $mform->addElement('select', 'rssarticles', get_string('rssarticles'), $choices);
             $mform->addHelpButton('rssarticles', 'rssarticles', 'forum');
+            $mform->disabledIf('rssarticles', 'rsstype', 'eq', '0');
         }
 
 //-------------------------------------------------------------------------------
index 7ea5cd7..1bd5277 100644 (file)
@@ -111,6 +111,12 @@ if (!is_null($mode) and has_capability('mod/forum:managesubscriptions', $context
             redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
             break;
         case FORUM_INITIALSUBSCRIBE : // 2
+            if ($forum->forcesubscribe <> FORUM_INITIALSUBSCRIBE) {
+                $users = forum_get_potential_subscribers($context, 0, 'u.id, u.email', '');
+                foreach ($users as $user) {
+                    forum_subscribe($user->id, $forum->id);
+                }
+            }
             forum_forcesubscribe($forum->id, FORUM_INITIALSUBSCRIBE);
             redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
             break;
index b8178f6..55787fc 100644 (file)
@@ -31,6 +31,7 @@ $string['dndmedia'] = 'Media drag and drop';
 $string['dndresizeheight'] = 'Resize drag and drop height';
 $string['dndresizewidth'] = 'Resize drag and drop width';
 $string['dnduploadlabel'] = 'Add image to course page';
+$string['dnduploadlabeltext'] = 'Add text to course page';
 $string['label:addinstance'] = 'Add a new label';
 $string['labeltext'] = 'Label text';
 $string['modulename'] = 'Label';
index e265129..64208d4 100644 (file)
@@ -207,18 +207,24 @@ function label_supports($feature) {
  * @return array containing details of the files / types the mod can handle
  */
 function label_dndupload_register() {
+    $strdnd = get_string('dnduploadlabel', 'mod_label');
     if (get_config('label', 'dndmedia')) {
         $mediaextensions = file_get_typegroup('extension', 'web_image');
-        $strdnd = get_string('dnduploadlabel', 'mod_label');
         $files = array();
         foreach ($mediaextensions as $extn) {
             $extn = trim($extn, '.');
             $files[] = array('extension' => $extn, 'message' => $strdnd);
         }
-        return array('files' => $files);
+        $ret = array('files' => $files);
     } else {
-        return array();
+        $ret = array();
     }
+
+    $strdndtext = get_string('dnduploadlabeltext', 'mod_label');
+    return array_merge($ret, array('types' => array(
+        array('identifier' => 'text/html', 'message' => $strdndtext),
+        array('identifier' => 'text', 'message' => $strdndtext)
+    )));
 }
 
 /**
@@ -256,6 +262,8 @@ function label_dndupload_handle($uploadinfo) {
             $data->intro = file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_label', 'intro', 0,
                                                       null, $data->intro);
         }
+    } else if (!empty($uploadinfo->content)) {
+        $data->intro = $uploadinfo->content;
     }
 
     return label_add_instance($data, null);
@@ -333,4 +341,4 @@ function label_generate_resized_image(stored_file $file, $maxwidth, $maxheight)
     } else {
         return $img;
     }
-}
\ No newline at end of file
+}
index 83ab18b..fa880a0 100644 (file)
@@ -165,19 +165,10 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st
         // at this point (lesson, lesson_pages, lesson_answers)
         $lesson->set_source_table('lesson', array('id' => backup::VAR_ACTIVITYID));
         //we use SQL here as it must be ordered by prevpageid so that restore gets the pages in the right order.
-        $page->set_source_sql("
-                SELECT *
-                  FROM {lesson_pages}
-                 WHERE lessonid = ? ORDER BY prevpageid",
-                array(backup::VAR_PARENTID));
+        $page->set_source_table('lesson_pages', array('lessonid' => backup::VAR_PARENTID), 'prevpageid ASC');
 
         // We use SQL here as answers must be ordered by id so that the restore gets them in the right order
-        $answer->set_source_sql('
-                SELECT *
-                FROM {lesson_answers}
-                WHERE pageid = :pageid
-                ORDER BY id',
-                array('pageid' => backup::VAR_PARENTID));
+        $answer->set_source_table('lesson_answers', array('pageid' => backup::VAR_PARENTID), 'id ASC');
 
         // Check if we are also backing up user information
         if ($this->get_setting_value('userinfo')) {
index 46032a0..884fa23 100644 (file)
@@ -277,8 +277,11 @@ $remove = optional_param('remove', false, PARAM_INT);
 if ($remove && confirm_sesskey()) {
     // Remove a question from the quiz.
     // We require the user to have the 'use' capability on the question,
-    // so that then can add it back if they remove the wrong one by mistake.
-    quiz_require_question_use($remove);
+    // so that then can add it back if they remove the wrong one by mistake,
+    // but, if the question is missing, it can always be removed.
+    if ($DB->record_exists('question', array('id' => $remove))) {
+        quiz_require_question_use($remove);
+    }
     quiz_remove_question($quiz, $remove);
     quiz_delete_previews($quiz);
     quiz_update_sumgrades($quiz);
index 2a08a4b..38f4268 100644 (file)
 /** Attempt and review pages **/
 #page-mod-quiz-attempt #page .controls,
 #page-mod-quiz-summary #page .controls,
-#page-mod-quiz-review #page .controls {text-align: center;margin: 8px auto;}
+#page-mod-quiz-review #page .controls {
+    text-align: center;
+    margin: 8px auto;
+}
 
 #page-mod-quiz-attempt .submitbtns,
-#page-mod-quiz-review .submitbtns {clear: left; text-align: left; padding-top: 1.5em;}
+#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;}
+#page-mod-quiz-review.dir-rtl .submitbtns {
+    text-align: right;
+}
 
-body.jsenabled .questionflagcheckbox {display: none;}
+body.jsenabled .questionflagcheckbox {
+    display: none;
+}
 
 /** Mod quiz attempt **/
-.generalbox#passwordbox { /* Should probably match .generalbox#intro above */width:70%;margin-left:auto;margin-right:auto;}
-#passwordform {margin: 1em 0;}
+.generalbox#passwordbox {
+    /* Should probably match .generalbox#intro above */
+    width: 70%;
+    margin-left: auto;
+    margin-right: auto;
+}
+#passwordform {
+    margin: 1em 0;
+}
 
 /* Question navigation block. */
-#quiznojswarning {color: red;}
-#quiznojswarning {font-size: 0.7em;line-height: 1.1;}
-.jsenabled #quiznojswarning {display: none;}
+#quiznojswarning {
+    color: red;
+}
+#quiznojswarning {
+    font-size: 0.7em;
+    line-height: 1.1;
+}
+.jsenabled #quiznojswarning {
+    display: none;
+}
 
-.path-mod-quiz #user-picture {margin: 0.5em 0;}
-.path-mod-quiz #user-picture img {width: auto;height: auto;float: left;}
+.path-mod-quiz #user-picture {
+    margin: 0.5em 0;
+}
+.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 {
+    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;}
+.path-mod-quiz .qnbutton .thispageholder {
+    display: block;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+}
 
-.path-mod-quiz .qnbutton.thispage {border-color: #666;}
-.path-mod-quiz .qnbutton.thispage .thispageholder {border: 1px solid #666;}
+.path-mod-quiz .qnbutton.thispage {
+    border-color: #666;
+}
+.path-mod-quiz .qnbutton.thispage .thispageholder {
+    border: 1px solid #666;
+}
 
-.path-mod-quiz .qnbutton.flagged .trafficlight {background: url([[pix:quiz|navflagged]]) no-repeat top right;}
+.path-mod-quiz .qnbutton.flagged .trafficlight {
+    background: url([[pix: quiz|navflagged]]) no-repeat top right;
+}
 
 .path-mod-quiz .qnbutton.notyetanswered,
 .path-mod-quiz .qnbutton.requiresgrading,
-.path-mod-quiz .qnbutton.invalidanswer {background-color: white;}
-.path-mod-quiz .qnbutton.correct {background-color: #cfc;}
-.path-mod-quiz .qnbutton.correct .trafficlight {border-bottom: 3px solid #080;}
-.path-mod-quiz .qnbutton.partiallycorrect {background-color: #ffa;}
+.path-mod-quiz .qnbutton.invalidanswer {
+    background-color: white;
+}
+.path-mod-quiz .qnbutton.correct {
+    background-color: #cfc;
+}
+.path-mod-quiz .qnbutton.correct .trafficlight {
+    border-bottom: 3px solid #080;
+}
+.path-mod-quiz .qnbutton.partiallycorrect {
+    background-color: #ffa;
+}
 .path-mod-quiz .qnbutton.notanswered,
-.path-mod-quiz .qnbutton.incorrect {background-color: #fcc;}
+.path-mod-quiz .qnbutton.incorrect {
+    background-color: #fcc;
+}
 .path-mod-quiz .qnbutton.notanswered .trafficlight,
-.path-mod-quiz .qnbutton.incorrect .trafficlight {border-top: 3px solid #800;}
+.path-mod-quiz .qnbutton.incorrect .trafficlight {
+    border-top: 3px solid #800;
+}
 
-.path-mod-quiz .qnbutton.free:hover {text-decoration: underline;}
-.path-mod-quiz .qnbutton.free span {cursor: pointer;}
+.path-mod-quiz .qnbutton.free:hover {
+    text-decoration: underline;
+}
+.path-mod-quiz .qnbutton.free span {
+    cursor: pointer;
+}
 
-.path-mod-quiz .othernav {clear: both; margin: 0.5em 0;}
+.path-mod-quiz .othernav {
+    clear: both;
+    margin: 0.5em 0;
+}
 .path-mod-quiz .othernav a,
-.path-mod-quiz .othernav input {display: block;margin: 0.5em 0;}
+.path-mod-quiz .othernav input {
+    display: block;
+    margin: 0.5em 0;
+}
 
 /* Countdown timer. */
-#quiz-timer {display: none; margin-top: 1em;}
-#quiz-time-left {font-weight: bold;}
-#quiz-timer.timeleft15 {background: #ffffff;}
-#quiz-timer.timeleft14 {background: #ffeeee;}
-#quiz-timer.timeleft13 {background: #ffdddd;}
-#quiz-timer.timeleft12 {background: #ffcccc;}
-#quiz-timer.timeleft11 {background: #ffbbbb;}
-#quiz-timer.timeleft10 {background: #ffaaaa;}
-#quiz-timer.timeleft9 {background: #ff9999;}
-#quiz-timer.timeleft8 {background: #ff8888;}
-#quiz-timer.timeleft7 {background: #ff7777;}
-#quiz-timer.timeleft6 {background: #ff6666;}
-#quiz-timer.timeleft5 {background: #ff5555;}
-#quiz-timer.timeleft4 {background: #ff4444;}
-#quiz-timer.timeleft3 {background: #ff3333;}
-#quiz-timer.timeleft2 {background: #ff2222;}
-#quiz-timer.timeleft1 {background: #ff1111;}
-#quiz-timer.timeleft0 {background: #ff0000;}
+#quiz-timer {
+    display: none;
+    margin-top: 1em;
+}
+#quiz-time-left {
+    font-weight: bold;
+}
+#quiz-timer.timeleft15 {
+    background: #ffffff;
+}
+#quiz-timer.timeleft14 {
+    background: #ffeeee;
+}
+#quiz-timer.timeleft13 {
+    background: #ffdddd;
+}
+#quiz-timer.timeleft12 {
+    background: #ffcccc;
+}
+#quiz-timer.timeleft11 {
+    background: #ffbbbb;
+}
+#quiz-timer.timeleft10 {
+    background: #ffaaaa;
+}
+#quiz-timer.timeleft9 {
+    background: #ff9999;
+}
+#quiz-timer.timeleft8 {
+    background: #ff8888;
+}
+#quiz-timer.timeleft7 {
+    background: #ff7777;
+}
+#quiz-timer.timeleft6 {
+    background: #ff6666;
+}
+#quiz-timer.timeleft5 {
+    background: #ff5555;
+}
+#quiz-timer.timeleft4 {
+    background: #ff4444;
+}
+#quiz-timer.timeleft3 {
+    background: #ff3333;
+}
+#quiz-timer.timeleft2 {
+    background: #ff2222;
+}
+#quiz-timer.timeleft1 {
+    background: #ff1111;
+}
+#quiz-timer.timeleft0 {
+    background: #ff0000;
+}
 
 /** mod quiz mod **/
-#page-mod-quiz-mod #reviewoptionshdr .fitem {width: 23%;margin-left: 10px;}
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {width: 100%;text-align: left;margin-left: 0;}
+#page-mod-quiz-mod #reviewoptionshdr .fitem {
+    width: 23%;
+    margin-left: 10px;
+}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {
+    width: 100%;
+    text-align: left;
+    margin-left: 0;
+}
 
 #page-mod-quiz-edit div.question div.content .questiontext,
-#categoryquestions .questiontext {-o-text-overflow:ellipsis;text-overflow:ellipsis;position:relative;zoom:1;padding-left:0.3em;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
+#categoryquestions .questiontext {
+    text-overflow: ellipsis;
+    position: relative;
+    zoom: 1;
+    padding-left: 0.3em;
+    max-width: 40%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
 
 #page-mod-quiz-edit div.question div.content .questionname,
-#categoryquestions .questionname {white-space:nowrap;overflow:hidden;zoom:1;position:relative;max-width:20%;}
+#categoryquestions .questionname {
+    white-space: nowrap;
+    overflow: hidden;
+    zoom: 1;
+    position: relative;
+    max-width: 20%;
+}
 
 #page-mod-quiz-edit div.editq div.question div.content .singlequestion a .questionname,
-div.editq div.question div.content .singlequestion a .questiontext{text-decoration:underline;}
+div.editq div.question div.content .singlequestion a .questiontext {
+    text-decoration: underline;
+}
 
-#page-mod-quiz-edit.ie6 div.question div.content .questiontext {width:50%;}
-#page-mod-quiz-edit.ie6 div.question div.content .questionname {width:20%;}
+#page-mod-quiz-edit.ie6 div.question div.content .questiontext {
+    width: 50%;
+}
+#page-mod-quiz-edit.ie6 div.question div.content .questionname {
+    width: 20%;
+}
 
-#page-mod-quiz-mod #reviewoptionshdr .fitem {float: left;width: 23%;clear: none;}
+#page-mod-quiz-mod #reviewoptionshdr .fitem {
+    float: left;
+    width: 23%;
+    clear: none;
+}
 
-#page-mod-quiz-mod #reviewoptionshdr .fitemtitle {width: 100%;font-weight: bold;text-align: left;height: 2.5em;margin-left: 0;}
+#page-mod-quiz-mod #reviewoptionshdr .fitemtitle {
+    width: 100%;
+    font-weight: bold;
+    text-align: left;
+    height: 2.5em;
+    margin-left: 0;
+}
 
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {clear: left; margin: 0 0 1em; }
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup > span {float: left;clear: left; line-height: 1.7; }
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span label {margin-left: 0.4em;}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {
+    clear: left;
+    margin: 0 0 1em;
+}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup > span {
+    float: left;
+    clear: left;
+    line-height: 1.7;
+}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span label {
+    margin-left: 0.4em;
+}
 
-#page-mod-quiz-mod.dir-rtl #reviewoptionshdr .fitem {float: right;}
-#page-mod-quiz-mod.dir-rtl #reviewoptionshdr fieldset.fgroup span {float: right; clear: right;}
-#page-mod-quiz-mod.dir-rtl #reviewoptionshdr .fitemtitle {text-align: right;}
+#page-mod-quiz-mod.dir-rtl #reviewoptionshdr .fitem {
+    float: right;
+}
+#page-mod-quiz-mod.dir-rtl #reviewoptionshdr fieldset.fgroup span {
+    float: right;
+    clear: right;
+}
+#page-mod-quiz-mod.dir-rtl #reviewoptionshdr .fitemtitle {
+    text-align: right;
+}
 
 /** Mod quiz view **/
 #page-mod-quiz-view .quizinfo,
 #page-mod-quiz-view #page .quizgradefeedback,
-#page-mod-quiz-view #page .quizattempt {text-align: center;}
-#page-mod-quiz-view #page .quizattemptsummary td p {margin-top: 0;}
-table.quizattemptsummary .bestrow td {background-color: #e8e8e8;}
-table.quizattemptsummary .noreviewmessage {color: gray;}
-#page-mod-quiz-view .generaltable.quizattemptsummary {margin-left:auto;margin-right:auto;}
-#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; }
+#page-mod-quiz-view #page .quizattempt {
+    text-align: center;
+}
+#page-mod-quiz-view #page .quizattemptsummary td p {
+    margin-top: 0;
+}
+table.quizattemptsummary .bestrow td {
+    background-color: #e8e8e8;
+}
+table.quizattemptsummary .noreviewmessage {
+    color: gray;
+}
+#page-mod-quiz-view .generaltable.quizattemptsummary {
+    margin-left: auto;
+    margin-right: auto;
+}
+#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;
+}
 
 .mod-quiz .gradedattempt,
-.mod-quiz tr.gradedattempt td { background-color: #e8e8e8; }
+.mod-quiz tr.gradedattempt td {
+    background-color: #e8e8e8;
+}
 
-.quizattemptcounts {clear: left; text-align: center;}
+.quizattemptcounts {
+    clear: left;
+    text-align: center;
+}
 
 /** Mod quiz summary **/
-#page-mod-quiz-summary #content {text-align: center;}
-#page-mod-quiz-summary .questionflag { vertical-align: text-bottom; }
-#page-mod-quiz-summary #quiz-timer {text-align: center; margin-top: 1em;}
-#page-mod-quiz-summary .submitbtns {margin-top: 1.5em;}
+#page-mod-quiz-summary #content {
+    text-align: center;
+}
+#page-mod-quiz-summary .questionflag {
+    vertical-align: text-bottom;
+}
+#page-mod-quiz-summary #quiz-timer {
+    text-align: center;
+    margin-top: 1em;
+}
+#page-mod-quiz-summary .submitbtns {
+    margin-top: 1.5em;
+}
 @media print {
-    .quiz-secure-window * { display: none !important; }
+    .quiz-secure-window * {
+        display: none !important;
+    }
 }
 
 /** Mod quiz review **/
-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;}
+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%;}
-#page-mod-quiz-comment .mform fieldset {margin: 0;}
-#page-mod-quiz-comment .que {margin: 0;}
+#page-mod-quiz-comment .mform {
+    width: 100%;
+}
+#page-mod-quiz-comment .mform fieldset {
+    margin: 0;
+}
+#page-mod-quiz-comment .que {
+    margin: 0;
+}
 
 /** Mod quiz report **/
-#page-mod-quiz-report h2.main {clear: both;}
+#page-mod-quiz-report h2.main {
+    clear: both;
+}
 #page-mod-quiz-report div#commands,
-#page-mod-quiz-report .controls {text-align: center;}
-#page-mod-quiz-report .dubious{background-color: #fcc;}
-#page-mod-quiz-report .highlight{border :medium solid yellow;background-color:lightYellow;}
-#page-mod-quiz-report .negcovar{border :medium solid pink;}
-#page-mod-quiz-report .toggleincludeauto {text-align: center;}
-#page-mod-quiz-report .gradetheselink {font-size: 0.8em;}
-#page-mod-quiz-report .mform fieldset {margin: 0;}
-#page-mod-quiz-report fieldset.felement.fgroup {margin: 0;}
-#page-mod-quiz-report table.titlesleft td.c0 {font-weight: bold;}
-#page-mod-quiz-report table .numcol {text-align: center;vertical-align : middle !important;}
-
-#page-mod-quiz-report table#attempts {clear: both;width: 80%; margin: 0.2em auto;}
+#page-mod-quiz-report .controls {
+    text-align: center;
+}
+#page-mod-quiz-report .dubious {
+    background-color: #fcc;
+}
+#page-mod-quiz-report .highlight {
+    border : medium solid yellow;
+    background-color: lightYellow;
+}
+#page-mod-quiz-report .negcovar {
+    border : medium solid pink;
+}
+#page-mod-quiz-report .toggleincludeauto {
+    text-align: center;
+}
+#page-mod-quiz-report .gradetheselink {
+    font-size: 0.8em;
+}
+#page-mod-quiz-report .mform fieldset {
+    margin: 0;
+}
+#page-mod-quiz-report fieldset.felement.fgroup {
+    margin: 0;
+}
+#page-mod-quiz-report table.titlesleft td.c0 {
+    font-weight: bold;
+}
+#page-mod-quiz-report table .numcol {
+    text-align: center;
+    vertical-align : middle !important;
+}
+
+#page-mod-quiz-report table#attempts {
+    clear: both;
+    width: 80%;
+    margin: 0.2em auto;
+}
 #page-mod-quiz-report table#attempts .header,
-#page-mod-quiz-report table#attempts .cell{padding: 4px;}
-#page-mod-quiz-report table#attempts .header .commands {display: inline;}
-#page-mod-quiz-report table#attempts .picture {width: 40px;}
-#page-mod-quiz-report table#attempts td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: middle;}
-#page-mod-quiz-report table#attempts .header {text-align: left;}
-#page-mod-quiz-report table#attempts .picture {text-align: center !important;}
+#page-mod-quiz-report table#attempts .cell {
+    padding: 4px;
+}
+#page-mod-quiz-report table#attempts .header .commands {
+    display: inline;
+}
+#page-mod-quiz-report table#attempts .picture {
+    width: 40px;
+}
+#page-mod-quiz-report table#attempts td {
+    border-left-width: 1px;
+    border-right-width: 1px;
+    border-left-style: solid;
+    border-right-style: solid;
+    vertical-align: middle;
+}
+#page-mod-quiz-report table#attempts .header {
+    text-align: left;
+}
+#page-mod-quiz-report table#attempts .picture {
+    text-align: center !important;
+}
 #page-mod-quiz-report table#attempts.grades span.que,
-#page-mod-quiz-report table#attempts span.avgcell {white-space: nowrap;}
-#page-mod-quiz-report table#attempts span.que .requiresgrading {white-space: normal;}
-#page-mod-quiz-report table#attempts .questionflag { vertical-align: text-bottom; padding-left: 6px; }
-.dir-rtl#page-mod-quiz-report table#attempts .questionflag { padding-right: 6px; padding-left: 0; }
+#page-mod-quiz-report table#attempts span.avgcell {
+    white-space: nowrap;
+}
+#page-mod-quiz-report table#attempts span.que .requiresgrading {
+    white-space: normal;
+}
+#page-mod-quiz-report table#attempts .questionflag {
+    vertical-align: text-bottom;
+    padding-left: 6px;
+}
+.dir-rtl#page-mod-quiz-report table#attempts .questionflag {
+    padding-right: 6px;
+    padding-left: 0;
+}
 
-#page-mod-quiz-report .graph.flexible-wrap {text-align:center; overflow:auto;}
+#page-mod-quiz-report .graph.flexible-wrap {
+    text-align: center;
+    overflow: auto;
+}
 
-#page-mod-quiz-report #cachingnotice {margin-bottom: 1em; padding: 0.2em; }
-#page-mod-quiz-report #cachingnotice .singlebutton {margin: 0.5em 0 0;}
-#page-mod-quiz-report .bold .reviewlink {font-weight: normal;}
+#page-mod-quiz-report #cachingnotice {
+    margin-bottom: 1em;
+    padding: 0.2em;
+}
+#page-mod-quiz-report #cachingnotice .singlebutton {
+    margin: 0.5em 0 0;
+}
+#page-mod-quiz-report .bold .reviewlink {
+    font-weight: normal;
+}
 
 /** Mod quiz edit **/
-#page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;}
+#page-mod-quiz-edit h2.main {
+    display: inline;
+    padding-right: 1em;
+    clear: left;
+}
 
-#categoryquestions .r1 {background: #e4e4e4;}
-#categoryquestions .r1.highlight {background-color:#AAFFAA;}
-#categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;}
+#categoryquestions .r1 {
+    background: #e4e4e4;
+}
+#categoryquestions .r1.highlight {
+    background-color: #AAFFAA;
+}
+#categoryquestions .header {
+    text-align: center;
+    padding: 0 2px;
+    border: 0 none;
+}
 #categoryquestions th.modifiername .sorters,
-#categoryquestions th.creatorname .sorters {font-weight: normal;font-size: 0.8em;}
-table#categoryquestions {width: 100%;overflow: hidden;table-layout: fixed;}
-#categoryquestions .iconcol {width: 15px;text-align: center;padding: 0;}
-#categoryquestions .checkbox {width: 19px;text-align: center;padding: 0;}
-#categoryquestions .qtype {text-align: center;}
-#categoryquestions .qtype {width: 28px;padding: 0;}
-#categoryquestions .questiontext p {margin: 0;}
-
-#page-mod-quiz-edit div.quizcontents {float:left;width:70%;display:block;clear:left;}
-#page-mod-quiz-edit div.quizwhenbankcollapsed {width:100%;}
-#page-mod-quiz-edit div.quizpage {display:block;clear:both;width:100%;}
-#page-mod-quiz-edit div.quizpage span.pagetitle {margin-top:0.3em;float:left;display:block;color:#006;}
-#page-mod-quiz-edit div.quizpage .pagecontent {margin-top:0.3em;display:block;float:left;position:relative;margin-left:0.3em;margin-bottom:0.2em;border-left:thin solid #777;line-height:1.3em;-webkit-border-radius:0.6em;-webkit-border-radius-bottomleft:0;-webkit-border-radius-topleft:0;border-radius:0.6em;border-radius-bottomleft:0;border-radius-topleft:0;width:88%;padding:0.15em 0 0.3em;background-color:#d6d6d6;}
-#page-mod-quiz-edit div.quizpage .pagecontent .pagestatus {-webkit-border-radius-bottomright:0.3em;-webkit-border-radius-topright:0.3em;border-radius-bottomright:0.3em;border-radius-topright:0.3em;margin:0.3em;padding:0.1em 0.1em 0.1em 0.3em;background-color:#eee;font-weight:bold;}
-#page-mod-quiz-edit div.quizpage .pagecontent form#addquestion{background-color:#fff;}
-#page-mod-quiz-edit div.quizpage .pagecontent form.randomquestionform div {/* it is a mystery why this has to be inline-table but otherwise the layout gets screwed, even if it is "inline" */display: inline-table;}
-#page-mod-quiz-edit div.quizpage .pagecontent form.randomquestionform div input {display: inline;}
-
-#page-mod-quiz-edit .addpage{clear:both;padding-top:0.3em;float:right;margin-right:2em;}
-#page-mod-quiz-edit .statusdisplay {background-color: #ffc;clear: both;margin: 0.3em 1em 0.3em 0;padding: 1px ; /* Stop margin collapse. */}
-#page-mod-quiz-edit .statusdisplay p {margin: 0.4em;}
-
-#page-mod-quiz-edit div.reorder .reordercontrols{clear:both;padding-right:1em;margin-top:0.5em;padding-top:0.5em;padding-bottom:0.5em}
-#page-mod-quiz-edit div.reorder .reordercontrols .moveselectedonpage{clear:right;float:right;padding:0.5em 0.3em;text-align:right;}
+#categoryquestions th.creatorname .sorters {
+    font-weight: normal;
+    font-size: 0.8em;
+}
+table#categoryquestions {
+    width: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+}
+#categoryquestions .iconcol {
+    width: 15px;
+    text-align: center;
+    padding: 0;
+}
+#categoryquestions .checkbox {
+    width: 19px;
+    text-align: center;
+    padding: 0;
+}
+#categoryquestions .qtype {
+    text-align: center;
+}
+#categoryquestions .qtype {
+    width: 28px;
+    padding: 0;
+}
+#categoryquestions .questiontext p {
+    margin: 0;
+}
+
+#page-mod-quiz-edit div.quizcontents {
+    float: left;
+    width: 70%;
+    display: block;
+    clear: left;
+}
+#page-mod-quiz-edit div.quizwhenbankcollapsed {
+    width: 100%;
+}
+#page-mod-quiz-edit div.quizpage {
+    display: block;
+    clear: both;
+    width: 100%;
+}
+#page-mod-quiz-edit div.quizpage span.pagetitle {
+    margin-top: 0.3em;
+    float: left;
+    display: block;
+    color: #006;
+}
+#page-mod-quiz-edit div.quizpage .pagecontent {
+    margin-top: 0.3em;
+    display: block;
+    float: left;
+    position: relative;
+    margin-left: 0.3em;
+    margin-bottom: 0.2em;
+    border-left: thin solid #777;
+    line-height: 1.3em;
+    border-radius: 0.6em;
+    border-radius-bottomleft: 0;
+    border-radius-topleft: 0;
+    width: 88%;
+    padding: 0.15em 0 0.3em;
+    background-color: #d6d6d6;
+}
+#page-mod-quiz-edit div.quizpage .pagecontent .pagestatus {
+    border-radius-bottomright: 0.3em;
+    border-radius-topright: 0.3em;
+    margin: 0.3em;
+    padding: 0.1em 0.1em 0.1em 0.3em;
+    background-color: #eee;
+    font-weight: bold;
+}
+#page-mod-quiz-edit div.quizpage .pagecontent form#addquestion {
+    background-color: #fff;
+}
+#page-mod-quiz-edit div.quizpage .pagecontent form.randomquestionform div {
+    /* it is a mystery why this has to be inline-table but otherwise the layout gets screwed, even if it is "inline" */display: inline-table;
+}
+#page-mod-quiz-edit div.quizpage .pagecontent form.randomquestionform div input {
+    display: inline;
+}
+
+#page-mod-quiz-edit .addpage {
+    clear: both;
+    padding-top: 0.3em;
+    float: right;
+    margin-right: 2em;
+}
+#page-mod-quiz-edit .statusdisplay {
+    background-color: #ffc;
+    clear: both;
+    margin: 0.3em 1em 0.3em 0;
+    padding: 1px ;
+    /* Stop margin collapse. */
+}
+#page-mod-quiz-edit .statusdisplay p {
+    margin: 0.4em;
+}
+
+#page-mod-quiz-edit div.reorder .reordercontrols {
+    clear: both;
+    padding-right: 1em;
+    margin-top: 0.5em;
+    padding-top: 0.5em;
+    padding-bottom: 0.5em
+}
+#page-mod-quiz-edit div.reorder .reordercontrols .moveselectedonpage {
+    clear: right;
+    float: right;
+    padding: 0.5em 0.3em;
+    text-align: right;
+}
 #page-mod-quiz-edit div.reorder .reordercontrols .addnewpagesafterselected,
-#page-mod-quiz-edit .repaginatecommand {float:right;clear:right;padding-right:1em;}
-#page-mod-quiz-edit div.reorder .reordercontrols .deleteselected{float:right;margin-right:1em;}
-#page-mod-quiz-edit div.reorder div.question{padding-top:0.2em;}
-#page-mod-quiz-edit div.reorder div.question div.qnum {width:2.9em;padding-top:0.1em;}
-#page-mod-quiz-edit .reorder div.question div.content {width:87%;float:left;position:relative;-webkit-border-radius:0.3em;-webkit-border-radius-bottomleft:0;-webkit-border-radius-topleft:0;border-radius:0.3em;border-radius-bottomleft:0;border-radius-topleft:0;line-height:1.2em;padding:0.1em;background-color:#F9F9F9;}
-#page-mod-quiz-edit .reorder .questioncontentcontainer .quiz_randomquestion {position:relative;}
-#page-mod-quiz-edit .reorder div.question div.content div.quiz_randomquestion {line-height:1em;}
-
-#page-mod-quiz-edit .reorder .questioncontentcontainer {position:relative;overflow:hidden;white-space:nowrap;}
-#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory {overflow:hidden;white-space:nowrap;display:inline;float:none;}
-#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label {max-width: 25%;overflow:hidden;padding-left:0.3em;white-space:nowrap;display:inline-block;}
-#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestionfromcategory label {overflow:hidden;white-space:nowrap;display:inline-block;}
+#page-mod-quiz-edit .repaginatecommand {
+    float: right;
+    clear: right;
+    padding-right: 1em;
+}
+#page-mod-quiz-edit div.reorder .reordercontrols .deleteselected {
+    float: right;
+    margin-right: 1em;
+}
+#page-mod-quiz-edit div.reorder div.question {
+    padding-top: 0.2em;
+}
+#page-mod-quiz-edit div.reorder div.question div.qnum {
+    width: 2.9em;
+    padding-top: 0.1em;
+}
+#page-mod-quiz-edit .reorder div.question div.content {
+    width: 87%;
+    float: left;
+    position: relative;
+    border-radius: 0.3em;
+    border-radius-bottomleft: 0;
+    border-radius-topleft: 0;
+    line-height: 1.2em;
+    padding: 0.1em;
+    background-color: #F9F9F9;
+}
+#page-mod-quiz-edit .reorder .questioncontentcontainer .quiz_randomquestion {
+    position: relative;
+}
+#page-mod-quiz-edit .reorder div.question div.content div.quiz_randomquestion {
+    line-height: 1em;
+}
+
+#page-mod-quiz-edit .reorder .questioncontentcontainer {
+    position: relative;
+    overflow: hidden;
+    white-space: nowrap;
+}
+#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory {
+    overflow: hidden;
+    white-space: nowrap;
+    display: inline;
+    float: none;
+}
+#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label {
+    max-width: 25%;
+    overflow: hidden;
+    padding-left: 0.3em;
+    white-space: nowrap;
+    display: inline-block;
+}
+#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestionfromcategory label {
+    overflow: hidden;
+    white-space: nowrap;
+    display: inline-block;
+}
 #page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestionfromcategory,
-#page-mod-quiz-edit .reorder div.question div.content .questionpreview {display:inline;float:none;}
-
-#page-mod-quiz-edit .reorder fieldset{display:inline;}
-#page-mod-quiz-edit div.reorder div.question div.qnum{text-align:right;font-size:1em;}
-
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist {padding-left:0.2em;padding-right:0.2em;clear:both;margin:0.5em;margin-top:0.8em;}
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist .totalquestionsinrandomqcategory{overflow:auto;white-space:normal;}
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul{list-style-type:none;margin:0;padding:0;}
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li {clear:left;width:100%;overflow:hidden;white-space:nowrap;}
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li img{padding-right:0.3em;}
-#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li span{display:inline;}
-#page-mod-quiz-edit .questioncontentcontainer a {text-decoration:underline;}
-#page-mod-quiz-edit .questioncontentcontainer div.singlequestion a{text-decoration:underline;}
-#page-mod-quiz-edit .questioncontentcontainer .randomquestioncategory{font-weight:bold;}
-
-#page-mod-quiz-edit div.question {clear:left;width:100%;}
-#page-mod-quiz-edit div.question div.qnum {display:block;float:left;width:1.4em;padding-right:0.3em;padding-left:0;z-index:99;text-align:right;color:#333;}
-#page-mod-quiz-edit div.question div.questioncontainer{background-color:#ffc;}
-#page-mod-quiz-edit div.editq div.question div.content{width:87%;float:left;position:relative;-webkit-border-radius:0.6em;-webkit-border-radius-bottomleft:0;-webkit-border-radius-topleft:0;border-radius:0.6em;border-radius-bottomleft:0;border-radius-topleft:0;line-height:1.4em;padding:0.5em;}
-#page-mod-quiz-edit div.question div.content div.points{top:0.5em;border-left:0.4em solid #FFF;width:8.5em;padding:0.2em;line-height:1em;max-width:30%;position:absolute;right:60px;-webkit-border-radius:0.2em;-webkit-border-radius-bottomleft:0;-webkit-border-radius-topleft:0;border-radius:0.2em;border-radius-bottomleft:0;border-radius-topleft:0;z-index:900;display:block;margin:0;background-color:#ddf;}
-#page-mod-quiz-edit div.question div.content div.points input{width:2em;padding:0;}
-#page-mod-quiz-edit div.question div.content div.points input.pointssubmitbutton{width:auto;}
-#page-mod-quiz-edit div.question div.content div.qorder {line-height:1em;max-width:30%;position:absolute;right:50px;-webkit-border-radius:0.2em;-webkit-border-radius-bottomleft:0;-webkit-border-radius-topleft:0;border-radius:0.2em;border-radius-bottomleft:0;border-radius-topleft:0;z-index:900;display:block;margin:0;background-color:#ddf;}
-#page-mod-quiz-edit div.question div.content .editicon {width:15px;}
+#page-mod-quiz-edit .reorder div.question div.content .questionpreview {
+    display: inline;
+    float: none;
+}
+
+#page-mod-quiz-edit .reorder fieldset {
+    display: inline;
+}
+#page-mod-quiz-edit div.reorder div.question div.qnum {
+    text-align: right;
+    font-size: 1em;
+}
+
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist {
+    padding-left: 0.2em;
+    padding-right: 0.2em;
+    clear: both;
+    margin: 0.5em;
+    margin-top: 0.8em;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist .totalquestionsinrandomqcategory {
+    overflow: auto;
+    white-space: normal;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul {
+    list-style-type: none;
+    margin: 0;
+    padding: 0;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li {
+    clear: left;
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li img {
+    padding-right: 0.3em;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.randomquestionqlist ul li span {
+    display: inline;
+}
+#page-mod-quiz-edit .questioncontentcontainer a {
+    text-decoration: underline;
+}
+#page-mod-quiz-edit .questioncontentcontainer div.singlequestion a {
+    text-decoration: underline;
+}
+#page-mod-quiz-edit .questioncontentcontainer .randomquestioncategory {
+    font-weight: bold;
+}
+
+#page-mod-quiz-edit div.question {
+    clear: left;
+    width: 100%;
+}
+#page-mod-quiz-edit div.question div.qnum {
+    display: block;
+    float: left;
+    width: 1.4em;
+    padding-right: 0.3em;
+    padding-left: 0;
+    z-index: 99;
+    text-align: right;
+    color: #333;
+}
+#page-mod-quiz-edit div.question div.questioncontainer {
+    background-color: #ffc;
+}
+#page-mod-quiz-edit div.editq div.question div.content {
+    width: 87%;
+    float: left;
+    position: relative;
+    border-radius: 0.6em;
+    border-radius-bottomleft: 0;
+    border-radius-topleft: 0;
+    line-height: 1.4em;
+    padding: 0.5em;
+}
+#page-mod-quiz-edit div.question div.content div.points {
+    top: 0.5em;
+    border-left: 0.4em solid #FFF;
+    width: 8.5em;
+    padding: 0.2em;
+    line-height: 1em;
+    max-width: 30%;
+    position: absolute;
+    right: 60px;
+    border-radius: 0.2em;
+    border-radius-bottomleft: 0;
+    border-radius-topleft: 0;
+    z-index: 900;
+    display: block;
+    margin: 0;
+    background-color: #ddf;
+}
+#page-mod-quiz-edit div.question div.content div.points input {
+    width: 2em;
+    padding: 0;
+}
+#page-mod-quiz-edit div.question div.content div.points input.pointssubmitbutton {
+    width: auto;
+}
+#page-mod-quiz-edit div.question div.content div.qorder {
+    line-height: 1em;
+    max-width: 30%;
+    position: absolute;
+    right: 50px;
+    border-radius: 0.2em;
+    border-radius-bottomleft: 0;
+    border-radius-topleft: 0;
+    z-index: 900;
+    display: block;
+    margin: 0;
+    background-color: #ddf;
+}
+#page-mod-quiz-edit div.question div.content .editicon {
+    width: 15px;
+}
 #page-mod-quiz-edit div.question div.content .singlequestion .questionname,
-#page-mod-quiz-edit div.question div.content .singlequestion .questiontext{display:inline-block;}
-#page-mod-quiz-edit div.question div.content .singlequestion .questionpreview{background-color:#eee;}
-#page-mod-quiz-edit div.question div.content .questiontype{display:block;clear:left;float:left;}
-#page-mod-quiz-edit.dir-rtl div.question div.content .questiontype {clear: right;float: right;}
-#page-mod-quiz-edit div.question div.content .questionpreview {display:block;float:left;margin-left:0.3em;padding-left:0.2em;padding-right:0.2em;}
-#page-mod-quiz-edit div.question div.content .questionpreview a{background-color:#eee;}
-#page-mod-quiz-edit div.question div.content div.quiz_randomquestion .questionpreview{display:inline;float:none;}
-#page-mod-quiz-edit div.question div.content div.questioncontrols{float:right;width:55px;position:absolute;right:0.3em;top:0;z-index:900;display:block;padding:0.2em;background-color:#F9F9F9;text-align:right;}
-#page-mod-quiz-edit div.question div.content div.questioncontrols img.upwithoutdown{padding-right:12px;display:inline;}
-#page-mod-quiz-edit div.question div.content .questiontext {font-weight:bold;}
-#page-mod-quiz-edit div.question div.content .questiontype{font-style:italic;}
-
-#page-mod-quiz-edit .editq div.question div.qnum {padding-top:0.2em;}
-#page-mod-quiz-edit .editq div.question {padding-top:0.3em;}
-#page-mod-quiz-edit .editq div.questioncontentcontainer  div.singlequestion img {float:left;padding-top:0.3em;padding-right:0.3em;}
-#page-mod-quiz-edit .editq div.question div.content {background-color:#F9F9F9;}
-#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory{margin-top:0.4em;position:relative;display:inline-block;}
-#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory a{display:block;max-width:15em;overflow: hidden;text-overflow:ellipsis;-o-text-overflow:ellipsis;white-space: nowrap;float:left;position:relative;}
-#page-mod-quiz-edit .editq div.question div.content .questionpreview {float:left;}
-#page-mod-quiz-edit .editq div.question div.content .questionpreview a{font-weight:normal;margin-left:0em;display:inline;float:none;}
-#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory .questionpreview img{padding-right:0.3em;}
+#page-mod-quiz-edit div.question div.content .singlequestion .questiontext {
+    display: inline-block;
+}
+#page-mod-quiz-edit div.question div.content .singlequestion .questionpreview {
+    background-color: #eee;
+}
+#page-mod-quiz-edit div.question div.content .questiontype {
+    display: block;
+    clear: left;
+    float: left;
+}
+#page-mod-quiz-edit.dir-rtl div.question div.content .questiontype {
+    clear: right;
+    float: right;
+}
+#page-mod-quiz-edit div.question div.content .questionpreview {
+    display: block;
+    float: left;
+    margin-left: 0.3em;
+    padding-left: 0.2em;
+    padding-right: 0.2em;
+}
+#page-mod-quiz-edit div.question div.content .questionpreview a {
+    background-color: #eee;
+}
+#page-mod-quiz-edit div.question div.content div.quiz_randomquestion .questionpreview {
+    display: inline;
+    float: none;
+}
+#page-mod-quiz-edit div.question div.content div.questioncontrols {
+    float: right;
+    width: 55px;
+    position: absolute;
+    right: 0.3em;
+    top: 0;
+    z-index: 900;
+    display: block;
+    padding: 0.2em;
+    background-color: #F9F9F9;
+    text-align: right;
+}
+#page-mod-quiz-edit div.question div.content div.questioncontrols img.upwithoutdown {
+    padding-right: 12px;
+    display: inline;
+}
+#page-mod-quiz-edit div.question div.content .questiontext {
+    font-weight: bold;
+}
+#page-mod-quiz-edit div.question div.content .questiontype {
+    font-style: italic;
+}
+
+#page-mod-quiz-edit .editq div.question div.qnum {
+    padding-top: 0.2em;
+}
+#page-mod-quiz-edit .editq div.question {
+    padding-top: 0.3em;
+}
+#page-mod-quiz-edit .editq div.questioncontentcontainer  div.singlequestion img {
+    float: left;
+    padding-top: 0.3em;
+    padding-right: 0.3em;
+}
+#page-mod-quiz-edit .editq div.question div.content {
+    background-color: #F9F9F9;
+}
+#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory {
+    margin-top: 0.4em;
+    position: relative;
+    display: inline-block;
+}
+#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory a {
+    display: block;
+    max-width: 15em;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    float: left;
+    position: relative;
+}
+#page-mod-quiz-edit .editq div.question div.content .questionpreview {
+    float: left;
+}
+#page-mod-quiz-edit .editq div.question div.content .questionpreview a {
+    font-weight: normal;
+    margin-left: 0em;
+    display: inline;
+    float: none;
+}
+#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory .questionpreview img {
+    padding-right: 0.3em;
+}
 #page-mod-quiz-edit .editq div.question div.content .singlequestion .questioneditbutton .questionname,
-#page-mod-quiz-edit .editq div.question div.content .singlequestion .questioneditbutton .questiontext{float:left;}
+#page-mod-quiz-edit .editq div.question div.content .singlequestion .questioneditbutton .questiontext {
+    float: left;
+}
 #page-mod-quiz-edit .reorder div.question div.content .singlequestion.missingtype .questionname,
-#page-mod-quiz-edit .editq div.question div.content .singlequestion.missingtype .questionname {font-style: italic; max-width: 75%;}
-#page-mod-quiz-edit .editq div.question div.description div.content .questiontext {max-width: 75%;}
-#page-mod-quiz-edit .editq div.question div.qnum{font-size:1.5em;}
+#page-mod-quiz-edit .editq div.question div.content .singlequestion.missingtype .questionname {
+    font-style: italic;
+    max-width: 75%;
+}
+#page-mod-quiz-edit .editq div.question div.description div.content .questiontext {
+    max-width: 75%;
+}
+#page-mod-quiz-edit .editq div.question div.qnum {
+    font-size: 1.5em;
+}
 
 table#categoryquestions td,
-#page-mod-quiz-edit table#categoryquestions th{overflow:hidden;white-space:nowrap;}
+#page-mod-quiz-edit table#categoryquestions th {
+    overflow: hidden;
+    white-space: nowrap;
+}