Merge branch 'wip-MDL-38000-m25' of git://github.com/samhemelryk/moodle
authorDamyon Wiese <damyon@moodle.com>
Tue, 19 Feb 2013 05:48:12 +0000 (13:48 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 19 Feb 2013 05:48:12 +0000 (13:48 +0800)
76 files changed:
README.txt
admin/tool/behat/cli/util.php
admin/tool/behat/tests/behat/nasty_strings.feature [new file with mode: 0644]
blocks/html/backup/moodle1/lib.php
blocks/recent_activity/block_recent_activity.php
blocks/rss_client/backup/moodle1/lib.php
blocks/settings/lang/en/block_settings.php
blog/edit_form.php
cache/classes/config.php
cache/classes/definition.php
cache/classes/factory.php
cache/classes/helper.php
cache/locallib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
comment/comment_ajax.php
comment/lib.php
course/lib.php
course/tests/behat/behat_course.php
course/tests/courselib_test.php
course/yui/toolboxes/toolboxes.js
enrol/imsenterprise/locallib.php
enrol/locallib.php
enrol/manual/ajax.php
enrol/manual/yui/quickenrolment/quickenrolment.js
filter/activitynames/filter.php
grade/report/grader/module.js
lang/en/admin.php
lang/en/grading.php
lang/en/rating.php
lib/behat/behat_base.php
lib/behat/form_field/behat_form_editor.php
lib/db/services.php
lib/db/upgrade.php
lib/filelib.php
lib/filterlib.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/phpunit/classes/util.php
lib/statslib.php
lib/testing/classes/nasty_strings.php [new file with mode: 0644]
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 [new file with mode: 0644]
lib/upgradelib.php
mod/assign/externallib.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/submission/file/locallib.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/data/renderer.php
mod/label/db/upgrade.php
mod/label/lib.php
mod/label/version.php
mod/quiz/lib.php
mod/quiz/version.php
mod/scorm/report/basic/report.php
mod/scorm/report/graphs/graph.php
mod/scorm/report/interactions/report.php
question/format/blackboard/tests/fixtures/sample_blackboard.dat
question/format/blackboard_six/formatbase.php
question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat
question/format/xml/tests/fixtures/truefalse.xml
report/courseoverview/index.php
report/courseoverview/settings.php
report/log/locallib.php
tag/coursetagslib.php
theme/base/style/core.css
user/externallib.php
user/tests/externallib_test.php
version.php

index 245848b..b1017df 100644 (file)
@@ -8,7 +8,7 @@ a few minutes:
 1) Move the Moodle files into your web directory.
 
 2) Create a single database for Moodle to store all
-   it's tables in (or choose an existing database).
+   its tables in (or choose an existing database).
 
 3) Visit your Moodle site with a browser, you should
    be taken to the install.php script, which will lead
index cfd3581..8cba367 100644 (file)
@@ -84,6 +84,7 @@ error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
+// Getting $CFG data.
 require_once(__DIR__ . '/../../../../config.php');
 
 // CFG->behat_prefix must be set and with value different than CFG->prefix and phpunit_prefix.
@@ -141,6 +142,10 @@ foreach ($vars as $var) {
 $CFG->noemailever = true;
 $CFG->passwordsaltmain = 'moodle';
 
+// Unset cache and temp directories to reset them again with the new $CFG->dataroot.
+unset($CFG->cachedir);
+unset($CFG->tempdir);
+
 // Continues setup.
 define('ABORT_AFTER_CONFIG_CANCEL', true);
 require("$CFG->dirroot/lib/setup.php");
diff --git a/admin/tool/behat/tests/behat/nasty_strings.feature b/admin/tool/behat/tests/behat/nasty_strings.feature
new file mode 100644 (file)
index 0000000..9135798
--- /dev/null
@@ -0,0 +1,60 @@
+@tool_behat
+Feature: Transform steps arguments
+  In order to write tests with complex nasty arguments
+  As a tests writer
+  I need to apply some transformations to the steps arguments
+
+  Background:
+    Given I am on homepage
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    And I follow "Admin User"
+    And I follow "Edit profile"
+
+  Scenario: Use nasty strings on steps arguments
+    When I fill in "Surname" with "$NASTYSTRING1"
+    And I fill in "Description" with "$NASTYSTRING2"
+    And I fill in "City/town" with "$NASTYSTRING3"
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "Surname" field should match "$NASTYSTRING1" value
+    And the "City/town" field should match "$NASTYSTRING3" value
+
+  Scenario: Use nasty strings on table nodes
+    When I fill the moodle form with:
+      | Surname | $NASTYSTRING1 |
+      | Description | $NASTYSTRING2 |
+      | City/town | $NASTYSTRING3 |
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "Surname" field should match "$NASTYSTRING1" value
+    And the "City/town" field should match "$NASTYSTRING3" value
+
+  Scenario: Use double quotes
+    When I fill the moodle form with:
+      | First name | va"lue1 |
+      | Description | va\"lue2 |
+    And I fill in "City/town" with "va\"lue3"
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "First name" field should match "va\"lue1" value
+    And the "Description" field should match "va\"lue2" value
+    And the "City/town" field should match "va\"lue3" value
+
+  @javascript
+  Scenario: Nasty strings with other contents
+    When I fill in "First name" with "My Firstname $NASTYSTRING1"
+    And I fill the moodle form with:
+      | Surname | My Surname $NASTYSTRING2 |
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And I should see "My Firstname"
+    And I should see "My Surname"
+    And the "First name" field should match "My Firstname $NASTYSTRING1" value
+    And the "Surname" field should match "My Surname $NASTYSTRING2" value
index d4a491f..6373a01 100644 (file)
@@ -1,46 +1,46 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_html\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for html\r
- */\r
-class moodle1_block_html_handler extends moodle1_block_handler {\r
-    private $fileman = null;\r
-    protected function convert_configdata(array $olddata) {\r
-        $instanceid = $olddata['id'];\r
-        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);\r
-        $configdata = unserialize(base64_decode($olddata['configdata']));\r
-\r
-        // get a fresh new file manager for this instance\r
-        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');\r
-\r
-        // convert course files embedded in the block content\r
-        $this->fileman->filearea = 'content';\r
-        $this->fileman->itemid   = 0;\r
-        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);\r
-        $configdata->format = FORMAT_HTML;\r
-\r
-        return base64_encode(serialize($configdata));\r
-    }\r
-\r
-    protected function write_inforef_xml($newdata, $data) {\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");\r
-        $this->xmlwriter->begin_tag('inforef');\r
-        $this->xmlwriter->begin_tag('fileref');\r
-        foreach ($this->fileman->get_fileids() as $fileid) {\r
-            $this->write_xml('file', array('id' => $fileid));\r
-        }\r
-        $this->xmlwriter->end_tag('fileref');\r
-        $this->xmlwriter->end_tag('inforef');\r
-        $this->close_xml_writer();\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_html
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for html
+ */
+class moodle1_block_html_handler extends moodle1_block_handler {
+    private $fileman = null;
+    protected function convert_configdata(array $olddata) {
+        $instanceid = $olddata['id'];
+        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
+        $configdata = unserialize(base64_decode($olddata['configdata']));
+
+        // get a fresh new file manager for this instance
+        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
+
+        // convert course files embedded in the block content
+        $this->fileman->filearea = 'content';
+        $this->fileman->itemid   = 0;
+        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
+        $configdata->format = FORMAT_HTML;
+
+        return base64_encode(serialize($configdata));
+    }
+
+    protected function write_inforef_xml($newdata, $data) {
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
+        $this->xmlwriter->begin_tag('inforef');
+        $this->xmlwriter->begin_tag('fileref');
+        foreach ($this->fileman->get_fileids() as $fileid) {
+            $this->write_xml('file', array('id' => $fileid));
+        }
+        $this->xmlwriter->end_tag('fileref');
+        $this->xmlwriter->end_tag('inforef');
+        $this->close_xml_writer();
+    }
+}
index 846ab98..b62eefb 100644 (file)
@@ -113,7 +113,8 @@ class block_recent_activity extends block_base {
      * Returns list of recent changes in course structure
      *
      * It includes adding, editing or deleting of the resources or activities
-     * Excludes changes on labels, and also if activity was both added and deleted
+     * Excludes changes on modules without a view link (i.e. labels), and also
+     * if activity was both added and deleted
      *
      * @return array array of changes. Each element is an array containing attributes:
      *    'action' - one of: 'add mod', 'update mod', 'delete mod'
@@ -135,13 +136,6 @@ class block_recent_activity extends block_base {
             foreach ($logs as $key => $log) {
                 $info = explode(' ', $log->info);
 
-                // note: in most cases I replaced hardcoding of label with use of
-                // $cm->has_view() but it was not possible to do this here because
-                // we don't necessarily have the $cm for it
-                if ($info[0] == 'label') {     // Labels are ignored in recent activity
-                    continue;
-                }
-
                 if (count($info) != 2) {
                     debugging("Incorrect log entry info: id = ".$log->id, DEBUG_DEVELOPER);
                     continue;
@@ -151,6 +145,11 @@ class block_recent_activity extends block_base {
                 $instanceid = $info[1];
 
                 if ($log->action == 'delete mod') {
+                    if (plugin_supports('mod', $modname, FEATURE_NO_VIEW_LINK, false)) {
+                        // we should better call cm_info::has_view() because it can be
+                        // dynamic. But there is no instance of cm_info now
+                        continue;
+                    }
                     // unfortunately we do not know if the mod was visible
                     if (!array_key_exists($log->info, $newgones)) {
                         $changelist[$log->info] = array('action' => $log->action,
@@ -168,7 +167,7 @@ class block_recent_activity extends block_base {
                         continue;
                     }
                     $cm = $modinfo->instances[$modname][$instanceid];
-                    if ($cm->uservisible && empty($changelist[$log->info])) {
+                    if ($cm->has_view() && $cm->uservisible && empty($changelist[$log->info])) {
                         $changelist[$log->info] = array('action' => $log->action, 'module' => $cm);
                     }
                 }
index 95da634..d698365 100644 (file)
@@ -1,34 +1,34 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_rss_client\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for rss_client\r
- */\r
-class moodle1_block_rss_client_handler extends moodle1_block_handler {\r
-    public function process_block(array $data) {\r
-        parent::process_block($data);\r
-        $instanceid = $data['id'];\r
-        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);\r
-\r
-        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml\r
-        // for the restore process to find\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");\r
-        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));\r
-        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));\r
-        $this->xmlwriter->full_tag('feeds', '');\r
-        $this->xmlwriter->end_tag('rss_client');\r
-        $this->xmlwriter->end_tag('block');\r
-        $this->close_xml_writer();\r
-\r
-        return $data;\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_rss_client
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for rss_client
+ */
+class moodle1_block_rss_client_handler extends moodle1_block_handler {
+    public function process_block(array $data) {
+        parent::process_block($data);
+        $instanceid = $data['id'];
+        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
+
+        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml
+        // for the restore process to find
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");
+        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));
+        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));
+        $this->xmlwriter->full_tag('feeds', '');
+        $this->xmlwriter->end_tag('rss_client');
+        $this->xmlwriter->end_tag('block');
+        $this->close_xml_writer();
+
+        return $data;
+    }
+}
index 3563843..2ac37aa 100644 (file)
@@ -25,6 +25,6 @@
  */
 
 $string['enabledock'] = 'Allow the user to dock this block';
-$string['pluginname'] = 'Settings';
-$string['settings:addinstance'] = 'Add a new settings block';
-$string['settings:myaddinstance'] = 'Add a new settings block to the My Moodle page';
+$string['pluginname'] = 'Administration';
+$string['settings:addinstance'] = 'Add a new administration block';
+$string['settings:myaddinstance'] = 'Add a new administration block to the My Moodle page';
index 919fed1..42ab6e3 100644 (file)
@@ -87,8 +87,8 @@ class blog_edit_form extends moodleform {
                 }
 
                 if (has_capability('moodle/blog:associatecourse', $context)) {
-                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));\r
-                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));\r
+                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));
+                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));
                     $mform->setDefault('courseassoc', $contextid);
                 }
 
index fee8aea..232aa46 100644 (file)
@@ -71,6 +71,12 @@ class cache_config {
      */
     protected $configlocks = array();
 
+    /**
+     * The site identifier used when the cache config was last saved.
+     * @var string
+     */
+    protected $siteidentifier = null;
+
     /**
      * Please use cache_config::instance to get an instance of the cache config that is ready to be used.
      */
@@ -139,6 +145,12 @@ class cache_config {
         $this->configdefinitionmappings = array();
         $this->configlockmappings = array();
 
+        $siteidentifier = 'unknown';
+        if (array_key_exists('siteidentifier', $configuration)) {
+            $siteidentifier = $configuration['siteidentifier'];
+        }
+        $this->siteidentifier = $siteidentifier;
+
         // Filter the lock instances.
         $defaultlock = null;
         foreach ($configuration['locks'] as $conf) {
@@ -271,6 +283,14 @@ class cache_config {
         return true;
     }
 
+    /**
+     * Returns the site identifier used by the cache API.
+     * @return string
+     */
+    public function get_site_identifier() {
+        return $this->siteidentifier;
+    }
+
     /**
      * Includes the configuration file and makes sure it contains the expected bits.
      *
index e278bbb..8c5a16a 100644 (file)
@@ -723,7 +723,8 @@ class cache_definition {
      */
     public function generate_single_key_prefix() {
         if ($this->keyprefixsingle === null) {
-            $this->keyprefixsingle = $this->mode.'/'.$this->mode;
+            $this->keyprefixsingle = $this->mode.'/'.$this->component.'/'.$this->area;
+            $this->keyprefixsingle .= '/'.$this->get_cache_identifier();
             $identifiers = $this->get_identifiers();
             if ($identifiers) {
                 foreach ($identifiers as $key => $value) {
@@ -746,6 +747,7 @@ class cache_definition {
                 'mode' => $this->mode,
                 'component' => $this->component,
                 'area' => $this->area,
+                'siteidentifier' => $this->get_cache_identifier()
             );
             if (!empty($this->identifiers)) {
                 $identifiers = array();
@@ -785,4 +787,13 @@ class cache_definition {
     public function get_invalidation_events() {
         return $this->invalidationevents;
     }
+
+    /**
+     * Returns a cache identification string.
+     *
+     * @return string A string to be used as part of keys.
+     */
+    protected function get_cache_identifier() {
+        return cache_helper::get_site_identifier();
+    }
 }
\ No newline at end of file
index f3d520c..24c7c1e 100644 (file)
@@ -203,6 +203,7 @@ class cache_factory {
         }
         // Get the class. Note this is a late static binding so we need to use get_called_class.
         $definition = cache_definition::load_adhoc($mode, $component, $area, $options);
+        $config = $this->create_config_instance();
         $definition->set_identifiers($identifiers);
         $cache = $this->create_cache($definition, $identifiers);
         if ($definition->should_be_persistent()) {
index 4439a56..a8f3a26 100644 (file)
@@ -54,6 +54,13 @@ class cache_helper {
      */
     protected static $instance;
 
+    /**
+     * The site identifier used by the cache.
+     * Set the first time get_site_identifier is called.
+     * @var string
+     */
+    protected static $siteidentifier = null;
+
     /**
      * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
      *
@@ -489,11 +496,52 @@ class cache_helper {
      */
     public static function update_definitions($coreonly = false) {
         global $CFG;
-        // Include locallib
+        // Include locallib.
         require_once($CFG->dirroot.'/cache/locallib.php');
         // First update definitions
         cache_config_writer::update_definitions($coreonly);
         // Second reset anything we have already initialised to ensure we're all up to date.
         cache_factory::reset();
     }
-}
\ No newline at end of file
+
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public static function update_site_identifier($siteidentifier) {
+        global $CFG;
+        // Include locallib.
+        require_once($CFG->dirroot.'/cache/locallib.php');
+        $factory = cache_factory::instance();
+        $factory->updating_started();
+        $config = $factory->create_config_instance(true);
+        $config->update_site_identifier($siteidentifier);
+        $factory->updating_finished();
+        cache_factory::reset();
+    }
+
+    /**
+     * Returns the site identifier.
+     *
+     * @return string
+     */
+    public static function get_site_identifier() {
+        if (is_null(self::$siteidentifier)) {
+            $factory = cache_factory::instance();
+            $config = $factory->create_config_instance();
+            self::$siteidentifier = $config->get_site_identifier();
+        }
+        return self::$siteidentifier;
+    }
+
+    /**
+     * Returns the site version.
+     *
+     * @return string
+     */
+    public static function get_site_version() {
+        global $CFG;
+        return (string)$CFG->version;
+    }
+}
index ce926a5..4d6d745 100644 (file)
@@ -119,6 +119,7 @@ class cache_config_writer extends cache_config {
      */
     protected function generate_configuration_array() {
         $configuration = array();
+        $configuration['siteidentifier'] = $this->siteidentifier;
         $configuration['stores'] = $this->configstores;
         $configuration['modemappings'] = $this->configmodemappings;
         $configuration['definitions'] = $this->configdefinitions;
@@ -524,6 +525,15 @@ class cache_config_writer extends cache_config {
         $this->config_save();
     }
 
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public function update_site_identifier($siteidentifier) {
+        $this->siteidentifier = md5((string)$siteidentifier);
+        $this->config_save();
+    }
 }
 
 /**
@@ -1002,4 +1012,4 @@ abstract class cache_administration_helper extends cache_helper {
         }
         return $locks;
     }
-}
\ No newline at end of file
+}
index 98d9c5b..545986a 100644 (file)
@@ -553,6 +553,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
@@ -567,13 +569,16 @@ class cache_phpunit_tests extends advanced_testcase {
 
         // OK data added, data invalidated, and invalidation time has been set.
         // Now we need to manually add back the data and adjust the invalidation time.
-        $timefile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/a65/a65b1dc524cf6e03c1795197c84d5231eb229b86.cache';
+        $hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
+        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+        // Make sure the file is correct.
+        $this->assertTrue(file_exists($timefile));
         $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
         make_writable_directory(dirname($timefile));
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
 
-        $datafile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/626/626e9c7a45febd98f064c2b383de8d9d4ebbde7b.cache';
+        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
         $datacont = serialize("test data 1");
         make_writable_directory(dirname($datafile));
         file_put_contents($datafile, $datacont);
@@ -586,6 +591,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
         ));
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         $this->assertEquals('test data 1', $cache->get('testkey1'));
@@ -597,6 +604,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
index d5e5297..471521b 100644 (file)
@@ -102,6 +102,16 @@ class cache_config_phpunittest extends cache_config_writer {
             'sort' => (int)$sort
         );
     }
+
+    /**
+     * Overrides the default site identifier used by the Cache API so that we can be sure of what it is.
+     *
+     * @return string
+     */
+    public function get_site_identifier() {
+        global $CFG;
+        return $CFG->wwwroot.'phpunit';
+    }
 }
 
 /**
index 91b99ca..c1fe37c 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define('AJAX_SCRIPT', true);
+define('NO_DEBUG_DISPLAY', true);
 
 require_once('../config.php');
 require_once($CFG->dirroot . '/comment/lib.php');
@@ -35,6 +36,10 @@ if (empty($CFG->usecomments)) {
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
+if ( $contextid == SYSCONTEXTID ) {
+    $course = $SITE;
+}
+
 $PAGE->set_url('/comment/comment_ajax.php');
 
 // Allow anonymous user to view comments providing forcelogin now enabled
index c146119..9ca0a41 100644 (file)
@@ -526,10 +526,10 @@ class comment {
             $c->content     = $u->ccontent;
             $c->format      = $u->cformat;
             $c->timecreated = $u->ctimecreated;
+            $c->strftimeformat = get_string('strftimerecent', 'langconfig');
             $url = new moodle_url('/user/view.php', array('id'=>$u->id, 'course'=>$this->courseid));
             $c->profileurl = $url->out(false);
             $c->fullname = fullname($u);
-            $c->time = userdate($c->timecreated, get_string('strftimerecent', 'langconfig'));
             $c->content = format_text($c->content, $c->format, $formatoptions);
             $c->avatar = $OUTPUT->user_picture($u, array('size'=>18));
 
@@ -628,12 +628,27 @@ class comment {
         $cmt_id = $DB->insert_record('comments', $newcmt);
         if (!empty($cmt_id)) {
             $newcmt->id = $cmt_id;
-            $newcmt->time = userdate($now, get_string('strftimerecent', 'langconfig'));
+            $newcmt->strftimeformat = get_string('strftimerecent', 'langconfig');
             $newcmt->fullname = fullname($USER);
             $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
             $newcmt->profileurl = $url->out();
             $newcmt->content = format_text($newcmt->content, $format, array('overflowdiv'=>true));
             $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
+
+            $commentlist = array($newcmt);
+
+            if (!empty($this->plugintype)) {
+                // Call the display callback to allow the plugin to format the newly added comment.
+                $commentlist = plugin_callback($this->plugintype,
+                                               $this->pluginname,
+                                               'comment',
+                                               'display',
+                                               array($commentlist, $this->comment_param),
+                                               $commentlist);
+                $newcmt = $commentlist[0];
+            }
+            $newcmt->time = userdate($newcmt->timecreated, $newcmt->strftimeformat);
+
             return $newcmt;
         } else {
             throw new comment_exception('dbupdatefailed');
@@ -793,7 +808,7 @@ class comment {
         $replacements[] = $cmt->avatar;
         $replacements[] = html_writer::link($cmt->profileurl, $cmt->fullname);
         $replacements[] = $cmt->content;
-        $replacements[] = userdate($cmt->timecreated, get_string('strftimerecent', 'langconfig'));
+        $replacements[] = userdate($cmt->timecreated, $cmt->strftimeformat);
 
         // use html template to format a single comment.
         return str_replace($patterns, $replacements, $this->template);
index 5b901b2..257fec8 100644 (file)
@@ -2531,7 +2531,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     $actions = array();
 
     // AJAX edit title
-    if ($mod->modname !== 'label' && $hasmanageactivities &&
+    if ($mod->has_view() && $hasmanageactivities &&
                 (($mod->course == $COURSE->id && course_ajax_enabled($COURSE)) ||
                  ($mod->course == SITEID && course_ajax_enabled($SITE)))) {
         // we will not display link if we are on some other-course page (where we should not see this module anyway)
index e20cdc5..0f1e0ca 100644 (file)
@@ -83,9 +83,6 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section($activity, $section) {
 
-        $activity = $this->fixStepArgument($activity);
-        $section = $this->fixStepArgument($section);
-
         // Clicks add activity or resource section link.
         $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/span/a";
         $sectionnode = $this->find('xpath', $sectionxpath);
index a59351b..ef8ef1b 100644 (file)
@@ -297,22 +297,71 @@ class courselib_testcase extends advanced_testcase {
     }
 
     public function test_move_module_in_course() {
+        global $DB;
+
         $this->resetAfterTest(true);
         // Setup fixture
-        $course = $this->getDataGenerator()->create_course(array('numsections'=>5));
+        $course = $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections' => true));
         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
 
         $cms = get_fast_modinfo($course)->get_cms();
         $cm = reset($cms);
 
-        course_create_sections_if_missing($course, 3);
-        $section3 = get_fast_modinfo($course)->get_section_info(3);
+        $newsection = get_fast_modinfo($course)->get_section_info(3);
+        $oldsectionid = $cm->section;
+
+        // Perform the move
+        moveto_module($cm, $newsection);
 
-        moveto_module($cm, $section3);
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
 
+        // Check that the cached modinfo contains the correct section info
         $modinfo = get_fast_modinfo($course);
         $this->assertTrue(empty($modinfo->sections[0]));
         $this->assertFalse(empty($modinfo->sections[3]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
+
+        // Check that the section number has been changed in the cm
+        $this->assertEquals($newsection->id, $cm->section);
+
+
+        // Perform a second move as some issues were only seen on the second move
+        $newsection = get_fast_modinfo($course)->get_section_info(2);
+        $oldsectionid = $cm->section;
+        $result = moveto_module($cm, $newsection);
+        $this->assertTrue($result);
+
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
+
+        // Check that the cached modinfo contains the correct section info
+        $modinfo = get_fast_modinfo($course);
+        $this->assertTrue(empty($modinfo->sections[0]));
+        $this->assertFalse(empty($modinfo->sections[2]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
     }
 
     public function test_module_visibility() {
index 4e6ad15..8560eef 100644 (file)
@@ -15,7 +15,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
         GROUPSNONE : 'a.editing_groupsnone',
         GROUPSSEPARATE : 'a.editing_groupsseparate',
         GROUPSVISIBLE : 'a.editing_groupsvisible',
-        HASLABEL : 'label',
         HIDE : 'a.editing_hide',
         HIGHLIGHT : 'a.editing_highlight',
         INSTANCENAME : 'span.instancename',
@@ -62,7 +61,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             var dimarea;
             var toggle_class;
-            if (this.is_label(element)) {
+            if (this.get_instance_name(element) == null) {
                 toggle_class = CSS.DIMMEDTEXT;
                 dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1);
             } else {
@@ -172,8 +171,19 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             Y.io(uri, config);
             return responsetext;
         },
-        is_label : function(target) {
-            return target.hasClass(CSS.HASLABEL);
+        /**
+         * Return the name of the activity instance
+         *
+         * If activity has no name (for example label) null is returned
+         *
+         * @param element The <li> element to determine a name for
+         * @return string|null Instance name
+         */
+        get_instance_name : function(target) {
+            if (target.one(CSS.INSTANCENAME)) {
+                return target.one(CSS.INSTANCENAME).get('firstChild').get('data');
+            }
+            return null;
         },
         /**
          * Return the module ID for the specified element
@@ -330,19 +340,16 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             // Get the element we're working on
             var element   = e.target.ancestor(CSS.ACTIVITYLI);
 
+            // Create confirm string (different if element has or does not have name)
             var confirmstring = '';
-            if (this.is_label(element)) {
-                // Labels are slightly different to other activities
-                var plugindata = {
-                    type : M.util.get_string('pluginname', 'label')
-                }
-                confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
-            } else {
-                var plugindata = {
-                    type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]),
-                    name : element.one(CSS.INSTANCENAME).get('firstChild').get('data')
-                }
+            var plugindata = {
+                type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1])
+            }
+            if (this.get_instance_name(element) != null) {
+                plugindata.name = this.get_instance_name(element)
                 confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
+            } else {
+                confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
             }
 
             // Confirm element removal
index 13c6114..921bb30 100644 (file)
@@ -114,9 +114,9 @@ class imsenterprise_courses {
      * @return array Array of assignable values
      */
     function get_imsnames($courseattr) {
-\r
-        $values = $this->imsnames;\r
-        if ($courseattr == 'summary') {\r
+
+        $values = $this->imsnames;
+        if ($courseattr == 'summary') {
             $values = array_merge(array('ignore' => get_string('emptyattribute', 'enrol_imsenterprise')), $values);
         }
         return $values;
index 5e947b6..3f6a506 100644 (file)
@@ -318,11 +318,12 @@ class course_enrolment_manager {
      * @param array $params query parameters.
      * @param int $page which page number of the results to show.
      * @param int $perpage number of users per page.
+     * @param int $addedenrollment number of users added to enrollment.
      * @return array with two elememts:
      *      int total number of users matching the search.
      *      array of user objects returned by the query.
      */
-    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage) {
+    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage, $addedenrollment=0) {
         global $DB, $CFG;
 
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context());
@@ -330,7 +331,7 @@ class course_enrolment_manager {
 
         $totalusers = $DB->count_records_sql($countfields . $sql, $params);
         $availableusers = $DB->get_records_sql($fields . $sql . $order,
-                array_merge($params, $sortparams), $page*$perpage, $perpage);
+                array_merge($params, $sortparams), ($page*$perpage) - $addedenrollment, $perpage);
 
         return array('totalusers' => $totalusers, 'users' => $availableusers);
     }
@@ -344,9 +345,10 @@ class course_enrolment_manager {
      * @param bool $searchanywhere
      * @param int $page Defaults to 0
      * @param int $perpage Defaults to 25
+     * @param int $addedenrollment Defaults to 0
      * @return array Array(totalusers => int, users => array)
      */
-    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25) {
+    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25, $addedenrollment=0) {
         global $DB;
 
         list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere);
@@ -359,7 +361,7 @@ class course_enrolment_manager {
                       AND ue.id IS NULL";
         $params['enrolid'] = $enrolid;
 
-        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage);
+        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, $addedenrollment);
     }
 
     /**
index 093ee05..ea4acd8 100644 (file)
@@ -67,7 +67,9 @@ switch ($action) {
         $enrolid = required_param('enrolid', PARAM_INT);
         $search = optional_param('search', '', PARAM_RAW);
         $page = optional_param('page', 0, PARAM_INT);
-        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page);
+        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
+        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
+        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page, $perpage, $addedenrollment);
         $extrafields = get_extra_user_fields($context);
         foreach ($outcome->response['users'] as &$user) {
             $user->picture = $OUTPUT->user_picture($user);
index aad6e62..8362185 100644 (file)
@@ -23,7 +23,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         DEFAULTDURATION : 'defaultDuration',
         ASSIGNABLEROLES : 'assignableRoles',
         DISABLEGRADEHISTORY : 'disableGradeHistory',
-        RECOVERGRADESDEFAULT : 'recoverGradesDefault'
+        RECOVERGRADESDEFAULT : 'recoverGradesDefault',
+        ENROLCOUNT : 'enrolCount',
+        PERPAGE : 'perPage'
     };
     /** CSS classes for nodes in structure **/
     var CSS = {
@@ -309,6 +311,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             params['action'] = 'searchusers';
             params['search'] = this.get(UEP.SEARCH).get('value');
             params['page'] = this.get(UEP.PAGE);
+            params['enrolcount'] = this.get(UEP.ENROLCOUNT);
+            params['perpage'] = this.get(UEP.PERPAGE);
+
             if (this.get(UEP.MULTIPLE)) {
                 alert('oh no there are multiple');
             } else {
@@ -376,7 +381,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 var content = create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
                     .append(create('<div class="'+CSS.TOTALUSERS+'">'+usersstr+'</div>'))
                     .append(users);
-                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     var fetchmore = create('<div class="'+CSS.MORERESULTS+'"><a href="#">'+M.str.enrol.ajaxnext25+'</a></div>');
                     fetchmore.on('click', this.search, this, true);
                     content.append(fetchmore)
@@ -384,7 +389,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 this.setContent(content);
                 Y.delegate("click", this.enrolUser, users, '.'+CSS.USER+' .'+CSS.ENROL, this, args);
             } else {
-                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     this.get(UEP.BASE).one('.'+CSS.MORERESULTS).remove();
                 }
             }
@@ -420,6 +425,8 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                                 args.userNode.addClass(CSS.ENROLLED);
                                 args.userNode.one('.'+CSS.ENROL).remove();
                                 this.set(UEP.REQUIREREFRESH, true);
+                                var countenrol = this.get(UEP.ENROLCOUNT)+1;
+                                this.set(UEP.ENROLCOUNT, countenrol);
                             }
                         } catch (e) {
                             new M.core.exception(e);
@@ -532,6 +539,14 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             },
             recoverGradesDefault : {
                 value : ''
+            },
+            enrolCount : {
+                value : 0,
+                validator : Y.Lang.isNumber
+            },
+            perPage : {
+                value: 25,
+                Validator: Y.Lang.isNumber
             }
         }
     });
index bef80bc..dc2b636 100644 (file)
@@ -36,8 +36,6 @@ class filter_activitynames extends moodle_text_filter {
     static $cachedcourseid;
 
     function filter($text, array $options = array()) {
-        global $CFG, $COURSE, $DB;
-
         if (!$courseid = get_courseid_from_context($this->context)) {
             return $text;
         }
@@ -53,35 +51,25 @@ class filter_activitynames extends moodle_text_filter {
         if (is_null(self::$activitylist)) {
             self::$activitylist = array();
 
-            if ($COURSE->id == $courseid) {
-                $course = $COURSE;
-            } else {
-                $course = $DB->get_record("course", array("id"=>$courseid));
-            }
-
-            if (!isset($course->modinfo)) {
-                return $text;
-            }
-
-        /// Casting $course->modinfo to string prevents one notice when the field is null
-            $modinfo = unserialize((string)$course->modinfo);
-
-            if (!empty($modinfo)) {
-
+            $modinfo = get_fast_modinfo($courseid);
+            if (!empty($modinfo->cms)) {
                 self::$activitylist = array();      /// We will store all the activities here
 
                 //Sort modinfo by name length
-                usort($modinfo, 'filter_activitynames_comparemodulenamesbylength');
+                $sortedactivities = fullclone($modinfo->cms);
+                usort($sortedactivities, 'filter_activitynames_comparemodulenamesbylength');
 
-                foreach ($modinfo as $activity) {
+                foreach ($sortedactivities as $cm) {
                     //Exclude labels, hidden activities and activities for group members only
-                    if ($activity->mod != "label" and $activity->visible and empty($activity->groupmembersonly)) {
-                        $title = s(trim(strip_tags($activity->name)));
-                        $currentname = trim($activity->name);
+                    if ($cm->visible and empty($cm->groupmembersonly) and $cm->has_view()) {
+                        $title = s(trim(strip_tags($cm->name)));
+                        $currentname = trim($cm->name);
                         $entitisedname  = s($currentname);
                         /// Avoid empty or unlinkable activity names
                         if (!empty($title)) {
-                            $href_tag_begin = "<a class=\"autolink\" title=\"$title\" href=\"$CFG->wwwroot/mod/$activity->mod/view.php?id=$activity->cm\">";
+                            $href_tag_begin = html_writer::start_tag('a',
+                                    array('class' => 'autolink', 'title' => $title,
+                                        'href' => $cm->get_url()));
                             self::$activitylist[] = new filterobject($currentname, $href_tag_begin, '</a>', false, true);
                             if ($currentname != $entitisedname) { /// If name has some entity (&amp; &quot; &lt; &gt;) add that filter too. MDL-17545
                                 self::$activitylist[] = new filterobject($entitisedname, $href_tag_begin, '</a>', false, true);
index 0ee92ba..54af548 100644 (file)
@@ -323,9 +323,9 @@ M.gradereport_grader.classes.ajax = function(report, cfg) {
                 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
             }
         }
-        // Hide the Update button
+        // Disable the Update button as we're saving using ajax.
         submitbutton = this.report.Y.one('#gradersubmit');
-        submitbutton.setStyle('visibility', 'hidden');
+        submitbutton.set('disabled', true);
     }
 };
 /**
index 4996af6..53f68e2 100644 (file)
@@ -1035,7 +1035,7 @@ $string['updateavailable_version'] = 'Version {$a}';
 $string['updateavailableinstall'] = 'Install this update';
 $string['updateavailablenot'] = 'Your Moodle code is up-to-date!';
 $string['updatenotifications'] = 'Update notifications';
-$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Settings block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
+$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Administration block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
 $string['updatenotificationsubject'] = 'Moodle updates are available ({$a->siteurl})';
 $string['updateautocheck'] = 'Automatically check for available updates';
 $string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all additional plugins. If there is a new update available, a notification will be sent to site admins.';
index fed4bbc..3be622d 100644 (file)
@@ -31,7 +31,7 @@ $string['activemethodinfonone'] = 'There is no advanced grading method selected
 $string['changeactivemethod'] = 'Change active grading method to';
 $string['clicktoclose'] = 'click to close';
 $string['exc_gradingformelement'] = 'Unable to instantiate grading form element';
-$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Settings block.';
+$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Administration block.';
 $string['gradingformunavailable'] = 'Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status.';
 $string['gradingmanagement'] = 'Advanced grading';
 $string['gradingmanagementtitle'] = 'Advanced grading: {$a->component} ({$a->area})';
index 3ab441e..79cc8d2 100644 (file)
@@ -54,4 +54,4 @@ $string['ratinginvalid'] = 'Rating is invalid';
 $string['ratingtime'] = 'Restrict ratings to items with dates in this range:';
 $string['ratings'] = 'Ratings';
 $string['rolewarning'] = 'Roles with permission to rate';
-$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the settings block.';
+$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the administration block.';
index 41505e0..d703a77 100644 (file)
@@ -51,21 +51,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     const TIMEOUT = 6;
 
-    /**
-     * Returns fixed step argument (with \\" replaced back to ").
-     *
-     * \\ is the chars combination to add when you
-     * want to escape the " character that is used as var
-     * delimiter.
-     *
-     * @see Behat\MinkExtension\Context\MinkContext
-     * @param string $argument
-     * @return string
-     */
-    protected function fixStepArgument($argument) {
-        return str_replace('\\"', '"', $argument);
-    }
-
     /**
      * Locates url, based on provided path.
      * Override to provide custom routing mechanism.
index c2bf778..04e0b3c 100644 (file)
@@ -49,10 +49,21 @@ class behat_form_editor extends behat_form_field {
      */
     public function set_value($value) {
 
-        // Set the value to the iframe and save it to the textarea.
-        $editorid = $this->field->getAttribute('id');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").setContent("' . $value . '");');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If tinyMCE var exists means that we are using that editor.
+        if ($this->is_editor_available()) {
+
+            // Set the value to the iframe and save it to the textarea.
+            $editorid = $this->field->getAttribute('id');
+
+            $this->session->executeScript('
+                tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
+                tinyMCE.get("'.$editorid.'").save();
+            ');
+
+        } else {
+            // Set the value to a textarea otherwise.
+            parent::set_value($value);
+        }
     }
 
     /**
@@ -62,12 +73,36 @@ class behat_form_editor extends behat_form_field {
      */
     public function get_value() {
 
-        // Save the current iframe value in case default value has been edited.
-        $editorid = $this->field->getAttribute('id');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If tinyMCE var exists means that we are using that editor.
+        if ($this->is_editor_available()) {
+
+            // Save the current iframe value in case default value has been edited.
+            $editorid = $this->field->getAttribute('id');
+            $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        }
 
         return $this->field->getValue();
     }
 
+    /**
+     * Returns if the HTML editor is available.
+     *
+     * The editor availability depends on the driver running the tests; Goutte
+     * can not execute Javascript, also some Moodle settings disables the HTML
+     * editor.
+     *
+     * @return bool
+     */
+    protected function is_editor_available() {
+
+        // Non-JS drivers throws exceptions when running JS.
+        try {
+            $available = $this->session->evaluateScript('return (typeof tinyMCE != "undefined")');
+        } catch (Exception $e) {
+            $available = false;
+        }
+
+        return $available;
+    }
 }
 
index a2a8d93..ddf6a4f 100644 (file)
@@ -273,6 +273,15 @@ $functions = array(
         'capabilities'=> 'moodle/user:create',
     ),
 
+    'core_user_get_users' => array(
+        'classname'   => 'core_user_external',
+        'methodname'  => 'get_users',
+        'classpath'   => 'user/externallib.php',
+        'description' => 'search for users matching the parameters',
+        'type'        => 'read',
+        'capabilities'=> 'moodle/user:viewdetails, moodle/user:viewhiddendetails, moodle/course:useremail, moodle/user:update',
+    ),
+
     'moodle_user_get_users_by_id' => array(
         'classname'   => 'core_user_external',
         'methodname'  => 'get_users_by_id',
index b563121..e8a7278 100644 (file)
@@ -1577,5 +1577,79 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013021100.01);
     }
 
+    if ($oldversion < 2013021800.00) {
+        // Add the site identifier to the cache config's file.
+        $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
+        cache_helper::update_site_identifier($siteidentifier);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021800.00);
+    }
+
+    if ($oldversion < 2013021801.00) {
+        // Fixing possible wrong MIME types for SMART Notebook files.
+        $extensions = array('%.gallery', '%.galleryitem', '%.gallerycollection', '%.nbk', '%.notebook', '%.xbk');
+        $select = $DB->sql_like('filename', '?', false);
+        foreach ($extensions as $extension) {
+            $DB->set_field_select(
+                'files',
+                'mimetype',
+                'application/x-smarttech-notebook',
+                $select,
+                array($extension)
+            );
+        }
+        upgrade_main_savepoint(true, 2013021801.00);
+    }
+
+    if ($oldversion < 2013021801.01) {
+        // Retrieve the list of course_sections as a recordset to save memory
+        $coursesections = $DB->get_recordset('course_sections', null, 'course, id', 'id, course, sequence');
+        foreach ($coursesections as $coursesection) {
+            // Retrieve all of the actual modules in this course and section combination to reduce DB calls
+            $actualsectionmodules = $DB->get_records('course_modules',
+                    array('course' => $coursesection->course, 'section' => $coursesection->id), '', 'id, section');
+
+            // Break out the current sequence so that we can compare it
+            $currentsequence = explode(',', $coursesection->sequence);
+            $newsequence = array();
+
+            // Check each of the modules in the current sequence
+            foreach ($currentsequence as $module) {
+                if (isset($actualsectionmodules[$module])) {
+                    $newsequence[] = $module;
+                    // We unset the actualsectionmodules so that we don't get duplicates and that we can add orphaned
+                    // modules later
+                    unset($actualsectionmodules[$module]);
+                }
+            }
+
+            // Append any modules which have somehow been orphaned
+            foreach ($actualsectionmodules as $module) {
+                $newsequence[] = $module->id;
+            }
+
+            // Piece it all back together
+            $sequence = implode(',', $newsequence);
+
+            // Only update if there have been changes
+            if ($sequence !== $coursesection->sequence) {
+                $coursesection->sequence = $sequence;
+                $DB->update_record('course_sections', $coursesection);
+
+                // And clear the sectioncache and modinfo cache - they'll be regenerated on next use
+                $course = new stdClass();
+                $course->id = $coursesection->course;
+                $course->sectioncache = null;
+                $course->modinfo = null;
+                $DB->update_record('course', $course);
+            }
+        }
+        $coursesections->close();
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021801.01);
+    }
+
     return true;
 }
index d5b84aa..6fb311d 100644 (file)
@@ -1469,6 +1469,10 @@ function &get_mimetypes_array() {
         'fdf'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'flv'  => array ('type'=>'video/x-flv', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'f4v'  => array ('type'=>'video/mp4', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
+
+        'gallery'           => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'galleryitem,'      => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'gallerycollection' => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'gif'  => array ('type'=>'image/gif', 'icon'=>'gif', 'groups'=>array('image', 'web_image'), 'string'=>'image'),
         'gtar' => array ('type'=>'application/x-gtar', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
         'tgz'  => array ('type'=>'application/g-zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
@@ -1512,6 +1516,9 @@ function &get_mimetypes_array() {
         'mpe'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'mpg'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
 
+        'nbk'       => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'notebook'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+
         'odt'  => array ('type'=>'application/vnd.oasis.opendocument.text', 'icon'=>'writer', 'groups'=>array('document')),
         'ott'  => array ('type'=>'application/vnd.oasis.opendocument.text-template', 'icon'=>'writer', 'groups'=>array('document')),
         'oth'  => array ('type'=>'application/vnd.oasis.opendocument.text-web', 'icon'=>'oth', 'groups'=>array('document')),
@@ -1591,6 +1598,8 @@ function &get_mimetypes_array() {
         'webm'  => array ('type'=>'video/webm', 'icon'=>'video', 'groups'=>array('video'), 'string'=>'video'),
         'wmv'  => array ('type'=>'video/x-ms-wmv', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
         'asf'  => array ('type'=>'video/x-ms-asf', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
+
+        'xbk'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'xdp'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfd'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'),
@@ -1605,6 +1614,7 @@ function &get_mimetypes_array() {
 
         'xml'  => array ('type'=>'application/xml', 'icon'=>'markup'),
         'xsl'  => array ('type'=>'text/xml', 'icon'=>'markup'),
+
         'zip'  => array ('type'=>'application/zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive')
     );
     return $mimearray;
index b8760c0..a01de83 100644 (file)
@@ -90,6 +90,25 @@ class filter_manager {
         return self::$singletoninstance;
     }
 
+    /**
+     * Resets the caches, usually to be called between unit tests
+     */
+    public static function reset_caches() {
+        if (self::$singletoninstance) {
+            self::$singletoninstance->unload_all_filters();
+        }
+        self::$singletoninstance = null;
+    }
+
+    /**
+     * Unloads all filters and other cached information
+     */
+    protected function unload_all_filters() {
+        $this->textfilters = array();
+        $this->stringfilters = array();
+        $this->stringfilternames = array();
+    }
+
     /**
      * Load all the filters required by this context.
      *
@@ -286,6 +305,16 @@ class performance_measuring_filter_manager extends filter_manager {
     protected $textsfiltered = 0;
     protected $stringsfiltered = 0;
 
+    /**
+     * Unloads all filters and other cached information
+     */
+    protected function unload_all_filters() {
+        parent::unload_all_filters();
+        $this->filterscreated = 0;
+        $this->textsfiltered = 0;
+        $this->stringsfiltered = 0;
+    }
+
     /**
      * @param string $filtername
      * @param object $context
index c40efc7..f575761 100644 (file)
@@ -785,11 +785,6 @@ class cm_info extends stdClass {
         if (empty($this->content)) {
             return '';
         }
-        if ($this->modname === 'label') {
-            // special case, label returns already formatted content, see cm_info::__construct()
-            // and label_get_coursemodule_info()
-            return $this->content;
-        }
         // Improve filter performance by preloading filter setttings for all
         // activities on the course (this does nothing if called multiple
         // times)
@@ -1094,13 +1089,6 @@ class cm_info extends stdClass {
         $this->showdescription  = isset($mod->showdescription) ? $mod->showdescription : 0;
         $this->state = self::STATE_BASIC;
 
-        // This special case handles old label data. Labels used to use the 'name' field for
-        // content
-        if ($this->modname === 'label' && $this->content === '') {
-            $this->content = $this->extra;
-            $this->extra = '';
-        }
-
         // Note: These fields from $cm were not present in cm_info in Moodle
         // 2.0.2 and prior. They may not be available if course cache hasn't
         // been rebuilt since then.
@@ -1491,8 +1479,8 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
  * Class that is the return value for the _get_coursemodule_info module API function.
  *
  * Note: For backward compatibility, you can also return a stdclass object from that function.
- * The difference is that the stdclass object may contain an 'extra' field (deprecated because
- * it was crazy, except for label which uses it differently). The stdclass object may not contain
+ * The difference is that the stdclass object may contain an 'extra' field (deprecated,
+ * use extraclasses and onclick instead). The stdclass object may not contain
  * the new fields defined here (content, extraclasses, customdata).
  */
 class cached_cm_info {
index eca1097..6890a83 100644 (file)
@@ -1327,6 +1327,9 @@ function set_config($name, $value, $plugin=NULL) {
                 $DB->insert_record('config', $config, false);
             }
         }
+        if ($name === 'siteidentifier') {
+            cache_helper::update_site_identifier($value);
+        }
         cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
     } else { // plugin scope
         if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
@@ -1360,6 +1363,8 @@ function set_config($name, $value, $plugin=NULL) {
  * If called with 2 parameters it will return a string single
  * value or false if the value is not found.
  *
+ * @static $siteidentifier The site identifier is not cached. We use this static cache so
+ *     that we need only fetch it once per request.
  * @param string $plugin full component name
  * @param string $name default NULL
  * @return mixed hash-like object or single value, return false no config found
@@ -1367,6 +1372,8 @@ function set_config($name, $value, $plugin=NULL) {
 function get_config($plugin, $name = NULL) {
     global $CFG, $DB;
 
+    static $siteidentifier = null;
+
     if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
         $forced =& $CFG->config_php_settings;
         $iscore = true;
@@ -1380,8 +1387,28 @@ function get_config($plugin, $name = NULL) {
         $iscore = false;
     }
 
-    if (!empty($name) && array_key_exists($name, $forced)) {
-        return (string)$forced[$name];
+    if ($siteidentifier === null) {
+        try {
+            // This may fail during installation.
+            // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
+            // install the database.
+            $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
+        } catch (dml_exception $ex) {
+            // It's failed. We'll use this opportunity to disable cache stores so that we don't inadvertingly start using
+            // old caches. People should delete their moodledata dirs when reinstalling the database... but they don't.
+            cache_factory::disable_stores();
+            // Set siteidentifier to false. We don't want to trip this continually.
+            $siteidentifier = false;
+            throw $ex;
+        }
+    }
+
+    if (!empty($name)) {
+        if (array_key_exists($name, $forced)) {
+            return (string)$forced[$name];
+        } else if ($name === 'siteidentifier' && $plugin == 'core') {
+            return $siteidentifier;
+        }
     }
 
     $cache = cache::make('core', 'config');
@@ -1405,6 +1432,10 @@ function get_config($plugin, $name = NULL) {
         return false;
     }
 
+    if ($plugin === 'core') {
+        $result['siteidentifier'] = $siteidentifier;
+    }
+
     foreach ($forced as $key => $value) {
         if (is_null($value) or is_array($value) or is_object($value)) {
             // we do not want any extra mess here, just real settings that could be saved in db
@@ -10634,24 +10665,33 @@ function get_performance_info() {
 
     if ($stats = cache_helper::get_stats()) {
         $html = '<span class="cachesused">';
-        $html .= '<span class="cache-stats-heading">Caches interaction by definition then store</span>';
+        $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
         $text = 'Caches used (hits/misses/sets): ';
         $hits = 0;
         $misses = 0;
         $sets = 0;
         foreach ($stats as $definition => $stores) {
-            $html .= '<span class="cache-definition-stats">'.$definition.'</span>';
+            $html .= '<span class="cache-definition-stats">';
+            $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
             $text .= "$definition {";
             foreach ($stores as $store => $data) {
                 $hits += $data['hits'];
                 $misses += $data['misses'];
                 $sets += $data['sets'];
+                if ($data['hits'] == 0 and $data['misses'] > 0) {
+                    $cachestoreclass = 'nohits';
+                } else if ($data['hits'] < $data['misses']) {
+                    $cachestoreclass = 'lowhits';
+                } else {
+                    $cachestoreclass = 'hihits';
+                }
                 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
-                $html .= "<span class='cache-store-stats'>$store: $data[hits] / $data[misses] / $data[sets]</span>";
+                $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
             }
+            $html .= '</span>';
             $text .= '} ';
         }
-        $html .= "<span class='cache-total-stats'>Total Hits / Misses / Sets : $hits / $misses / $sets</span>";
+        $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
         $html .= '</span> ';
         $info['cachesused'] = "$hits / $misses / $sets";
         $info['html'] .= $html;
index 699cc73..f350701 100644 (file)
@@ -2169,38 +2169,6 @@ class global_navigation extends navigation_node {
             $usernode->add(get_string('notes', 'notes'), $url);
         }
 
-        // Add reports node
-        $reporttab = $usernode->add(get_string('activityreports'));
-        $reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
-        foreach ($reports as $reportfunction) {
-            $reportfunction($reporttab, $user, $course);
-        }
-        $anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
-        if ($anyreport || ($course->showreports && $iscurrentuser && $forceforcontext)) {
-            // Add grade hardcoded grade report if necessary
-            $gradeaccess = false;
-            if (has_capability('moodle/grade:viewall', $coursecontext)) {
-                //ok - can view all course grades
-                $gradeaccess = true;
-            } else if ($course->showgrades) {
-                if ($iscurrentuser && has_capability('moodle/grade:view', $coursecontext)) {
-                    //ok - can view own grades
-                    $gradeaccess = true;
-                } else if (has_capability('moodle/grade:viewall', $usercontext)) {
-                    // ok - can view grades of this user - parent most probably
-                    $gradeaccess = true;
-                } else if ($anyreport) {
-                    // ok - can view grades of this user - parent most probably
-                    $gradeaccess = true;
-                }
-            }
-            if ($gradeaccess) {
-                $reporttab->add(get_string('grade'), new moodle_url('/course/user.php', array('mode'=>'grade', 'id'=>$course->id, 'user'=>$usercontext->instanceid)));
-            }
-        }
-        // Check the number of nodes in the report node... if there are none remove the node
-        $reporttab->trim_if_empty();
-
         // If the user is the current user add the repositories for the current user
         $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
         if ($iscurrentuser) {
@@ -2439,26 +2407,6 @@ class global_navigation extends navigation_node {
             $participants = $coursenode->add(get_string('participants'), null, self::TYPE_CONTAINER, get_string('participants'), 'participants');
         }
 
-        // View course reports
-        if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
-            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
-            $coursereports = get_plugin_list('coursereport'); // deprecated
-            foreach ($coursereports as $report=>$dir) {
-                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
-                if (file_exists($libfile)) {
-                    require_once($libfile);
-                    $reportfunction = $report.'_report_extend_navigation';
-                    if (function_exists($report.'_report_extend_navigation')) {
-                        $reportfunction($reportnav, $course, $this->page->context);
-                    }
-                }
-            }
-
-            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
-            foreach ($reports as $reportfunction) {
-                $reportfunction($reportnav, $course, $this->page->context);
-            }
-        }
         return true;
     }
     /**
@@ -2514,26 +2462,6 @@ class global_navigation extends navigation_node {
             $coursenode->add(get_string('calendar', 'calendar'), $calendarurl, self::TYPE_CUSTOM, null, 'calendar');
         }
 
-        // View course reports
-        if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
-            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
-            $coursereports = get_plugin_list('coursereport'); // deprecated
-            foreach ($coursereports as $report=>$dir) {
-                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
-                if (file_exists($libfile)) {
-                    require_once($libfile);
-                    $reportfunction = $report.'_report_extend_navigation';
-                    if (function_exists($report.'_report_extend_navigation')) {
-                        $reportfunction($reportnav, $course, $this->page->context);
-                    }
-                }
-            }
-
-            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
-            foreach ($reports as $reportfunction) {
-                $reportfunction($reportnav, $course, $this->page->context);
-            }
-        }
         return true;
     }
 
@@ -3470,6 +3398,28 @@ class settings_navigation extends navigation_node {
             $coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/filter', ''));
         }
 
+        // View course reports.
+        if (has_capability('moodle/site:viewreports', $coursecontext)) { // Basic capability for listing of reports.
+            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null,
+                    new pix_icon('i/stats', ''));
+            $coursereports = get_plugin_list('coursereport');
+            foreach ($coursereports as $report => $dir) {
+                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
+                if (file_exists($libfile)) {
+                    require_once($libfile);
+                    $reportfunction = $report.'_report_extend_navigation';
+                    if (function_exists($report.'_report_extend_navigation')) {
+                        $reportfunction($reportnav, $course, $coursecontext);
+                    }
+                }
+            }
+
+            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
+            foreach ($reports as $reportfunction) {
+                $reportfunction($reportnav, $course, $coursecontext);
+            }
+        }
+
         // Add view grade report is permitted
         $reportavailable = false;
         if (has_capability('moodle/grade:viewall', $coursecontext)) {
@@ -3976,6 +3926,40 @@ class settings_navigation extends navigation_node {
             }
         }
 
+        // Add reports node.
+        $reporttab = $usersetting->add(get_string('activityreports'));
+        $reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
+        foreach ($reports as $reportfunction) {
+            $reportfunction($reporttab, $user, $course);
+        }
+        $anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
+        if ($anyreport || ($course->showreports && $iscurrentuser && $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)) {
+                    // Can view own grades.
+                    $gradeaccess = true;
+                } else if (has_capability('moodle/grade:viewall', $usercontext)) {
+                    // Can view grades of this user - parent most probably.
+                    $gradeaccess = true;
+                } else if ($anyreport) {
+                    // Can view grades of this user - parent most probably.
+                    $gradeaccess = true;
+                }
+            }
+            if ($gradeaccess) {
+                $reporttab->add(get_string('grade'), new moodle_url('/course/user.php', array('mode'=>'grade', 'id'=>$course->id,
+                        'user'=>$usercontext->instanceid)));
+            }
+        }
+        // Check the number of nodes in the report node... if there are none remove the node
+        $reporttab->trim_if_empty();
+
+
         // Login as ...
         if (!$user->deleted and !$currentuser && !session_is_loggedinas() && has_capability('moodle/user:loginas', $coursecontext) && !is_siteadmin($user->id)) {
             $url = new moodle_url('/course/loginas.php', array('id'=>$course->id, 'user'=>$user->id, 'sesskey'=>sesskey()));
@@ -4141,6 +4125,28 @@ class settings_navigation extends navigation_node {
             $frontpage->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/filter', ''));
         }
 
+        // View course reports.
+        if (has_capability('moodle/site:viewreports', $coursecontext)) { // Basic capability for listing of reports.
+            $frontpagenav = $frontpage->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null,
+                    new pix_icon('i/stats', ''));
+            $coursereports = get_plugin_list('coursereport');
+            foreach ($coursereports as $report=>$dir) {
+                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
+                if (file_exists($libfile)) {
+                    require_once($libfile);
+                    $reportfunction = $report.'_report_extend_navigation';
+                    if (function_exists($report.'_report_extend_navigation')) {
+                        $reportfunction($frontpagenav, $course, $coursecontext);
+                    }
+                }
+            }
+
+            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
+            foreach ($reports as $reportfunction) {
+                $reportfunction($frontpagenav, $course, $coursecontext);
+            }
+        }
+
         // Backup this course
         if (has_capability('moodle/backup:backupcourse', $coursecontext)) {
             $url = new moodle_url('/backup/backup.php', array('id'=>$course->id));
index 23a34d9..ba15273 100644 (file)
@@ -171,6 +171,7 @@ class phpunit_util extends testing_util {
         if (class_exists('repository')) {
             repository::reset_caches();
         }
+        filter_manager::reset_caches();
         //TODO MDL-25290: add more resets here and probably refactor them to new core function
 
         // Reset course and module caches.
index 3582c01..5218444 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,29 @@ 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);
+    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.
+        if ($timestr == 'monthly' && date('d', $s->timeend) > 1 && date('d', $s->timeend) < 29) {
+            $s->timeend = mktime(date('H', $s->timeend), date('i', $s->timeend), date('s', $s->timeend),
+                date('m', $s->timeend), 1, 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 +1491,26 @@ 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(date('H', $timeafter), date('i', $timeafter), date('s', $timeafter), $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 +1531,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
diff --git a/lib/testing/classes/nasty_strings.php b/lib/testing/classes/nasty_strings.php
new file mode 100644 (file)
index 0000000..e200263
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Nasty strings to use in tests.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Nasty strings manager.
+ *
+ * Responds to nasty strings requests with a random string of the list
+ * to try with different combinations in different places.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class nasty_strings {
+
+    /**
+     * List of different strings to fill fields and assert against them
+     *
+     * Non of these strings can be a part of another one, this would not be good
+     * when using more one string at the same time and asserting results.
+     *
+     * @static
+     * @var array
+     */
+    protected static $strings = array(
+        '< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ @@TEST@@ \\\" \\ , ; : .   日本語 ­ % %% ',
+        '&amp; \' \\" \ \'$@NULL@$ < > & &lt; &gt; @@TEST@@ \\\" \\ , ; : .   日本語 ­ % %% ',
+        '< > & &lt; &gt; &amp; \' \\" \ \\\" \\ , ; : .   \'$@NULL@$ @@TEST@@ 日本語 ­ % %% ',
+        '< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ 日本語 ­ % %%@@TEST@@ \\\" \\ , ; : .    ',
+        '< > & &lt; &gt;  \\\" \\ , ; : .   日本語&amp; \' \\" \ \'$@NULL@$ @@TEST@@ ­ % %% ',
+        '\' \\" \ \'$@NULL@$ @@TEST@@ < > & &lt; &gt; &amp; \\\" \\ , ; : .   日本語 ­ % %% ',
+        '\\\" \\ , ; : .   日本語 ­ % < > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ @@TEST@@ %% ',
+        '< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ 日本語 ­ % %% @@TEST@@ \\\" \\ , ; : .   ',
+        '.   日本語&amp; \' \\"< > & &lt; &gt;  \\\" \\ , ; :  \ \'$@NULL@$ @@TEST@@ ­ % %% ',
+        '&amp; \' \\" \ < > & &lt; &gt;  \\\" \\ , ; : .   日本語\'$@NULL@$ @@TEST@@ ­ % %% ',
+    );
+
+    /**
+     * Already used nasty strings.
+     *
+     * This array will be cleaned before each scenario.
+     *
+     * @static
+     * @var array
+     */
+    protected static $usedstrings = array();
+
+    /**
+     * Returns a nasty string and stores the key mapping.
+     *
+     * @static
+     * @param string $key The key
+     * @return string
+     */
+    public static function get($key) {
+
+        // If have been used during the this tests return it.
+        if (isset(self::$usedstrings[$key])) {
+            return self::$strings[self::$usedstrings[$key]];
+        }
+
+        // Getting non-used random string.
+        do {
+            $index = self::random_index();
+        } while (in_array($index, self::$usedstrings));
+
+        // Mark the string as already used.
+        self::$usedstrings[$key] = $index;
+
+        return self::$strings[$index];
+    }
+
+    /**
+     * Resets the used strings var.
+     * @static
+     * @return void
+     */
+    public static function reset_used_strings() {
+        self::$usedstrings = array();
+    }
+
+    /**
+     * Returns a random index.
+     * @static
+     * @return int
+     */
+    protected static function random_index() {
+        return mt_rand(0, count(self::$strings) - 1);
+    }
+
+}
index c9df3e9..260adae 100644 (file)
@@ -52,7 +52,6 @@ class behat_forms extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function press_button($button) {
-        $button = $this->fixStepArgument($button);
 
         // Ensures the button is present.
         $buttonnode = $this->find_button($button);
@@ -75,9 +74,6 @@ class behat_forms extends behat_base {
 
             unset($fieldnode);
 
-            // Removing \\ that escapes " of the steps arguments.
-            $locator = $this->fixStepArgument($locator);
-
             // Getting the NodeElement.
             $fieldnode = $this->find_field($locator);
 
@@ -85,7 +81,6 @@ class behat_forms extends behat_base {
             $field = $this->get_field($fieldnode, $locator);
 
             // Delegates to the field class.
-            $value = $this->fixStepArgument($value);
             $field->set_value($value);
         }
     }
@@ -97,8 +92,6 @@ class behat_forms extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function fill_field($field, $value) {
-        $field = $this->fixStepArgument($field);
-        $value = $this->fixStepArgument($value);
 
         $fieldnode = $this->find_field($field);
         $fieldnode->setValue($value);
@@ -111,8 +104,6 @@ class behat_forms extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function select_option($option, $select) {
-        $select = $this->fixStepArgument($select);
-        $option = $this->fixStepArgument($option);
 
         $selectnode = $this->find_field($select);
         $selectnode->selectOption($option);
@@ -128,7 +119,6 @@ class behat_forms extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function check_option($option) {
-        $option = $this->fixStepArgument($option);
 
         $checkboxnode = $this->find_field($option);
         $checkboxnode->check();
@@ -141,7 +131,6 @@ class behat_forms extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function uncheck_option($option) {
-        $option = $this->fixStepArgument($option);
 
         $checkboxnode = $this->find_field($option);
         $checkboxnode->uncheck();
@@ -158,9 +147,6 @@ class behat_forms extends behat_base {
      */
     public function the_field_should_match_value($locator, $value) {
 
-        $locator = $this->fixStepArgument($locator);
-        $value = $this->fixStepArgument($value);
-
         $fieldnode = $this->find_field($locator);
 
         // Gets the field instance.
@@ -182,7 +168,6 @@ class behat_forms extends behat_base {
      * @see Behat\MinkExtension\Context\MinkContext
      */
     public function assert_checkbox_checked($checkbox) {
-        $checkbox = $this->fixStepArgument($checkbox);
         $this->assertSession()->checkboxChecked($checkbox);
     }
 
@@ -193,7 +178,6 @@ class behat_forms extends behat_base {
      * @see Behat\MinkExtension\Context\MinkContext
      */
     public function assert_checkbox_not_checked($checkbox) {
-        $checkbox = $this->fixStepArgument($checkbox);
         $this->assertSession()->checkboxNotChecked($checkbox);
     }
 
@@ -208,9 +192,6 @@ class behat_forms extends behat_base {
      */
     public function the_select_box_should_contain($select, $option) {
 
-        $select = $this->fixStepArgument($select);
-        $option = $this->fixStepArgument($option);
-
         $selectnode = $this->find_field($select);
 
         $regex = '/' . preg_quote($option, '/') . '/ui';
@@ -234,9 +215,6 @@ class behat_forms extends behat_base {
      */
     public function the_select_box_should_not_contain($select, $option) {
 
-        $select = $this->fixStepArgument($select);
-        $option = $this->fixStepArgument($option);
-
         $selectnode = $this->find_field($select);
 
         $regex = '/' . preg_quote($option, '/') . '/ui';
@@ -258,8 +236,6 @@ class behat_forms extends behat_base {
     protected function get_field(NodeElement $fieldnode, $locator) {
         global $CFG;
 
-        $locator = $this->fixStepArgument($locator);
-
         // Get the field type.
         $type = $this->get_node_type($fieldnode, $locator);
         $classname = 'behat_form_' . $type;
@@ -289,8 +265,6 @@ class behat_forms extends behat_base {
      */
     protected function get_node_type(NodeElement $fieldnode, $locator) {
 
-        $locator = $this->fixStepArgument($locator);
-
         // We look for a parent node with 'felement' class.
         if ($class = $fieldnode->getParent()->getAttribute('class')) {
 
index 7fe1bb7..9a2c794 100644 (file)
@@ -60,7 +60,6 @@ class behat_general extends behat_base {
      * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function click_link($link) {
-        $link = $this->fixStepArgument($link);
 
         $linknode = $this->find_link($link);
         $linknode->click();
@@ -109,7 +108,7 @@ class behat_general extends behat_base {
      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
      */
     public function assert_page_contains_text($text) {
-        $this->assertSession()->pageTextContains($this->fixStepArgument($text));
+        $this->assertSession()->pageTextContains($text);
     }
 
     /**
@@ -119,7 +118,7 @@ class behat_general extends behat_base {
      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
      */
     public function assert_page_not_contains_text($text) {
-        $this->assertSession()->pageTextNotContains($this->fixStepArgument($text));
+        $this->assertSession()->pageTextNotContains($text);
     }
 
     /**
@@ -128,7 +127,7 @@ class behat_general extends behat_base {
      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
      */
     public function assert_element_contains_text($text, $element) {
-        $this->assertSession()->elementTextContains('css', $element, $this->fixStepArgument($text));
+        $this->assertSession()->elementTextContains('css', $element, $text);
     }
 
     /**
@@ -137,7 +136,7 @@ class behat_general extends behat_base {
      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
      */
     public function assert_element_not_contains_text($text, $element) {
-        $this->assertSession()->elementTextNotContains('css', $element, $this->fixStepArgument($text));
+        $this->assertSession()->elementTextNotContains('css', $element, $text);
     }
 
     /**
index f7aa002..8724326 100644 (file)
@@ -76,6 +76,7 @@ class behat_hooks extends behat_base {
         require_once(__DIR__ . '/../../behat/classes/behat_command.php');
         require_once(__DIR__ . '/../../behat/classes/util.php');
         require_once(__DIR__ . '/../../testing/classes/test_lock.php');
+        require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
 
         // Avoids vendor/bin/behat to be executed directly without test environment enabled
         // to prevent undesired db & dataroot modifications, this is also checked
@@ -119,6 +120,12 @@ class behat_hooks extends behat_base {
         behat_util::reset_database();
         behat_util::reset_dataroot();
 
+        purge_all_caches();
+        accesslib_clear_all_caches(true);
+
+        // Reset the nasty strings list used during the last test.
+        nasty_strings::reset_used_strings();
+
         // Assing valid data to admin user (some generator-related code needs a valid user).
         $user = $DB->get_record('user', array('username' => 'admin'));
         session_set_user($user);
index ad5031e..13f282d 100644 (file)
@@ -47,8 +47,6 @@ class behat_navigation extends behat_base {
      */
     public function i_expand_node($nodetext) {
 
-        $nodetext = $this->fixStepArgument($nodetext);
-
         $xpath = "//ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]
 /descendant::li
 /descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]
diff --git a/lib/tests/behat/behat_transformations.php b/lib/tests/behat/behat_transformations.php
new file mode 100644 (file)
index 0000000..9f3b2b8
--- /dev/null
@@ -0,0 +1,135 @@
+<?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/>.
+
+/**
+ * Behat arguments transformations.
+ *
+ * This methods are used by Behat CLI command.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode;
+
+/**
+ * Transformations to apply to steps arguments.
+ *
+ * This methods are applied to the steps arguments that matches
+ * the regular expressions specified in the @Transform tag.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_transformations extends behat_base {
+
+    /**
+     * Removes escaped argument delimiters.
+     *
+     * We use double quotes as arguments delimiters and
+     * to add the " as part of an argument we escape it
+     * with a backslash, this method removes this backslash.
+     *
+     * @Transform /^((.*)"(.*))$/
+     * @param string $string
+     * @return string The string with the arguments fixed.
+     */
+    public function arg_replace_slashes($string) {
+        return $this->replace_slashes($string);
+    }
+
+    /**
+     * Replaces $NASTYSTRING vars for a nasty string.
+     *
+     * @Transform /^((.*)\$NASTYSTRING(\d)(.*))$/
+     * @param string $argument The whole argument value.
+     * @return string
+     */
+    public function arg_replace_nasty_strings($argument) {
+        return $this->replace_nasty_strings($argument);
+    }
+
+    /**
+     * Transformations for TableNode arguments.
+     *
+     * All the transformations have to be applied to tables,
+     * adding them in a different method for Behat API restrictions.
+     *
+     * @Transform /^table:(.*)/
+     * @param TableNode $table
+     * @return TableNode The transformed table
+     */
+    public function tablenode_transformations(TableNode $tablenode) {
+
+        // Walk through all values including the optional headers.
+        $rows = $tablenode->getRows();
+        foreach ($rows as $rowkey => $row) {
+            foreach ($row as $colkey => $value) {
+
+                // TableNodes values doesn't need to be escaped, but maybe somebody does it.
+                $rows[$rowkey][$colkey] = $this->replace_slashes($value);
+
+                // Transforms vars into nasty strings.
+                if (preg_match('/\$NASTYSTRING(\d)/', $rows[$rowkey][$colkey])) {
+                    $rows[$rowkey][$colkey] = $this->replace_nasty_strings($rows[$rowkey][$colkey]);
+                }
+            }
+        }
+
+        // Return the transformed TableNode.
+        $tablenode->setRows($rows);
+        return $tablenode;
+    }
+
+    /**
+     * Removes the escaped double quotes.
+     *
+     * Method reused by TableNode transformation.
+     *
+     * @param string $string
+     * @return string
+     */
+    public function replace_slashes($string) {
+        return str_replace('\"', '"', $string);
+    }
+
+    /**
+     * Replaces $NASTYSTRING vars for a nasty string.
+     *
+     * Method reused by TableNode tranformation.
+     *
+     * @param string $string
+     * @return string
+     */
+    public function replace_nasty_strings($string) {
+        return preg_replace_callback(
+            '/\$NASTYSTRING(\d)/',
+            function ($matches) {
+                return nasty_strings::get($matches[0]);
+            },
+            $string
+        );
+    }
+
+}
index 111026c..9382583 100644 (file)
@@ -1448,8 +1448,9 @@ function install_core($version, $verbose) {
 
         print_upgrade_part_end(null, true, $verbose);
 
-        // Reset the cache, this returns it to a normal operation state.
-        cache_factory::reset();
+        // Purge all caches. They're disabled but this ensures that we don't have any persistent data just in case something
+        // during installation didn't use APIs.
+        cache_helper::purge_all();
     } catch (exception $ex) {
         upgrade_handle_exception($ex);
     }
@@ -1536,8 +1537,8 @@ function upgrade_noncore($verbose) {
 
     // upgrade all plugins types
     try {
-        // Disable the use of cache stores here. We will reset the factory after we've performed the installation.
-        // This ensures that we don't permanently cache anything during installation.
+        // Disable the use of cache stores here.
+        // We don't reset this, the site can live without proper caching for life of this request.
         cache_factory::disable_stores();
 
         $plugintypes = get_plugin_types();
@@ -1546,8 +1547,6 @@ function upgrade_noncore($verbose) {
         }
         // Update cache definitions. Involves scanning each plugin for any changes.
         cache_helper::update_definitions();
-        // Reset the cache system to a normal state.
-        cache_factory::reset();
     } catch (Exception $ex) {
         upgrade_handle_exception($ex);
     }
index a5ad1c4..58b8083 100644 (file)
@@ -185,7 +185,7 @@ class mod_assign_external extends external_api {
             array(
                 'assignments' => new external_multiple_structure(self::assign_grades(), 'list of assignment grade information'),
                 'warnings'      => new external_warnings('item is always \'assignment\'',
-                    'when errorcode is 3 then itemid is an assignment id. When errorcode is 1, itemid is a course module instance id',
+                    'when errorcode is 3 then itemid is an assignment id. When errorcode is 1, itemid is a course module id',
                     'errorcode can be 3 (no grades found) or 1 (no permission to get grades)')
             )
         );
index ef1c7ed..a189bae 100644 (file)
@@ -324,4 +324,7 @@ $string['viewfull'] = 'View full';
 $string['viewsummary'] = 'View summary';
 $string['viewsubmissiongradingtable'] = 'View submission grading table.';
 $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
+$string['submissiontypes'] = 'Submission types';
+$string['feedbacktypes'] = 'Feedback types';
+$string['groupsubmissionsettings'] = 'Group submission settings';
 
index e28ba43..e428889 100644 (file)
@@ -657,7 +657,7 @@ class assign {
     protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
         if ($plugin->is_visible()) {
             $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
-            if ($formdata->$enabledname) {
+            if (!empty($formdata->$enabledname)) {
                 $plugin->enable();
                 if (!$plugin->save_settings($formdata)) {
                     print_error($plugin->get_error());
@@ -843,17 +843,18 @@ class assign {
      * @param assign_plugin $plugin The plugin to add the settings from
      * @param MoodleQuickForm $mform The form to add the configuration settings to.
      *                               This form is modified directly (not returned).
+     * @param array $pluginsenabled A list of form elements to be added to a group.
+     *                              The new element is added to this array by this function.
      * @return void
      */
-    protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform) {
+    protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
         global $CFG;
         if ($plugin->is_visible()) {
-            $mform->addElement('selectyesno',
-                               $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled',
-                               $plugin->get_name());
-            $mform->addHelpButton($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled',
-                                  'enabled',
-                                  $plugin->get_subtype() . '_' . $plugin->get_type());
+
+            $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
+            $label = $plugin->get_name();
+            $label .= ' ' . $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
+            $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
 
             $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
             if ($plugin->get_config('enabled') !== false) {
@@ -874,16 +875,23 @@ class assign {
      * @return void
      */
     public function add_all_plugin_settings(MoodleQuickForm $mform) {
-        $mform->addElement('header', 'general', get_string('submissionsettings', 'assign'));
+        $mform->addElement('header', 'submissiontypes', get_string('submissionsettings', 'assign'));
 
+        $submissionpluginsenabled = array();
+        $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
         foreach ($this->submissionplugins as $plugin) {
-            $this->add_plugin_settings($plugin, $mform);
-
+            $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
         }
-        $mform->addElement('header', 'general', get_string('feedbacksettings', 'assign'));
+        $group->setElements($submissionpluginsenabled);
+
+        $mform->addElement('header', 'feedbacktypes', get_string('feedbacksettings', 'assign'));
+        $feedbackpluginsenabled = array();
+        $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
         foreach ($this->feedbackplugins as $plugin) {
-            $this->add_plugin_settings($plugin, $mform);
+            $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
         }
+        $group->setElements($feedbackpluginsenabled);
+        $mform->setExpanded('submissiontypes');
     }
 
     /**
@@ -3112,7 +3120,6 @@ class assign {
                     }
                 }
 
-
                 $feedbackstatus = new assign_feedback_status($gradefordisplay,
                                                       $gradeddate,
                                                       $grader,
index 70bc77b..998d2e7 100644 (file)
@@ -73,7 +73,9 @@ class mod_assign_mod_form extends moodleform_mod {
 
         $config = get_config('assign');
 
-        $mform->addElement('header', 'general', get_string('settings', 'assign'));
+        $assignment->add_all_plugin_settings($mform);
+
+        $mform->addElement('header', 'availability', get_string('availability', 'assign'));
 
         $name = get_string('allowsubmissionsfromdate', 'assign');
         $options = array('optional'=>true);
@@ -92,10 +94,12 @@ class mod_assign_mod_form extends moodleform_mod {
         $mform->setDefault('cutoffdate', time()+7*24*3600);
 
         $name = get_string('alwaysshowdescription', 'assign');
-        $mform->addElement('selectyesno', 'alwaysshowdescription', $name);
+        $mform->addElement('checkbox', 'alwaysshowdescription', $name);
         $mform->addHelpButton('alwaysshowdescription', 'alwaysshowdescription', 'assign');
         $mform->setDefault('alwaysshowdescription', 1);
 
+        $mform->addElement('header', 'submissionsettings', get_string('submissionsettings', 'assign'));
+
         $name = get_string('submissiondrafts', 'assign');
         $mform->addElement('selectyesno', 'submissiondrafts', $name);
         $mform->addHelpButton('submissiondrafts', 'submissiondrafts', 'assign');
@@ -114,16 +118,7 @@ class mod_assign_mod_form extends moodleform_mod {
             $mform->addElement('hidden', 'requiresubmissionstatement', 1);
         }
 
-        $name = get_string('sendnotifications', 'assign');
-        $mform->addElement('selectyesno', 'sendnotifications', $name);
-        $mform->addHelpButton('sendnotifications', 'sendnotifications', 'assign');
-        $mform->setDefault('sendnotifications', 1);
-
-        $name = get_string('sendlatenotifications', 'assign');
-        $mform->addElement('selectyesno', 'sendlatenotifications', $name);
-        $mform->addHelpButton('sendlatenotifications', 'sendlatenotifications', 'assign');
-        $mform->setDefault('sendlatenotifications', 1);
-        $mform->disabledIf('sendlatenotifications', 'sendnotifications', 'eq', 1);
+        $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
 
         $name = get_string('teamsubmission', 'assign');
         $mform->addElement('selectyesno', 'teamsubmission', $name);
@@ -150,13 +145,18 @@ class mod_assign_mod_form extends moodleform_mod {
         $mform->setDefault('teamsubmissiongroupingid', 0);
         $mform->disabledIf('teamsubmissiongroupingid', 'teamsubmission', 'eq', 0);
 
-        $name = get_string('blindmarking', 'assign');
-        $mform->addElement('selectyesno', 'blindmarking', $name);
-        $mform->addHelpButton('blindmarking', 'blindmarking', 'assign');
-        $mform->setDefault('blindmarking', 0);
-        if ($assignment->has_submissions_or_grades() ) {
-            $mform->freeze('blindmarking');
-        }
+        $mform->addElement('header', 'notifications', get_string('notifications', 'assign'));
+
+        $name = get_string('sendnotifications', 'assign');
+        $mform->addElement('selectyesno', 'sendnotifications', $name);
+        $mform->addHelpButton('sendnotifications', 'sendnotifications', 'assign');
+        $mform->setDefault('sendnotifications', 1);
+
+        $name = get_string('sendlatenotifications', 'assign');
+        $mform->addElement('selectyesno', 'sendlatenotifications', $name);
+        $mform->addHelpButton('sendlatenotifications', 'sendlatenotifications', 'assign');
+        $mform->setDefault('sendlatenotifications', 1);
+        $mform->disabledIf('sendlatenotifications', 'sendnotifications', 'eq', 1);
 
         // Plagiarism enabling form.
         if (!empty($CFG->enableplagiarism)) {
@@ -164,8 +164,15 @@ class mod_assign_mod_form extends moodleform_mod {
             plagiarism_get_form_elements_module($mform, $ctx->get_course_context(), 'mod_assign');
         }
 
-        $assignment->add_all_plugin_settings($mform);
         $this->standard_grading_coursemodule_elements();
+        $name = get_string('blindmarking', 'assign');
+        $mform->addElement('selectyesno', 'blindmarking', $name);
+        $mform->addHelpButton('blindmarking', 'blindmarking', 'assign');
+        $mform->setDefault('blindmarking', 0);
+        if ($assignment->has_submissions_or_grades() ) {
+            $mform->freeze('blindmarking');
+        }
+
         $this->standard_coursemodule_elements();
 
         $this->add_action_buttons();
@@ -177,6 +184,7 @@ class mod_assign_mod_form extends moodleform_mod {
                                                   'assignment = ? AND grade <> -1',
                                                   array($this->_instance));
         }
+
         if ($mform->elementExists('grade') && $hasgrade) {
             $module = array(
                 'name' => 'mod_assign',
index 3d16000..bd08924 100644 (file)
@@ -85,7 +85,7 @@ class assign_submission_file extends assign_submission_plugin {
                               'maxfilessubmission',
                               'assignsubmission_file');
         $mform->setDefault('assignsubmission_file_maxfiles', $defaultmaxfilesubmissions);
-        $mform->disabledIf('assignsubmission_file_maxfiles', 'assignsubmission_file_enabled', 'eq', 0);
+        $mform->disabledIf('assignsubmission_file_maxfiles', 'assignsubmission_file_enabled', 'notchecked');
 
         $choices = get_max_upload_sizes($CFG->maxbytes,
                                         $COURSE->maxbytes,
@@ -105,7 +105,7 @@ class assign_submission_file extends assign_submission_plugin {
         $mform->setDefault('assignsubmission_file_maxsizebytes', $defaultmaxsubmissionsizebytes);
         $mform->disabledIf('assignsubmission_file_maxsizebytes',
                            'assignsubmission_file_enabled',
-                           'eq', 0);
+                           'notchecked');
     }
 
     /**
index 11a3de4..19d049c 100644 (file)
@@ -181,7 +181,6 @@ class mod_assign_lib_testcase extends advanced_testcase {
 
         $submission = $assign->get_user_submission($this->students[0]->id, true);
 
-
         $this->expectOutputRegex('/Draft/');
         assign_user_complete($this->course, $this->students[0], $assign->get_course_module(), $assign->get_instance());
 
index 6f533b8..2aba609 100644 (file)
@@ -38,18 +38,42 @@ require_once($CFG->dirroot . '/mod/assign/upgradelib.php');
  */
 class mod_assign_locallib_testcase extends advanced_testcase {
 
+    /** @const Default number of students to create */
+    const DEFAULT_STUDENT_COUNT = 3;
+    /** @const Default number of teachers to create */
+    const DEFAULT_TEACHER_COUNT = 2;
+    /** @const Default number of editing teachers to create */
+    const DEFAULT_EDITING_TEACHER_COUNT = 2;
+    /** @const Optional extra number of students to create */
+    const EXTRA_STUDENT_COUNT = 40;
+    /** @const Optional extra number of teachers to create */
+    const EXTRA_TEACHER_COUNT = 5;
+    /** @const Optional extra number of editing teachers to create */
+    const EXTRA_EDITING_TEACHER_COUNT = 5;
+    /** @const Number of groups to create */
+    const GROUP_COUNT = 6;
+
     /** @var stdClass $course New course created to hold the assignments */
     protected $course = null;
 
-    /** @var array $teachers List of 5 default teachers in the course*/
+    /** @var array $teachers List of DEFAULT_TEACHER_COUNT teachers in the course*/
     protected $teachers = null;
 
-    /** @var array $editingteachers List of 5 default editing teachers in the course*/
+    /** @var array $editingteachers List of DEFAULT_EDITING_TEACHER_COUNT editing teachers in the course */
     protected $editingteachers = null;
 
-    /** @var array $students List of 100 default students in the course*/
+    /** @var array $students List of DEFAULT_STUDENT_COUNT students in the course*/
     protected $students = null;
 
+    /** @var array $extrateachers List of EXTRA_TEACHER_COUNT teachers in the course*/
+    protected $extrateachers = null;
+
+    /** @var array $extraeditingteachers List of EXTRA_EDITING_TEACHER_COUNT editing teachers in the course*/
+    protected $extraeditingteachers = null;
+
+    /** @var array $extrastudents List of EXTRA_STUDENT_COUNT students in the course*/
+    protected $extrastudents = null;
+
     /** @var array $groups List of 10 groups in the course */
     protected $groups = null;
 
@@ -57,28 +81,28 @@ class mod_assign_locallib_testcase extends advanced_testcase {
      * Setup function - we will create a course and add an assign instance to it.
      */
     protected function setUp() {
-        global $DB, $CFG;
+        global $DB;
 
         $this->resetAfterTest(true);
 
         $this->course = $this->getDataGenerator()->create_course();
         $this->teachers = array();
-        for ($i = 0; $i < 5; $i++) {
+        for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
         }
 
         $this->editingteachers = array();
-        for ($i = 0; $i < 5; $i++) {
+        for ($i = 0; $i < self::DEFAULT_EDITING_TEACHER_COUNT; $i++) {
             array_push($this->editingteachers, $this->getDataGenerator()->create_user());
         }
 
         $this->students = array();
-        for ($i = 0; $i < 100; $i++) {
+        for ($i = 0; $i < self::DEFAULT_STUDENT_COUNT; $i++) {
             array_push($this->students, $this->getDataGenerator()->create_user());
         }
 
         $this->groups = array();
-        for ($i = 0; $i < 10; $i++) {
+        for ($i = 0; $i < self::GROUP_COUNT; $i++) {
             array_push($this->groups, $this->getDataGenerator()->create_group(array('courseid'=>$this->course->id)));
         }
 
@@ -87,7 +111,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
             $this->getDataGenerator()->enrol_user($teacher->id,
                                                   $this->course->id,
                                                   $teacherrole->id);
-            groups_add_member($this->groups[$i % 10], $teacher);
+            groups_add_member($this->groups[$i % self::GROUP_COUNT], $teacher);
         }
 
         $editingteacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'));
@@ -95,7 +119,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
             $this->getDataGenerator()->enrol_user($editingteacher->id,
                                                   $this->course->id,
                                                   $editingteacherrole->id);
-            groups_add_member($this->groups[$i % 10], $editingteacher);
+            groups_add_member($this->groups[$i % self::GROUP_COUNT], $editingteacher);
         }
 
         $studentrole = $DB->get_record('role', array('shortname'=>'student'));
@@ -103,10 +127,56 @@ class mod_assign_locallib_testcase extends advanced_testcase {
             $this->getDataGenerator()->enrol_user($student->id,
                                                   $this->course->id,
                                                   $studentrole->id);
-            if ($i < 80) {
-                groups_add_member($this->groups[$i % 10], $student);
+            groups_add_member($this->groups[$i % self::GROUP_COUNT], $student);
+        }
+    }
+
+    /*
+     * For tests that make sense to use alot of data, create extra students/teachers.
+     */
+    private function createExtraUsers() {
+        global $DB;
+        $this->extrateachers = array();
+        for ($i = 0; $i < self::EXTRA_TEACHER_COUNT; $i++) {
+            array_push($this->extrateachers, $this->getDataGenerator()->create_user());
+        }
+
+        $this->extraeditingteachers = array();
+        for ($i = 0; $i < self::EXTRA_EDITING_TEACHER_COUNT; $i++) {
+            array_push($this->extraeditingteachers, $this->getDataGenerator()->create_user());
+        }
+
+        $this->extrastudents = array();
+        for ($i = 0; $i < self::EXTRA_STUDENT_COUNT; $i++) {
+            array_push($this->extrastudents, $this->getDataGenerator()->create_user());
+        }
+
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        foreach ($this->extrateachers as $i => $teacher) {
+            $this->getDataGenerator()->enrol_user($teacher->id,
+                                                  $this->course->id,
+                                                  $teacherrole->id);
+            groups_add_member($this->groups[$i % self::GROUP_COUNT], $teacher);
+        }
+
+        $editingteacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'));
+        foreach ($this->extraeditingteachers as $i => $editingteacher) {
+            $this->getDataGenerator()->enrol_user($editingteacher->id,
+                                                  $this->course->id,
+                                                  $editingteacherrole->id);
+            groups_add_member($this->groups[$i % self::GROUP_COUNT], $editingteacher);
+        }
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        foreach ($this->extrastudents as $i => $student) {
+            $this->getDataGenerator()->enrol_user($student->id,
+                                                  $this->course->id,
+                                                  $studentrole->id);
+            if ($i < (self::EXTRA_STUDENT_COUNT / 2)) {
+                groups_add_member($this->groups[$i % self::GROUP_COUNT], $student);
             }
         }
+
     }
 
     private function create_instance($params=array()) {
@@ -368,26 +438,29 @@ class mod_assign_locallib_testcase extends advanced_testcase {
     }
 
     public function test_list_participants() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance(array('grade'=>100));
 
-        $this->assertEquals(100, count($assign->list_participants(null, true)));
+        $this->assertEquals(self::DEFAULT_STUDENT_COUNT + self::EXTRA_STUDENT_COUNT, count($assign->list_participants(null, true)));
     }
 
     public function test_count_teams() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance(array('teamsubmission'=>1));
 
-        $this->assertEquals(11, $assign->count_teams());
+        $this->assertEquals(self::GROUP_COUNT + 1, $assign->count_teams());
     }
 
     public function test_count_submissions() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled'=>1));
 
         // Simulate a submission.
-        $this->setUser($this->students[0]);
-        $submission = $assign->get_user_submission($this->students[0]->id, true);
+        $this->setUser($this->extrastudents[0]);
+        $submission = $assign->get_user_submission($this->extrastudents[0]->id, true);
         // Leave this one as DRAFT.
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
@@ -400,13 +473,13 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $this->setUser($this->teachers[0]);
         $data = new stdClass();
         $data->grade = '50.0';
-        $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+        $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id);
 
         // Simulate a submission.
-        $this->setUser($this->students[1]);
-        $submission = $assign->get_user_submission($this->students[1]->id, true);
+        $this->setUser($this->extrastudents[1]);
+        $submission = $assign->get_user_submission($this->extrastudents[1]->id, true);
         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-        $assign->testable_update_submission($submission, $this->students[1]->id, true, false);
+        $assign->testable_update_submission($submission, $this->extrastudents[1]->id, true, false);
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
                                          'text'=>'Submission text',
@@ -415,10 +488,10 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $plugin->save($submission, $data);
 
         // Simulate a submission.
-        $this->setUser($this->students[2]);
-        $submission = $assign->get_user_submission($this->students[2]->id, true);
+        $this->setUser($this->extrastudents[2]);
+        $submission = $assign->get_user_submission($this->extrastudents[2]->id, true);
         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-        $assign->testable_update_submission($submission, $this->students[2]->id, true, false);
+        $assign->testable_update_submission($submission, $this->extrastudents[2]->id, true, false);
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
                                          'text'=>'Submission text',
@@ -427,10 +500,10 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $plugin->save($submission, $data);
 
         // Simulate a submission.
-        $this->setUser($this->students[3]);
-        $submission = $assign->get_user_submission($this->students[3]->id, true);
+        $this->setUser($this->extrastudents[3]);
+        $submission = $assign->get_user_submission($this->extrastudents[3]->id, true);
         $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-        $assign->testable_update_submission($submission, $this->students[3]->id, true, false);
+        $assign->testable_update_submission($submission, $this->extrastudents[3]->id, true, false);
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
                                          'text'=>'Submission text',
@@ -442,7 +515,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $this->setUser($this->teachers[0]);
         $data = new stdClass();
         $data->grade = '50.0';
-        $assign->testable_apply_grade_to_user($data, $this->students[3]->id);
+        $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id);
 
         $this->assertEquals(2, $assign->count_grades());
         $this->assertEquals(4, $assign->count_submissions());
@@ -452,11 +525,12 @@ class mod_assign_locallib_testcase extends advanced_testcase {
     }
 
     public function test_get_grading_userid_list() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance();
 
         $users = $assign->testable_get_grading_userid_list();
-        $this->assertEquals(100, count($users));
+        $this->assertEquals(self::DEFAULT_STUDENT_COUNT + self::EXTRA_STUDENT_COUNT, count($users));
     }
 
     public function test_cron() {
@@ -472,7 +546,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $this->setUser($this->teachers[0]);
         $data = new stdClass();
         $data->grade = '50.0';
-        $assign->testable_apply_grade_to_user($data, $this->students[3]->id);
+        $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
 
         // Now run cron and see that one message was sent.
         $this->preventResetByRollback();
@@ -521,13 +595,14 @@ class mod_assign_locallib_testcase extends advanced_testcase {
 
 
     public function test_update_submission() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance();
 
-        $this->setUser($this->students[0]);
+        $this->setUser($this->extrastudents[0]);
         $now = time();
-        $submission = $assign->get_user_submission($this->students[0]->id, true);
-        $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
+        $submission = $assign->get_user_submission($this->extrastudents[0]->id, true);
+        $assign->testable_update_submission($submission, $this->extrastudents[0]->id, true, false);
 
         $this->setUser($this->teachers[0]);
         // Verify the gradebook update.
@@ -535,19 +610,19 @@ class mod_assign_locallib_testcase extends advanced_testcase {
                                         'mod',
                                         'assign',
                                         $assign->get_instance()->id,
-                                        $this->students[0]->id);
+                                        $this->extrastudents[0]->id);
 
-        $this->assertEquals($this->students[0]->id,
-                            $gradinginfo->items[0]->grades[$this->students[0]->id]->usermodified);
+        $this->assertEquals($this->extrastudents[0]->id,
+                            $gradinginfo->items[0]->grades[$this->extrastudents[0]->id]->usermodified);
 
         // Now verify group assignments.
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance(array('teamsubmission'=>1));
 
-        $this->setUser($this->students[0]);
+        $this->setUser($this->extrastudents[0]);
         $now = time();
-        $submission = $assign->get_group_submission($this->students[0]->id, 0, true);
-        $assign->testable_update_submission($submission, $this->students[0]->id, true, true);
+        $submission = $assign->get_group_submission($this->extrastudents[0]->id, 0, true);
+        $assign->testable_update_submission($submission, $this->extrastudents[0]->id, true, true);
 
         // Check that at least 2 members of the submission group had their submission updated.
 
@@ -556,37 +631,37 @@ class mod_assign_locallib_testcase extends advanced_testcase {
                                         'mod',
                                         'assign',
                                         $assign->get_instance()->id,
-                                        $this->students[0]->id);
+                                        $this->extrastudents[0]->id);
 
-        $this->assertEquals($this->students[0]->id,
-                            $gradinginfo->items[0]->grades[$this->students[0]->id]->usermodified);
+        $this->assertEquals($this->extrastudents[0]->id,
+                            $gradinginfo->items[0]->grades[$this->extrastudents[0]->id]->usermodified);
 
         $gradinginfo = grade_get_grades($this->course->id,
                                         'mod',
                                         'assign',
                                         $assign->get_instance()->id,
-                                        $this->students[10]->id);
+                                        $this->extrastudents[self::GROUP_COUNT]->id);
 
-        $this->assertEquals($this->students[10]->id,
-                            $gradinginfo->items[0]->grades[$this->students[10]->id]->usermodified);
+        $this->assertEquals($this->extrastudents[self::GROUP_COUNT]->id,
+                            $gradinginfo->items[0]->grades[$this->extrastudents[self::GROUP_COUNT]->id]->usermodified);
 
         // Now verify blind marking.
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance(array('blindmarking'=>1));
 
-        $this->setUser($this->students[0]);
+        $this->setUser($this->extrastudents[0]);
         $now = time();
-        $submission = $assign->get_user_submission($this->students[0]->id, true);
-        $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
+        $submission = $assign->get_user_submission($this->extrastudents[0]->id, true);
+        $assign->testable_update_submission($submission, $this->extrastudents[0]->id, true, false);
 
         $this->setUser($this->editingteachers[0]);
         $gradinginfo = grade_get_grades($this->course->id,
                                         'mod',
                                         'assign',
                                         $assign->get_instance()->id,
-                                        $this->students[0]->id);
+                                        $this->extrastudents[0]->id);
 
-        $this->assertEquals(null, $gradinginfo->items[0]->grades[$this->students[0]->id]->datesubmitted);
+        $this->assertEquals(null, $gradinginfo->items[0]->grades[$this->extrastudents[0]->id]->datesubmitted);
     }
 
     public function test_submissions_open() {
@@ -628,10 +703,15 @@ class mod_assign_locallib_testcase extends advanced_testcase {
     }
 
     public function test_get_graders() {
+        $this->createExtraUsers();
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance();
 
-        $this->assertCount(10, $assign->testable_get_graders($this->students[0]->id));
+        $this->assertCount(self::DEFAULT_TEACHER_COUNT +
+                           self::DEFAULT_EDITING_TEACHER_COUNT +
+                           self::EXTRA_TEACHER_COUNT +
+                           self::EXTRA_EDITING_TEACHER_COUNT,
+                           $assign->testable_get_graders($this->students[0]->id));
 
         $assign = $this->create_instance();
         // Force create an assignment with SEPARATEGROUPS.
@@ -645,7 +725,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $assign = new testable_assign($context, $cm, $this->course);
 
         $this->setUser($this->students[1]);
-        $this->assertCount(2, $assign->testable_get_graders($this->students[0]->id));
+        $this->assertCount(4, $assign->testable_get_graders($this->students[0]->id));
     }
 
     public function test_get_uniqueid_for_user() {
@@ -674,7 +754,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $data->grade = '50.0';
         $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
 
-        // Now we should see the feedback
+        // Now we should see the feedback.
         $this->setUser($this->students[0]);
         $output = $assign->view_student_summary($this->students[0], true);
         $this->assertNotEquals(false, strpos($output, 'Feedback'), 'Show feedback if there is a grade');
@@ -695,7 +775,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $output = $assign->view_student_summary($this->students[0], true);
         $this->assertEquals(false, strpos($output, 'Feedback'), 'Do not show feedback if the grade is hidden in the gradebook');
 
-        // Do the same but add feedback
+        // Do the same but add feedback.
         $assign = $this->create_instance(array('assignfeedback_comments_enabled' => 1));
 
         $this->setUser($this->teachers[0]);
@@ -706,7 +786,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $plugin = $assign->get_feedback_plugin_by_type('comments');
         $plugin->save($grade, $data);
 
-        // Should have feedback but no grade
+        // Should have feedback but no grade.
         $this->setUser($this->students[0]);
         $output = $assign->view_student_summary($this->students[0], true);
         $this->assertNotEquals(false, strpos($output, 'Tomato sauce'), 'Show feedback even if there is no grade');
index 3a23941..1dff332 100644 (file)
@@ -3,7 +3,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 class mod_data_renderer extends plugin_renderer_base {
-    
+
     public function import_setting_mappings($datamodule, data_preset_importer $importer) {
 
         $strblank = get_string('blank', 'data');
@@ -24,7 +24,7 @@ class mod_data_renderer extends plugin_renderer_base {
         $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'action', 'value'=>'finishimport'));
         $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
         $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'d', 'value'=>$datamodule->id));
-        
+
         if ($importer instanceof data_preset_existing_importer) {
             $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'fullname', 'value'=>$importer->get_userid().'/'.$importer->get_directory()));
         } else {
@@ -60,7 +60,7 @@ class mod_data_renderer extends plugin_renderer_base {
                 } else {
                     $row[1] .= html_writer::tag('option', get_string('mapnewfield', 'data'), array('value'=>'-1', 'selected'=>'selected'));
                 }
-                
+
                 $row[1] .= html_writer::end_tag('select');
                 $table->data[] = $row;
             }
@@ -82,5 +82,5 @@ class mod_data_renderer extends plugin_renderer_base {
 
         return $html;
     }
-    
-}
\ No newline at end of file
+
+}
index ccaad7b..576cd99 100644 (file)
@@ -62,6 +62,22 @@ function xmldb_label_upgrade($oldversion) {
     // Moodle v2.4.0 release upgrade line
     // Put any upgrade step following this
 
+    if ($oldversion < 2013021400) {
+        // find all courses that contain labels and reset their cache
+        $modid = $DB->get_field_sql("SELECT id FROM {modules} WHERE name=?",
+                array('label'));
+        if ($modid) {
+            $courses = $DB->get_fieldset_sql('SELECT DISTINCT course '.
+                'FROM {course_modules} WHERE module=?', array($modid));
+            foreach ($courses as $courseid) {
+                $DB->execute('UPDATE {course} set modinfo = ?, sectioncache = ? '.
+                        'WHERE id = ?', array(null, null, $courseid));
+            }
+        }
+
+        // label savepoint reached
+        upgrade_mod_savepoint(true, 2013021400, 'label');
+    }
 
     return true;
 }
index 7ab153c..e265129 100644 (file)
@@ -129,9 +129,9 @@ function label_get_coursemodule_info($coursemodule) {
             $label->name = "label{$label->id}";
             $DB->set_field('label', 'name', $label->name, array('id'=>$label->id));
         }
-        $info = new stdClass();
+        $info = new cached_cm_info();
         // no filtering hre because this info is cached and filtered later
-        $info->extra = format_module_intro('label', $label, $coursemodule->id, false);
+        $info->content = format_module_intro('label', $label, $coursemodule->id, false);
         $info->name  = $label->name;
         return $info;
     } else {
index 7d1e67f..3c91dd2 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2012112900;       // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2013021400;       // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2012112900;    // Requires this Moodle version
 $module->component = 'mod_label'; // Full name of the plugin (used for diagnostics)
 $module->cron      = 0;
index 42550fc..e5e01c4 100644 (file)
@@ -1553,48 +1553,6 @@ function quiz_get_extra_capabilities() {
     return $caps;
 }
 
-/**
- * This fucntion extends the global navigation for the site.
- * It is important to note that you should not rely on PAGE objects within this
- * body of code as there is no guarantee that during an AJAX request they are
- * available
- *
- * @param navigation_node $quiznode The quiz node within the global navigation
- * @param object $course The course object returned from the DB
- * @param object $module The module object returned from the DB
- * @param object $cm The course module instance returned from the DB
- */
-function quiz_extend_navigation($quiznode, $course, $module, $cm) {
-    global $CFG;
-
-    $context = context_module::instance($cm->id);
-
-    if (has_capability('mod/quiz:view', $context)) {
-        $url = new moodle_url('/mod/quiz/view.php', array('id'=>$cm->id));
-        $quiznode->add(get_string('info', 'quiz'), $url, navigation_node::TYPE_SETTING,
-                null, null, new pix_icon('i/info', ''));
-    }
-
-    if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
-        require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
-        $reportlist = quiz_report_list($context);
-
-        $url = new moodle_url('/mod/quiz/report.php',
-                array('id' => $cm->id, 'mode' => reset($reportlist)));
-        $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
-                navigation_node::TYPE_SETTING,
-                null, null, new pix_icon('i/report', ''));
-
-        foreach ($reportlist as $report) {
-            $url = new moodle_url('/mod/quiz/report.php',
-                    array('id' => $cm->id, 'mode' => $report));
-            $reportnode->add(get_string($report, 'quiz_'.$report), $url,
-                    navigation_node::TYPE_SETTING,
-                    null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
-        }
-    }
-}
-
 /**
  * This function extends the settings navigation block for the site.
  *
@@ -1603,6 +1561,7 @@ function quiz_extend_navigation($quiznode, $course, $module, $cm) {
  *
  * @param settings_navigation $settings
  * @param navigation_node $quiznode
+ * @return void
  */
 function quiz_extend_settings_navigation($settings, $quiznode) {
     global $PAGE, $CFG;
@@ -1652,6 +1611,25 @@ function quiz_extend_settings_navigation($settings, $quiznode) {
         $quiznode->add_node($node, $beforekey);
     }
 
+    if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $PAGE->cm->context)) {
+        require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+        $reportlist = quiz_report_list($PAGE->cm->context);
+
+        $url = new moodle_url('/mod/quiz/report.php',
+                array('id' => $PAGE->cm->id, 'mode' => reset($reportlist)));
+        $reportnode = $quiznode->add_node(navigation_node::create(get_string('results', 'quiz'), $url,
+                navigation_node::TYPE_SETTING,
+                null, null, new pix_icon('i/report', '')), $beforekey);
+
+        foreach ($reportlist as $report) {
+            $url = new moodle_url('/mod/quiz/report.php',
+                    array('id' => $PAGE->cm->id, 'mode' => $report));
+            $reportnode->add_node(navigation_node::create(get_string($report, 'quiz_'.$report), $url,
+                    navigation_node::TYPE_SETTING,
+                    null, 'quiz_report_' . $report, new pix_icon('i/item', '')));
+        }
+    }
+
     question_extend_settings_navigation($quiznode, $PAGE->cm->context)->trim_if_empty();
 }
 
index e03cf39..6c683b0 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2012112900;       // The current module version (Date: YYYYMMDDXX).
+$module->version   = 2013310100;       // The current module version (Date: YYYYMMDDXX).
 $module->requires  = 2012112900;    // Requires this Moodle version.
 $module->component = 'mod_quiz';       // Full name of the plugin (used for diagnostics).
 $module->cron      = 60;
index 86dbbff..deaa9ae 100644 (file)
@@ -34,6 +34,7 @@ class scorm_basic_report extends scorm_default_report {
      */
     function display($scorm, $cm, $course, $download) {
         global $CFG, $DB, $OUTPUT, $PAGE;
+
         $contextmodule= context_module::instance($cm->id);
         $action = optional_param('action', '', PARAM_ALPHA);
         $attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
@@ -271,7 +272,7 @@ class scorm_basic_report extends scorm_default_report {
             $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
             $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
                     'u.id AS userid, u.idnumber, u.firstname, u.lastname, u.picture, u.imagealt, u.email' .
-                    get_extra_user_fields_sql($coursecontext, 'u', '', array('idnumber')) . ' ';
+                    get_extra_user_fields_sql($coursecontext, 'u', '', array('email', 'idnumber')) . ' ';
 
             // This part is the same for all cases - join users and scorm_scoes_track tables
             $from = 'FROM {user} u ';
index 1a294dc..4350157 100644 (file)
@@ -79,11 +79,11 @@ for ($i = 0; $i < $bands; $i++) {
 
 // Do this only if we have students to report
 if(!$nostudents) {
-    // Construct the SQL\r
-    $select = 'SELECT DISTINCT '.$DB->sql_concat('st.userid', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';\r
-    $select .= 'st.userid AS userid, st.scormid AS scormid, st.attempt AS attempt, st.scoid AS scoid ';\r
-    $from = 'FROM {scorm_scoes_track} st ';\r
-    $where = ' WHERE st.userid ' .$usql. ' and st.scoid = ?';\r
+    // Construct the SQL
+    $select = 'SELECT DISTINCT '.$DB->sql_concat('st.userid', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
+    $select .= 'st.userid AS userid, st.scormid AS scormid, st.attempt AS attempt, st.scoid AS scoid ';
+    $from = 'FROM {scorm_scoes_track} st ';
+    $where = ' WHERE st.userid ' .$usql. ' and st.scoid = ?';
     $attempts = $DB->get_records_sql($select.$from.$where, $params);
 
     foreach ($attempts as $attempt) {
index 167be51..0b26637 100644 (file)
@@ -35,6 +35,7 @@ class scorm_interactions_report extends scorm_default_report {
      */
     function display($scorm, $cm, $course, $download) {
         global $CFG, $DB, $OUTPUT, $PAGE;
+
         $contextmodule = context_module::instance($cm->id);
         $action = optional_param('action', '', PARAM_ALPHA);
         $attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
@@ -159,7 +160,7 @@ class scorm_interactions_report extends scorm_default_report {
             $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
             $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
                     'u.id AS userid, u.idnumber, u.firstname, u.lastname, u.picture, u.imagealt, u.email'.
-                    get_extra_user_fields_sql($coursecontext, 'u', '', array('idnumber')) . ' ';
+                    get_extra_user_fields_sql($coursecontext, 'u', '', array('email', 'idnumber')) . ' ';
 
             // This part is the same for all cases - join users and scorm_scoes_track tables
             $from = 'FROM {user} u ';
index 93bb583..f4d9183 100644 (file)
-<?xml version='1.0' encoding='utf-8'?>\r
-<POOL>\r
-    <TITLE value='exam 3 2008-9'/>\r
-    <QUESTIONLIST>\r
-        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>\r
-        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>\r
-        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>\r
-        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>\r
-        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>\r
-        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>\r
-    </QUESTIONLIST>\r
-    <QUESTION_TRUEFALSE id='q1'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q1_a1'>\r
-            <TEXT>False</TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q1_a2'>\r
-            <TEXT>True</TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q1_a2'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_TRUEFALSE>\r
-    <QUESTION_MULTIPLECHOICE id='q7'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q7_a1' position='1'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q7_a2' position='2'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q7_a3' position='3'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q7_a2'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_MULTIPLECHOICE>\r
-    <QUESTION_MULTIPLEANSWER id='q8'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q8_a1' position='1'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a2' position='2'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a3' position='3'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a4' position='4'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q8_a1'/>\r
-            <CORRECTANSWER answer_id='q8_a3'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_MULTIPLEANSWER>\r
-    <QUESTION_MATCH id='q39-44'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q39-44_a1' position='1'>\r
-            <TEXT><![CDATA[frog]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q39-44_a2' position='2'>\r
-            <TEXT><![CDATA[cat]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q39-44_a3' position='3'>\r
-            <TEXT><![CDATA[newt]]></TEXT>\r
-        </ANSWER>\r
-        <CHOICE id='q39-44_c1' position='1'>\r
-            <TEXT><![CDATA[mammal]]></TEXT>\r
-        </CHOICE>\r
-        <CHOICE id='q39-44_c2' position='2'>\r
-            <TEXT><![CDATA[insect]]></TEXT>\r
-        </CHOICE>\r
-        <CHOICE id='q39-44_c3' position='3'>\r
-            <TEXT><![CDATA[amphibian]]></TEXT>\r
-        </CHOICE>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>\r
-            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>\r
-            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>\r
-        </GRADABLE>\r
-    </QUESTION_MATCH>\r
-    <QUESTION_ESSAY id='q9'>\r
-        <BODY>\r
-            <TEXT><![CDATA[How are you?]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q9_a1'>\r
-            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-        </GRADABLE>\r
-    </QUESTION_ESSAY>\r
-    <QUESTION_FILLINBLANK id='q27'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q27_a1' position='1'>\r
-            <TEXT>frog</TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-        </GRADABLE>\r
-    </QUESTION_FILLINBLANK>\r
-</POOL>\r
+<?xml version='1.0' encoding='utf-8'?>
+<POOL>
+    <TITLE value='exam 3 2008-9'/>
+    <QUESTIONLIST>
+        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
+        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>
+        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>
+        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>
+        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>
+        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>
+    </QUESTIONLIST>
+    <QUESTION_TRUEFALSE id='q1'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q1_a1'>
+            <TEXT>False</TEXT>
+        </ANSWER>
+        <ANSWER id='q1_a2'>
+            <TEXT>True</TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q1_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_TRUEFALSE>
+    <QUESTION_MULTIPLECHOICE id='q7'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q7_a1' position='1'>
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a2' position='2'>
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a3' position='3'>
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q7_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_MULTIPLECHOICE>
+    <QUESTION_MULTIPLEANSWER id='q8'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q8_a1' position='1'>
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a2' position='2'>
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a3' position='3'>
+        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a4' position='4'>
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q8_a1'/>
+            <CORRECTANSWER answer_id='q8_a3'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_MULTIPLEANSWER>
+    <QUESTION_MATCH id='q39-44'>
+        <BODY>
+            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q39-44_a1' position='1'>
+            <TEXT><![CDATA[frog]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a2' position='2'>
+            <TEXT><![CDATA[cat]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a3' position='3'>
+            <TEXT><![CDATA[newt]]></TEXT>
+        </ANSWER>
+        <CHOICE id='q39-44_c1' position='1'>
+            <TEXT><![CDATA[mammal]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c2' position='2'>
+            <TEXT><![CDATA[insect]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c3' position='3'>
+            <TEXT><![CDATA[amphibian]]></TEXT>
+        </CHOICE>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>
+            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>
+            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>
+        </GRADABLE>
+    </QUESTION_MATCH>
+    <QUESTION_ESSAY id='q9'>
+        <BODY>
+            <TEXT><![CDATA[How are you?]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q9_a1'>
+            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_ESSAY>
+    <QUESTION_FILLINBLANK id='q27'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q27_a1' position='1'>
+            <TEXT>frog</TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_FILLINBLANK>
+</POOL>
index 4424f5f..468f00f 100644 (file)
@@ -123,15 +123,16 @@ class qformat_blackboard_six_base extends qformat_based_on_xml {
         $data = array();
         // Step one, find all file refs then add to array.
         preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs.
-
+        $filepaths = array();
         foreach ($out[1] as $path) {
             $fullpath = $this->filebase . '/' . $path;
 
-            if (is_readable($fullpath)) {
+            if (is_readable($fullpath) && !in_array($path, $filepaths)) {
                 $dirpath = dirname($path);
                 $filename = basename($path);
                 $newfilename = $this->store_file_for_text_field($data, $this->filebase, $dirpath, $filename);
                 $text = preg_replace("|$path|", "@@PLUGINFILE@@/" . $newfilename, $text);
+                $filepaths[] = $path;
             }
 
         }
index 93bb583..f4d9183 100644 (file)
-<?xml version='1.0' encoding='utf-8'?>\r
-<POOL>\r
-    <TITLE value='exam 3 2008-9'/>\r
-    <QUESTIONLIST>\r
-        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>\r
-        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>\r
-        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>\r
-        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>\r
-        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>\r
-        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>\r
-    </QUESTIONLIST>\r
-    <QUESTION_TRUEFALSE id='q1'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q1_a1'>\r
-            <TEXT>False</TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q1_a2'>\r
-            <TEXT>True</TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q1_a2'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_TRUEFALSE>\r
-    <QUESTION_MULTIPLECHOICE id='q7'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q7_a1' position='1'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q7_a2' position='2'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q7_a3' position='3'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q7_a2'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_MULTIPLECHOICE>\r
-    <QUESTION_MULTIPLEANSWER id='q8'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q8_a1' position='1'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a2' position='2'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a3' position='3'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q8_a4' position='4'>\r
-        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q8_a1'/>\r
-            <CORRECTANSWER answer_id='q8_a3'/>\r
-            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
-            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
-        </GRADABLE>\r
-    </QUESTION_MULTIPLEANSWER>\r
-    <QUESTION_MATCH id='q39-44'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q39-44_a1' position='1'>\r
-            <TEXT><![CDATA[frog]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q39-44_a2' position='2'>\r
-            <TEXT><![CDATA[cat]]></TEXT>\r
-        </ANSWER>\r
-        <ANSWER id='q39-44_a3' position='3'>\r
-            <TEXT><![CDATA[newt]]></TEXT>\r
-        </ANSWER>\r
-        <CHOICE id='q39-44_c1' position='1'>\r
-            <TEXT><![CDATA[mammal]]></TEXT>\r
-        </CHOICE>\r
-        <CHOICE id='q39-44_c2' position='2'>\r
-            <TEXT><![CDATA[insect]]></TEXT>\r
-        </CHOICE>\r
-        <CHOICE id='q39-44_c3' position='3'>\r
-            <TEXT><![CDATA[amphibian]]></TEXT>\r
-        </CHOICE>\r
-        <GRADABLE>\r
-            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>\r
-            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>\r
-            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>\r
-        </GRADABLE>\r
-    </QUESTION_MATCH>\r
-    <QUESTION_ESSAY id='q9'>\r
-        <BODY>\r
-            <TEXT><![CDATA[How are you?]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q9_a1'>\r
-            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-        </GRADABLE>\r
-    </QUESTION_ESSAY>\r
-    <QUESTION_FILLINBLANK id='q27'>\r
-        <BODY>\r
-            <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>\r
-            <FLAGS>\r
-                <ISHTML value='true'/>\r
-                <ISNEWLINELITERAL value='false'/>\r
-            </FLAGS>\r
-        </BODY>\r
-        <ANSWER id='q27_a1' position='1'>\r
-            <TEXT>frog</TEXT>\r
-        </ANSWER>\r
-        <GRADABLE>\r
-        </GRADABLE>\r
-    </QUESTION_FILLINBLANK>\r
-</POOL>\r
+<?xml version='1.0' encoding='utf-8'?>
+<POOL>
+    <TITLE value='exam 3 2008-9'/>
+    <QUESTIONLIST>
+        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
+        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>
+        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>
+        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>
+        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>
+        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>
+    </QUESTIONLIST>
+    <QUESTION_TRUEFALSE id='q1'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q1_a1'>
+            <TEXT>False</TEXT>
+        </ANSWER>
+        <ANSWER id='q1_a2'>
+            <TEXT>True</TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q1_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_TRUEFALSE>
+    <QUESTION_MULTIPLECHOICE id='q7'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q7_a1' position='1'>
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a2' position='2'>
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a3' position='3'>
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q7_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_MULTIPLECHOICE>
+    <QUESTION_MULTIPLEANSWER id='q8'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q8_a1' position='1'>
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a2' position='2'>
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a3' position='3'>
+        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a4' position='4'>
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q8_a1'/>
+            <CORRECTANSWER answer_id='q8_a3'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_MULTIPLEANSWER>
+    <QUESTION_MATCH id='q39-44'>
+        <BODY>
+            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q39-44_a1' position='1'>
+            <TEXT><![CDATA[frog]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a2' position='2'>
+            <TEXT><![CDATA[cat]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a3' position='3'>
+            <TEXT><![CDATA[newt]]></TEXT>
+        </ANSWER>
+        <CHOICE id='q39-44_c1' position='1'>
+            <TEXT><![CDATA[mammal]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c2' position='2'>
+            <TEXT><![CDATA[insect]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c3' position='3'>
+            <TEXT><![CDATA[amphibian]]></TEXT>
+        </CHOICE>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>
+            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>
+            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>
+        </GRADABLE>
+    </QUESTION_MATCH>
+    <QUESTION_ESSAY id='q9'>
+        <BODY>
+            <TEXT><![CDATA[How are you?]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q9_a1'>
+            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_ESSAY>
+    <QUESTION_FILLINBLANK id='q27'>
+        <BODY>
+            <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q27_a1' position='1'>
+            <TEXT>frog</TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_FILLINBLANK>
+</POOL>
index 2453a30..f027530 100644 (file)
@@ -45,7 +45,7 @@
       <text><![CDATA[<p>Moodle <img src="@@PLUGINFILE@@/logo.jpg" alt="Moodle logo" width="48" height="48" /> is an acronym for <span style="font-style: italic;">Modular Object-Oriented Dynamic Learning Environment</span>.</p>]]></text>
 <file name="logo.jpg" encoding="base64">/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBggGBQkIBwgKCQkKDRYODQwMDRoTFBAWHxwhICocHh4jJzIqIyUvLR4eNzssLy41ODg0JSo4QTAqPDwsNDUBCQoKDQsNGQ4OGTUkHiQ1NTU1NTU1NTU1NTUuNTU1LTU1NTU1NTQsMjU1NTU1NSk0KSw0MjQ2Miw1NDQyNDQ0Nf/AABEIADAAMAMBIgACEQEDEQH/xAAbAAEAAgIDAAAAAAAAAAAAAAAHAAYDBQEECP/EADUQAAEDAwEEBgcJAAAAAAAAAAECAwQABREGEiExURNBYXORsQcUFRZDYqEXIyQ2QnGywfD/xAAYAQEAAwEAAAAAAAAAAAAAAAAAAQMEAv/EACIRAAEEAgIBBQAAAAAAAAAAAAMAAQIEETESIeEFUYGhsf/aAAwDAQACEQMRAD8AcalSsT8lmMkKfdbaSeBWoJH1oiy1K0z+sbDGTly7Q+WEuhR8BWW16lt16dUm3vF9KeLiUKCM8tojGaItpUqVKIqRq7Xr9sBj2i3yZEg/FUyoIT2jPH/caHbtepdzubqr3ch06fhLXgp7N/DwFOMwx/f9MZxoEuQUuBRJI3OKGMcOsdtHOsNMRGLxNu7TilqcfUVNuIBAwcYBGKpKcYcc3xlWjFMueDaVEj3Bj2mwOi9ZZ6QBaUfqGeA516GsKbTAtTD7LiSgpGyAN6ezZHDFG1j9GsyVLXd0pSwlP3jTSgOXnXfuM+VDiNR4qg2uSpKARu3msNu7MBIwjHPL9WqtViaDzeWkie9ltSrZcW432qbOK2caUzLZDsdxLrauCknIo6XpCdbrYJ65q3hu20L5E43eNcaZubtq1emEFH1eUkko6goddViuHhYYB2bvWF3OsKQXKF9e6sdzVI+0SGCn8N6go7Xz9Kn+qquskj2fK71f8qTHojTzyHlIBcQMJV1gccfQUa6ptl7lPSYzVv2mluKUle2N4Jzzp6oEheHBs4dcUiRg8uT46V8s35fa7s+VHN8QBNtffN+YpItTTjdjbQUkL6PGDzo6m2q9yrnEDtvKG47ySVBQO4H96i+EhDilBum39KapIxHNnfaQruM6acHyJ8xVAaSB6QIGOSqQbsy+7px1uO30j2wnZRnGSCKoVstt3e1dFly4JZbbyCQoGoshJK8ObN02O/l0CSLVpRd+38L/2Q==</file>    </questiontext>
     <generalfeedback format="html">
-      <text><![CDATA[<p><img src="@@PLUGINFILE@@/infos.jpg" alt="Informations" width="34" height="49" />For further information, see the <a title="Moodle Docs - About Moodle" href="http://docs.moodle.org/en/About_Moodle">documentation about Moodle</a></p>\r
+      <text><![CDATA[<p><img src="@@PLUGINFILE@@/infos.jpg" alt="Informations" width="34" height="49" />For further information, see the <a title="Moodle Docs - About Moodle" href="http://docs.moodle.org/en/About_Moodle">documentation about Moodle</a></p>
 <p>.</p>]]></text>
 <file name="infos.jpg" encoding="base64">/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBggGBQkIBwgKCQkKGRYaDQwMDRodFBAhKhwlIB8lHh4jIjIoIyQvJSQeKy8sLzMtODgsISo9QTs2QTIuMywBCQoKDQsNGQ4OGTUkHiQ1NTU1NTU1NTU1NTU1NTU1Ly82NTU1NDUtLTQ0NTU1MCwsLCw0KTU0LzU2NjMpKTQ2Kf/AABEIADEAIgMBIgACEQEDEQH/xAAaAAEAAwEBAQAAAAAAAAAAAAAABQYHBAID/8QALhAAAQMCBAMHBAMAAAAAAAAAAgABAwQFBhEhMUFRgRITIjJxsfAHYWKRFBVC/8QAGQEAAgMBAAAAAAAAAAAAAAAAAAEDBAUC/8QAIREAAgEDBAMBAAAAAAAAAAAAAAECAxExBBITISIjQQX/2gAMAwEAAhEDEQA/ANxXk5AiHtSGINzJ8lz3GvitlCdTOTCAcVl16xZU3SoMhMoof8iz65fdWaGmlXfWCOpUUMmrDVwGbCM8RE+zMbZuvqsdvzR2WSme3Xf+W8o5n3ZeX11dsn5PyVowJjUrof8AX1xN3w+Qufzbq3TuppHCHJF3RzGqnLa8l6REVMmKV9UpzhsMLA7sJF4v2yySe5OD9mPV+LvwW84osg3+xzUj6G7eF23+cfVmWAXW01Vnrnp62JwJn0LLwn6fNFt/mzi4OH0p6iLvuJ3HZi01EUEQQCTFm0YMLPtyUdg+eePFNI8RF2vFx/F392ZSGN7va7sVva0yHI0Ik0jlG46u7ZZZ77Kx/TPBdQFc1zr4ii7HkAm1bjryd9NOWee7KS/Fo/Zl3FbdV8TV0RFgF0LiuFmoboDjWU4SZ75t78H6rtRCdgISiwdaKCbvYKUBPg4gIu3UWZ1MhGEQMEYsAtswtoy9Im5OXbYrWCIiQwiIgAiIgAiIgD//2Q==</file>    </generalfeedback>
     <defaultgrade>1.0000000</defaultgrade>
index bf658ea..ebf91e9 100644 (file)
@@ -33,7 +33,7 @@ $numcourses = optional_param('numcourses', 20, PARAM_INT);
 
 if (empty($CFG->enablestats)) {
     if (has_capability('moodle/site:config', context_system::instance())) {
-        redirect("$CFG->wwwroot/$CFG->admin/settings.php?section=stats", get_string('mustenablestats', 'admin'), 3);
+        redirect("$CFG->wwwroot/$CFG->admin/search.php?query=enablestats", get_string('mustenablestats', 'admin'), 3);
     } else {
         print_error('statsdisable');
     }
index e952338..5167670 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$ADMIN->add('reports', new admin_externalpage('reportcourseoverview', get_string('pluginname', 'report_courseoverview'), "$CFG->wwwroot/report/courseoverview/index.php",'report/courseoverview:view'));
+$ADMIN->add('reports', new admin_externalpage('reportcourseoverview', get_string('pluginname', 'report_courseoverview'), "$CFG->wwwroot/report/courseoverview/index.php",'report/courseoverview:view', empty($CFG->enablestats)));
 
 // no report settings
 $settings = null;
index 4d5f1ae..df2b2a8 100644 (file)
@@ -223,28 +223,28 @@ function report_log_print_mnet_selector_form($hostid, $course, $selecteduser=0,
     $activities = array();
     $selectedactivity = "";
 
-/// Casting $course->modinfo to string prevents one notice when the field is null
-    if ($modinfo = unserialize((string)$course->modinfo)) {
+    $modinfo = get_fast_modinfo($course);
+    if (!empty($modinfo->cms)) {
         $section = 0;
-        foreach ($modinfo as $mod) {
-            if ($mod->mod == "label") {
+        foreach ($modinfo->cms as $cm) {
+            if (!$cm->uservisible || !$cm->has_view()) {
                 continue;
             }
-            if ($mod->section > 0 and $section <> $mod->section) {
-                $activities["section/$mod->section"] = '--- '.get_section_name($course, $mod->section).' ---';
+            if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) {
+                $activities["section/$cm->sectionnum"] = '--- '.get_section_name($course, $cm->sectionnum).' ---';
             }
-            $section = $mod->section;
-            $mod->name = strip_tags(format_string($mod->name, true));
-            if (textlib::strlen($mod->name) > 55) {
-                $mod->name = textlib::substr($mod->name, 0, 50)."...";
+            $section = $cm->sectionnum;
+            $modname = strip_tags($cm->get_formatted_name());
+            if (textlib::strlen($modname) > 55) {
+                $modname = textlib::substr($modname, 0, 50)."...";
             }
-            if (!$mod->visible) {
-                $mod->name = "(".$mod->name.")";
+            if (!$cm->visible) {
+                $modname = "(".$modname.")";
             }
-            $activities["$mod->cm"] = $mod->name;
+            $activities["$cm->id"] = $modname;
 
-            if ($mod->cm == $modid) {
-                $selectedactivity = "$mod->cm";
+            if ($cm->id == $modid) {
+                $selectedactivity = "$cm->id";
             }
         }
     }
@@ -475,28 +475,28 @@ function report_log_print_selector_form($course, $selecteduser=0, $selecteddate=
     $activities = array();
     $selectedactivity = "";
 
-/// Casting $course->modinfo to string prevents one notice when the field is null
-    if ($modinfo = unserialize((string)$course->modinfo)) {
+    $modinfo = get_fast_modinfo($course);
+    if (!empty($modinfo->cms)) {
         $section = 0;
-        foreach ($modinfo as $mod) {
-            if ($mod->mod == "label") {
+        foreach ($modinfo->cms as $cm) {
+            if (!$cm->uservisible || !$cm->has_view()) {
                 continue;
             }
-            if ($mod->section > 0 and $section <> $mod->section) {
-                $activities["section/$mod->section"] = '--- '.get_section_name($course, $mod->section).' ---';
+            if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) {
+                $activities["section/$cm->sectionnum"] = '--- '.get_section_name($course, $cm->sectionnum).' ---';
             }
-            $section = $mod->section;
-            $mod->name = strip_tags(format_string($mod->name, true));
-            if (textlib::strlen($mod->name) > 55) {
-                $mod->name = textlib::substr($mod->name, 0, 50)."...";
+            $section = $cm->sectionnum;
+            $modname = strip_tags($cm->get_formatted_name());
+            if (textlib::strlen($modname) > 55) {
+                $modname = textlib::substr($modname, 0, 50)."...";
             }
-            if (!$mod->visible) {
-                $mod->name = "(".$mod->name.")";
+            if (!$cm->visible) {
+                $modname = "(".$modname.")";
             }
-            $activities["$mod->cm"] = $mod->name;
+            $activities["$cm->id"] = $modname;
 
-            if ($mod->cm == $modid) {
-                $selectedactivity = "$mod->cm";
+            if ($cm->id == $modid) {
+                $selectedactivity = "$cm->id";
             }
         }
     }
index bde390d..97c2733 100644 (file)
@@ -24,7 +24,7 @@
  */
 
 require_once $CFG->dirroot.'/tag/lib.php';
-require_once $CFG->dirroot.'/tag/locallib.php';\r
+require_once $CFG->dirroot.'/tag/locallib.php';
 
 /**
  * Returns an ordered array of tags associated with visible courses
index ee78aa3..4f0c539 100644 (file)
@@ -72,7 +72,7 @@ a.dimmed_text:visited,
 .notifysuccess {color:#006600;}
 .reportlink {text-align:right;}
 a.autolink.glossary:hover {cursor: help;}
-/* Block which is hidden if javascript enabled, prevents fickering visible when JS from footer used! */
+/* Block which is hidden if javascript enabled, prevents flickering, visible when JS from footer used! */
 .collapsibleregioncaption {white-space: nowrap;}
 .collapsibleregioncaption img {vertical-align: middle;}
 .jsenabled .hiddenifjs {display: none;}
@@ -206,10 +206,15 @@ a.skip:active {position: static;display: block;}
 #page-footer .validators ul li {display:inline;margin-right:10px;margin-left:10px;}
 
 .performanceinfo .cachesused {margin-top:1em;}
-.performanceinfo .cachesused .cache-stats-heading {font-weight: bold;text-decoration: underline;font-size:110%;}
-.performanceinfo .cachesused .cache-definition-stats {font-weight:bold;margin-top:0.3em;}
+.performanceinfo .cachesused .cache-stats-heading {font-weight: bold;}
+.performanceinfo .cachesused .cache-definition-stats {margin:0.3em; padding:0px;border:1px solid #999;float:left;min-height:4em;}
+.performanceinfo .cachesused .cache-definition-stats span {padding-left:0.5em;padding-right:0.5em}
+.performanceinfo .cachesused .cache-definition-stats .cache-definition-stats-heading {background-color:#eee;}
 .performanceinfo .cachesused .cache-store-stats {text-indent: 1em;}
-.performanceinfo .cachesused .cache-total-stats {font-weight:bold;margin-top:0.3em;}
+.performanceinfo .cachesused .cache-store-stats.nohits {background-color:#ffd3d9;}
+.performanceinfo .cachesused .cache-store-stats.lowhits {background-color:#f3f2aa;}
+.performanceinfo .cachesused .cache-store-stats.hihits {background-color:#e7f1c3;}
+.performanceinfo .cachesused .cache-total-stats {clear:both;font-weight:bold;margin-top:0.3em;}
 
 #course-footer {clear:both;}
 
index 3ffcdb0..7bc342e 100644 (file)
@@ -51,7 +51,7 @@ class core_user_external extends external_api {
                 'users' => new external_multiple_structure(
                     new external_single_structure(
                         array(
-                            'username'    => new external_value(PARAM_USERNAME, 'Username policy is defined in Moodle security config. Must be lowercase.'),
+                            'username'    => new external_value(PARAM_USERNAME, 'Username policy is defined in Moodle security config.'),
                             'password'    => new external_value(PARAM_RAW, 'Plain text password consisting of any characters'),
                             'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user'),
                             'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user'),
@@ -276,7 +276,7 @@ class core_user_external extends external_api {
                     new external_single_structure(
                         array(
                             'id'    => new external_value(PARAM_INT, 'ID of the user'),
-                            'username'    => new external_value(PARAM_USERNAME, 'Username policy is defined in Moodle security config. Must be lowercase.', VALUE_OPTIONAL, '',NULL_NOT_ALLOWED),
+                            'username'    => new external_value(PARAM_USERNAME, 'Username policy is defined in Moodle security config.', VALUE_OPTIONAL, '',NULL_NOT_ALLOWED),
                             'password'    => new external_value(PARAM_RAW, 'Plain text password consisting of any characters', VALUE_OPTIONAL, '',NULL_NOT_ALLOWED),
                             'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL, '',NULL_NOT_ALLOWED),
                             'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
@@ -412,7 +412,7 @@ class core_user_external extends external_api {
                 $paramtype = PARAM_RAW;
                 break;
             case 'username':
-                $paramtype = PARAM_USERNAME;
+                $paramtype = PARAM_RAW;
                 break;
             case 'email':
                 $paramtype = PARAM_EMAIL;
@@ -438,7 +438,6 @@ class core_user_external extends external_api {
         // Finally retrieve each users information
         $returnedusers = array();
         foreach ($users as $user) {
-
             $userdetails = user_get_user_details_courses($user);
 
             // Return the user only if the searched field is returned
@@ -458,63 +457,184 @@ class core_user_external extends external_api {
      * @since Moodle 2.4
      */
     public static function get_users_by_field_returns() {
-        return new external_multiple_structure(
-            new external_single_structure(
-                array(
-                    'id'    => new external_value(PARAM_INT, 'ID of the user'),
-                    'username'    => new external_value(PARAM_USERNAME, 'Username policy is defined in Moodle security config', VALUE_OPTIONAL),
-                    'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
-                    'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
-                    'fullname'    => new external_value(PARAM_NOTAGS, 'The fullname of the user'),
-                    'email'       => new external_value(PARAM_EMAIL, 'An email address', VALUE_OPTIONAL),
-                    'address'     => new external_value(PARAM_TEXT, 'Postal address', VALUE_OPTIONAL),
-                    'phone1'      => new external_value(PARAM_NOTAGS, 'Phone 1', VALUE_OPTIONAL),
-                    'phone2'      => new external_value(PARAM_NOTAGS, 'Phone 2', VALUE_OPTIONAL),
-                    'icq'         => new external_value(PARAM_NOTAGS, 'icq number', VALUE_OPTIONAL),
-                    'skype'       => new external_value(PARAM_NOTAGS, 'skype id', VALUE_OPTIONAL),
-                    'yahoo'       => new external_value(PARAM_NOTAGS, 'yahoo id', VALUE_OPTIONAL),
-                    'aim'         => new external_value(PARAM_NOTAGS, 'aim id', VALUE_OPTIONAL),
-                    'msn'         => new external_value(PARAM_NOTAGS, 'msn number', VALUE_OPTIONAL),
-                    'department'  => new external_value(PARAM_TEXT, 'department', VALUE_OPTIONAL),
-                    'institution' => new external_value(PARAM_TEXT, 'institution', VALUE_OPTIONAL),
-                    'idnumber'    => new external_value(PARAM_RAW, 'An arbitrary ID code number perhaps from the institution', VALUE_OPTIONAL),
-                    'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
-                    'firstaccess' => new external_value(PARAM_INT, 'first access to the site (0 if never)', VALUE_OPTIONAL),
-                    'lastaccess'  => new external_value(PARAM_INT, 'last access to the site (0 if never)', VALUE_OPTIONAL),
-                    'auth'        => new external_value(PARAM_PLUGIN, 'Auth plugins include manual, ldap, imap, etc', VALUE_OPTIONAL),
-                    'confirmed'   => new external_value(PARAM_INT, 'Active user: 1 if confirmed, 0 otherwise', VALUE_OPTIONAL),
-                    'lang'        => new external_value(PARAM_SAFEDIR, 'Language code such as "en", must exist on server', VALUE_OPTIONAL),
-                    'theme'       => new external_value(PARAM_PLUGIN, 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL),
-                    'timezone'    => new external_value(PARAM_TIMEZONE, 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL),
-                    'mailformat'  => new external_value(PARAM_INT, 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL),
-                    'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
-                    'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
-                    'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
-                    'url'         => new external_value(PARAM_URL, 'URL of the user', VALUE_OPTIONAL),
-                    'country'     => new external_value(PARAM_ALPHA, 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL),
-                    'profileimageurlsmall' => new external_value(PARAM_URL, 'User image profile URL - small version'),
-                    'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version'),
-                    'customfields' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'type'  => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'),
-                                'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
-                                'name' => new external_value(PARAM_RAW, 'The name of the custom field'),
-                                'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'),
-                            )
-                        ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL),
-                    'preferences' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'name'  => new external_value(PARAM_ALPHANUMEXT, 'The name of the preferences'),
-                                'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
-                            )
-                    ), 'User preferences', VALUE_OPTIONAL)
+        return new external_multiple_structure(self::user_description());
+    }
+
+
+    /**
+     * Returns description of get_users() parameters.
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function get_users_parameters() {
+        return new external_function_parameters(
+            array(
+                'criteria' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'key' => new external_value(PARAM_ALPHA, 'the user column to search, expected keys (value format) are:
+                                "id" (int) matching user id,
+                                "lastname" (string) user last name (Note: you can use % for searching but it may be considerably slower!),
+                                "firstname" (string) user first name (Note: you can use % for searching but it may be considerably slower!),
+                                "idnumber" (string) matching user idnumber,
+                                "username" (string) matching user username,
+                                "email" (string) user email (Note: you can use % for searching but it may be considerably slower!),
+                                "auth" (string) matching user auth plugin'),
+                            'value' => new external_value(PARAM_RAW, 'the value to search')
+                        )
+                    ), 'the key/value pairs to be considered in user search. Values can not be empty.
+                        Specify different keys only once (fullname => \'user1\', auth => \'manual\', ...) -
+                        key occurences are forbidden.
+                        The search is executed with AND operator on the criterias. Invalid criterias (keys) are ignored,
+                        the search is still executed on the valid criterias.
+                        You can search without criteria, but the function is not designed for it.
+                        It could very slow or timeout. The function is designed to search some specific users.'
                 )
             )
         );
     }
 
+    /**
+     * Retrieve matching user.
+     *
+     * @param array $criteria the allowed array keys are id/lastname/firstname/idnumber/username/email/auth.
+     * @return array An array of arrays containing user profiles.
+     * @since Moodle 2.5
+     */
+    public static function get_users($criteria = array()) {
+        global $CFG, $USER, $DB;
+
+        require_once($CFG->dirroot . "/user/lib.php");
+
+        $params = self::validate_parameters(self::get_users_parameters(),
+                array('criteria' => $criteria));
+
+        // Validate the criteria and retrieve the users.
+        $users = array();
+        $warnings = array();
+        $sqlparams = array();
+        $usedkeys = array();
+
+        // Do not retrieve deleted users.
+        $sql = ' deleted = 0';
+
+        foreach ($params['criteria'] as $criteriaindex => $criteria) {
+
+            // Check that the criteria has never been used.
+            if (array_key_exists($criteria['key'], $usedkeys)) {
+                throw new moodle_exception('keyalreadyset', '', '', null, 'The key ' . $criteria['key'] . ' can only be sent once');
+            } else {
+                $usedkeys[$criteria['key']] = true;
+            }
+
+            $invalidcriteria = false;
+            // Clean the parameters.
+            $paramtype = PARAM_RAW;
+            switch ($criteria['key']) {
+                case 'id':
+                    $paramtype = PARAM_INT;
+                    break;
+                case 'idnumber':
+                    $paramtype = PARAM_RAW;
+                    break;
+                case 'username':
+                    $paramtype = PARAM_RAW;
+                    break;
+                case 'email':
+                    // We use PARAM_RAW to allow searches with %.
+                    $paramtype = PARAM_RAW;
+                    break;
+                case 'auth':
+                    $paramtype = PARAM_AUTH;
+                    break;
+                case 'lastname':
+                case 'firstname':
+                    $paramtype = PARAM_TEXT;
+                    break;
+                default:
+                    // Send back a warning that this search key is not supported in this version.
+                    // This warning will make the function extandable without breaking clients.
+                    $warnings[] = array(
+                        'item' => $criteria['key'],
+                        'warningcode' => 'invalidfieldparameter',
+                        'message' => 'The search key \'' . $criteria['key'] . '\' is not supported, look at the web service documentation'
+                    );
+                    // Do not add this invalid criteria to the created SQL request.
+                    $invalidcriteria = true;
+                    unset($params['criteria'][$criteriaindex]);
+                    break;
+            }
+
+            if (!$invalidcriteria) {
+                $cleanedvalue = clean_param($criteria['value'], $paramtype);
+
+                $sql .= ' AND ';
+
+                // Create the SQL.
+                switch ($criteria['key']) {
+                    case 'id':
+                    case 'idnumber':
+                    case 'username':
+                    case 'auth':
+                        $sql .= $criteria['key'] . ' = :' . $criteria['key'];
+                        $sqlparams[$criteria['key']] = $cleanedvalue;
+                        break;
+                    case 'email':
+                    case 'lastname':
+                    case 'firstname':
+                        $sql .= $DB->sql_like($criteria['key'], ':' . $criteria['key'], false);
+                        $sqlparams[$criteria['key']] = $cleanedvalue;
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        $users = $DB->get_records_select('user', $sql, $sqlparams, 'id ASC');
+
+        // Finally retrieve each users information.
+        $returnedusers = array();
+        foreach ($users as $user) {
+            $userdetails = user_get_user_details_courses($user);
+
+            // Return the user only if all the searched fields are returned.
+            // Otherwise it means that the $USER was not allowed to search the returned user.
+            if (!empty($userdetails)) {
+                $validuser = true;
+
+                foreach($params['criteria'] as $criteria) {
+                    if (empty($userdetails[$criteria['key']])) {
+                        $validuser = false;
+                    }
+                }
+
+                if ($validuser) {
+                    $returnedusers[] = $userdetails;
+                }
+            }
+        }
+
+        return array('users' => $returnedusers, 'warnings' => $warnings);
+    }
+
+    /**
+     * Returns description of get_users result value.
+     *
+     * @return external_description
+     * @since Moodle 2.5
+     */
+    public static function get_users_returns() {
+        return new external_single_structure(
+            array('users' => new external_multiple_structure(
+                                self::user_description()
+                             ),
+                  'warnings' => new external_warnings('always set to \'key\'', 'faulty key name')
+            )
+        );
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -590,70 +710,18 @@ class core_user_external extends external_api {
      * @since Moodle 2.2
      */
     public static function get_users_by_id_returns() {
-        return new external_multiple_structure(
+        $additionalfields = array (
+            'enrolledcourses' => new external_multiple_structure(
             new external_single_structure(
                 array(
-                    'id'    => new external_value(PARAM_INT, 'ID of the user'),
-                    'username'    => new external_value(PARAM_RAW, 'Username policy is defined in Moodle security config', VALUE_OPTIONAL),
-                    'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
-                    'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
-                    'fullname'    => new external_value(PARAM_NOTAGS, 'The fullname of the user'),
-                    'email'       => new external_value(PARAM_TEXT, 'An email address - allow email as root@localhost', VALUE_OPTIONAL),
-                    'address'     => new external_value(PARAM_TEXT, 'Postal address', VALUE_OPTIONAL),
-                    'phone1'      => new external_value(PARAM_NOTAGS, 'Phone 1', VALUE_OPTIONAL),
-                    'phone2'      => new external_value(PARAM_NOTAGS, 'Phone 2', VALUE_OPTIONAL),
-                    'icq'         => new external_value(PARAM_NOTAGS, 'icq number', VALUE_OPTIONAL),
-                    'skype'       => new external_value(PARAM_NOTAGS, 'skype id', VALUE_OPTIONAL),
-                    'yahoo'       => new external_value(PARAM_NOTAGS, 'yahoo id', VALUE_OPTIONAL),
-                    'aim'         => new external_value(PARAM_NOTAGS, 'aim id', VALUE_OPTIONAL),
-                    'msn'         => new external_value(PARAM_NOTAGS, 'msn number', VALUE_OPTIONAL),
-                    'department'  => new external_value(PARAM_TEXT, 'department', VALUE_OPTIONAL),
-                    'institution' => new external_value(PARAM_TEXT, 'institution', VALUE_OPTIONAL),
-                    'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
-                    'firstaccess' => new external_value(PARAM_INT, 'first access to the site (0 if never)', VALUE_OPTIONAL),
-                    'lastaccess'  => new external_value(PARAM_INT, 'last access to the site (0 if never)', VALUE_OPTIONAL),
-                    'auth'        => new external_value(PARAM_PLUGIN, 'Auth plugins include manual, ldap, imap, etc', VALUE_OPTIONAL),
-                    'confirmed'   => new external_value(PARAM_INT, 'Active user: 1 if confirmed, 0 otherwise', VALUE_OPTIONAL),
-                    'idnumber'    => new external_value(PARAM_RAW, 'An arbitrary ID code number perhaps from the institution', VALUE_OPTIONAL),
-                    'lang'        => new external_value(PARAM_SAFEDIR, 'Language code such as "en", must exist on server', VALUE_OPTIONAL),
-                    'theme'       => new external_value(PARAM_PLUGIN, 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL),
-                    'timezone'    => new external_value(PARAM_TIMEZONE, 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL),
-                    'mailformat'  => new external_value(PARAM_INT, 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL),
-                    'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
-                    'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
-                    'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
-                    'url'         => new external_value(PARAM_URL, 'URL of the user', VALUE_OPTIONAL),
-                    'country'     => new external_value(PARAM_ALPHA, 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL),
-                    'profileimageurlsmall' => new external_value(PARAM_URL, 'User image profile URL - small version'),
-                    'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version'),
-                    'customfields' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'type'  => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'),
-                                'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
-                                'name' => new external_value(PARAM_RAW, 'The name of the custom field'),
-                                'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'),
-                            )
-                    ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL),
-                    'preferences' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'name'  => new external_value(PARAM_ALPHANUMEXT, 'The name of the preferences'),
-                                'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
-                            )
-                    ), 'User preferences', VALUE_OPTIONAL),
-                    'enrolledcourses' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'id'  => new external_value(PARAM_INT, 'Id of the course'),
-                                'fullname'  => new external_value(PARAM_RAW, 'Fullname of the course'),
-                                'shortname' => new external_value(PARAM_RAW, 'Shortname of the course')
-                            )
-                    ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL)
+                    'id'  => new external_value(PARAM_INT, 'Id of the course'),
+                    'fullname'  => new external_value(PARAM_RAW, 'Fullname of the course'),
+                    'shortname' => new external_value(PARAM_RAW, 'Shortname of the course')
                 )
-            )
-        );
+            ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL));
+        return new external_multiple_structure(self::user_description($additionalfields));
     }
+
     /**
      * Returns description of method parameters
      *
@@ -742,11 +810,48 @@ class core_user_external extends external_api {
      * @since Moodle 2.2
      */
     public static function get_course_user_profiles_returns() {
-        return new external_multiple_structure(
-            new external_single_structure(
-                array(
+        $additionalfields = array(
+                    'groups' => new external_multiple_structure(
+                        new external_single_structure(
+                            array(
+                                'id'  => new external_value(PARAM_INT, 'group id'),
+                                'name' => new external_value(PARAM_RAW, 'group name'),
+                                'description' => new external_value(PARAM_RAW, 'group description'),
+                                'descriptionformat' => new external_format_value('description'),
+                            )
+                        ), 'user groups', VALUE_OPTIONAL),
+                    'roles' => new external_multiple_structure(
+                        new external_single_structure(
+                            array(
+                                'roleid'       => new external_value(PARAM_INT, 'role id'),
+                                'name'         => new external_value(PARAM_RAW, 'role name'),
+                                'shortname'    => new external_value(PARAM_ALPHANUMEXT, 'role shortname'),
+                                'sortorder'    => new external_value(PARAM_INT, 'role sortorder')
+                            )
+                        ), 'user roles', VALUE_OPTIONAL),
+                    'enrolledcourses' => new external_multiple_structure(
+                        new external_single_structure(
+                            array(
+                                'id'  => new external_value(PARAM_INT, 'Id of the course'),
+                                'fullname'  => new external_value(PARAM_RAW, 'Fullname of the course'),
+                                'shortname' => new external_value(PARAM_RAW, 'Shortname of the course')
+                            )
+                        ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL)
+                    );
+
+        return new external_multiple_structure(self::user_description($additionalfields));
+    }
+
+    /**
+     * Create user return value description.
+     *
+     * @param array $additionalfields some additional field
+     * @return single_structure_description
+     */
+    public static function user_description($additionalfields = array()) {
+        $userfields = array(
                     'id'    => new external_value(PARAM_INT, 'ID of the user'),
-                    'username'    => new external_value(PARAM_RAW, 'Username policy is defined in Moodle security config', VALUE_OPTIONAL),
+                    'username'    => new external_value(PARAM_RAW, 'The username', VALUE_OPTIONAL),
                     'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
                     'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
                     'fullname'    => new external_value(PARAM_NOTAGS, 'The fullname of the user'),
@@ -765,6 +870,12 @@ class core_user_external extends external_api {
                     'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
                     'firstaccess' => new external_value(PARAM_INT, 'first access to the site (0 if never)', VALUE_OPTIONAL),
                     'lastaccess'  => new external_value(PARAM_INT, 'last access to the site (0 if never)', VALUE_OPTIONAL),
+                    'auth'        => new external_value(PARAM_PLUGIN, 'Auth plugins include manual, ldap, imap, etc', VALUE_OPTIONAL),
+                    'confirmed'   => new external_value(PARAM_INT, 'Active user: 1 if confirmed, 0 otherwise', VALUE_OPTIONAL),
+                    'lang'        => new external_value(PARAM_SAFEDIR, 'Language code such as "en", must exist on server', VALUE_OPTIONAL),
+                    'theme'       => new external_value(PARAM_PLUGIN, 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL),
+                    'timezone'    => new external_value(PARAM_TIMEZONE, 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL),
+                    'mailformat'  => new external_value(PARAM_INT, 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL),
                     'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
                     'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
                     'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
@@ -780,44 +891,21 @@ class core_user_external extends external_api {
                                 'name' => new external_value(PARAM_RAW, 'The name of the custom field'),
                                 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'),
                             )
-                        ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL),
-                    'groups' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'id'  => new external_value(PARAM_INT, 'group id'),
-                                'name' => new external_value(PARAM_RAW, 'group name'),
-                                'description' => new external_value(PARAM_RAW, 'group description'),
-                                'descriptionformat' => new external_format_value('description'),
-                            )
-                        ), 'user groups', VALUE_OPTIONAL),
-                    'roles' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'roleid'       => new external_value(PARAM_INT, 'role id'),
-                                'name'         => new external_value(PARAM_RAW, 'role name'),
-                                'shortname'    => new external_value(PARAM_ALPHANUMEXT, 'role shortname'),
-                                'sortorder'    => new external_value(PARAM_INT, 'role sortorder')
-                            )
-                        ), 'user roles', VALUE_OPTIONAL),
+                        ), 'User custom fields (also known as user profile fields)', VALUE_OPTIONAL),
                     'preferences' => new external_multiple_structure(
                         new external_single_structure(
                             array(
                                 'name'  => new external_value(PARAM_ALPHANUMEXT, 'The name of the preferences'),
                                 'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
                             )
-                    ), 'User preferences', VALUE_OPTIONAL),
-                    'enrolledcourses' => new external_multiple_structure(
-                        new external_single_structure(
-                            array(
-                                'id'  => new external_value(PARAM_INT, 'Id of the course'),
-                                'fullname' => new external_value(PARAM_RAW, 'Fullname of the course'),
-                                'shortname' => new external_value(PARAM_RAW, 'Shortname of the course')
-                            )
-                    ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL)
-                )
-            )
-        );
+                    ), 'Users preferences', VALUE_OPTIONAL)
+                );
+        if (!empty($additionalfields)) {
+            $userfields = array_merge($userfields, $additionalfields);
+        }
+        return new external_single_structure($userfields);
     }
+
 }
 
  /**
@@ -1085,4 +1173,4 @@ class moodle_user_external extends external_api {
         require_once($CFG->dirroot . '/enrol/externallib.php');
         return core_enrol_external::get_enrolled_users_returns();
     }
-}
\ No newline at end of file
+}
index c582b6d..e761c14 100644 (file)
@@ -33,6 +33,166 @@ require_once($CFG->dirroot . '/user/externallib.php');
 
 class core_user_external_testcase extends externallib_advanced_testcase {
 
+    /**
+     * Test get_users
+     */
+    public function test_get_users() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+
+        $user1 = array(
+            'username' => 'usernametest1',
+            'idnumber' => 'idnumbertest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'email' => 'usertest1@email.com',
+            'address' => '2 Test Street Perth 6000 WA',
+            'phone1' => '01010101010',
+            'phone2' => '02020203',
+            'icq' => 'testuser1',
+            'skype' => 'testuser1',
+            'yahoo' => 'testuser1',
+            'aim' => 'testuser1',
+            'msn' => 'testuser1',
+            'department' => 'Department of user 1',
+            'institution' => 'Institution of user 1',
+            'description' => 'This is a description for user 1',
+            'descriptionformat' => FORMAT_MOODLE,
+            'city' => 'Perth',
+            'url' => 'http://moodle.org',
+            'country' => 'au'
+            );
+
+        $user1 = self::getDataGenerator()->create_user($user1);
+        set_config('usetags', 1);
+        require_once($CFG->dirroot . '/user/editlib.php');
+        require_once($CFG->dirroot . '/tag/lib.php');
+        $user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking');
+        useredit_update_interests($user1, $user1->interests);
+
+        $user2 = self::getDataGenerator()->create_user(
+                array('username' => 'usernametest2', 'idnumber' => 'idnumbertest2'));
+
+        $generatedusers = array();
+        $generatedusers[$user1->id] = $user1;
+        $generatedusers[$user2->id] = $user2;
+
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/user:viewdetails', $context->id);
+
+        // Enrol the users in the course.
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, $roleid);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleid);
+        $this->getDataGenerator()->enrol_user($USER->id, $course->id, $roleid);
+
+        // call as admin and receive all possible fields.
+        $this->setAdminUser();
+
+        $searchparams = array(
+            array('key' => 'invalidkey', 'value' => 'invalidkey'),
+            array('key' => 'email', 'value' => $user1->email),
+            array('key' => 'firstname', 'value' => $user1->firstname));
+
+        // Call the external function.
+        $result = core_user_external::get_users($searchparams);
+
+        // We need to execute the return values cleaning process to simulate the web service server
+        $result = external_api::clean_returnvalue(core_user_external::get_users_returns(), $result);
+
+        // Check we retrieve the good total number of enrolled users + no error on capability.
+        $expectedreturnedusers = 1;
+        $returnedusers = $result['users'];
+        $this->assertEquals($expectedreturnedusers, count($returnedusers));
+
+        foreach($returnedusers as $returneduser) {
+            $generateduser = ($returneduser['id'] == $USER->id) ?
+                                $USER : $generatedusers[$returneduser['id']];
+            $this->assertEquals($generateduser->username, $returneduser['username']);
+            if (!empty($generateduser->idnumber)) {
+                $this->assertEquals($generateduser->idnumber, $returneduser['idnumber']);
+            }
+            $this->assertEquals($generateduser->firstname, $returneduser['firstname']);
+            $this->assertEquals($generateduser->lastname, $returneduser['lastname']);
+            if ($generateduser->email != $USER->email) { // Don't check the tmp modified $USER email.
+                $this->assertEquals($generateduser->email, $returneduser['email']);
+            }
+            if (!empty($generateduser->address)) {
+                $this->assertEquals($generateduser->address, $returneduser['address']);
+            }
+            if (!empty($generateduser->phone1)) {
+                $this->assertEquals($generateduser->phone1, $returneduser['phone1']);
+            }
+            if (!empty($generateduser->phone2)) {
+                $this->assertEquals($generateduser->phone2, $returneduser['phone2']);
+            }
+            if (!empty($generateduser->icq)) {
+                $this->assertEquals($generateduser->icq, $returneduser['icq']);
+            }
+            if (!empty($generateduser->skype)) {
+                $this->assertEquals($generateduser->skype, $returneduser['skype']);
+            }
+            if (!empty($generateduser->yahoo)) {
+                $this->assertEquals($generateduser->yahoo, $returneduser['yahoo']);
+            }
+            if (!empty($generateduser->aim)) {
+                $this->assertEquals($generateduser->aim, $returneduser['aim']);
+            }
+            if (!empty($generateduser->msn)) {
+                $this->assertEquals($generateduser->msn, $returneduser['msn']);
+            }
+            if (!empty($generateduser->department)) {
+                $this->assertEquals($generateduser->department, $returneduser['department']);
+            }
+            if (!empty($generateduser->institution)) {
+                $this->assertEquals($generateduser->institution, $returneduser['institution']);
+            }
+            if (!empty($generateduser->description)) {
+                $this->assertEquals($generateduser->description, $returneduser['description']);
+            }
+            if (!empty($generateduser->descriptionformat)) {
+                $this->assertEquals(FORMAT_HTML, $returneduser['descriptionformat']);
+            }
+            if (!empty($generateduser->city)) {
+                $this->assertEquals($generateduser->city, $returneduser['city']);
+            }
+            if (!empty($generateduser->country)) {
+                $this->assertEquals($generateduser->country, $returneduser['country']);
+            }
+            if (!empty($generateduser->url)) {
+                $this->assertEquals($generateduser->url, $returneduser['url']);
+            }
+            if (!empty($CFG->usetags) and !empty($generateduser->interests)) {
+                $this->assertEquals(implode(', ', $generateduser->interests), $returneduser['interests']);
+            }
+        }
+
+        // Test the invalid key warning.
+        $warnings = $result['warnings'];
+        $this->assertEquals(count($warnings), 1);
+        $warning = array_pop($warnings);
+        $this->assertEquals($warning['item'], 'invalidkey');
+        $this->assertEquals($warning['warningcode'], 'invalidfieldparameter');
+
+        // Test sending twice the same search field.
+        try {
+            $searchparams = array(
+            array('key' => 'firstname', 'value' => 'Canard'),
+            array('key' => 'email', 'value' => $user1->email),
+            array('key' => 'firstname', 'value' => $user1->firstname));
+
+            // Call the external function.
+            $result = core_user_external::get_users($searchparams);
+            $this->fail('Expecting \'keyalreadyset\' moodle_exception to be thrown.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('keyalreadyset', $e->errorcode);
+        } catch (Exception $e) {
+            $this->fail('Expecting \'keyalreadyset\' moodle_exception to be thrown.');
+        }
+    }
+
     /**
      * Test get_users_by_field
      */
index 5ab02ab..9c120f0 100644 (file)
@@ -30,7 +30,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2013021400.00;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2013021900.00;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes