Merge branch 'MDL-36628-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 27 Nov 2012 02:10:04 +0000 (10:10 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 27 Nov 2012 02:10:04 +0000 (10:10 +0800)
83 files changed:
admin/index.php
backup/converter/moodle1/lib.php
backup/converter/moodle1/tests/lib_test.php
cache/README.md
cache/classes/config.php
cache/classes/dummystore.php
cache/classes/interfaces.php
cache/classes/loaders.php
cache/classes/store.php [new file with mode: 0644]
cache/lib.php
cache/locallib.php
cache/stores/file/lib.php
cache/stores/memcache/lib.php
cache/stores/memcached/lib.php
cache/stores/mongodb/lib.php
cache/stores/session/lib.php
cache/stores/static/lib.php
calendar/managesubscriptions_form.php
completion/criteria/completion_criteria_grade.php
course/category.php
course/format/topics/lib.php
course/format/weeks/lib.php
course/search.php
course/yui/toolboxes/toolboxes.js
files/renderer.php
install/lang/es_mx/install.php
lib/adminlib.php
lib/formslib.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputrenderers.php
lib/sessionlib.php
lib/upgrade.txt
lib/yui/formautosubmit/formautosubmit.js [new file with mode: 0644]
mod/assign/feedback/file/locallib.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/settings.php
mod/choice/lib.php
mod/data/lib.php
mod/feedback/analysis_course.php
mod/lesson/report.php
mod/quiz/accessmanager.php
mod/quiz/accessrule/accessrulebase.php
mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php
mod/quiz/accessrule/ipaddress/tests/rule_test.php
mod/quiz/accessrule/numattempts/tests/rule_test.php
mod/quiz/accessrule/openclosedate/rule.php
mod/quiz/accessrule/openclosedate/tests/rule_test.php
mod/quiz/accessrule/password/tests/rule_test.php
mod/quiz/accessrule/safebrowser/tests/rule_test.php
mod/quiz/accessrule/securewindow/tests/rule_test.php
mod/quiz/accessrule/timelimit/rule.php
mod/quiz/accessrule/timelimit/tests/rule_test.php
mod/quiz/accessrule/upgrade.txt
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/cronlib.php
mod/quiz/db/events.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/module.js
mod/quiz/overrideedit.php
mod/quiz/processattempt.php
mod/quiz/renderer.php
mod/quiz/startattempt.php
mod/quiz/tests/attempts_test.php [new file with mode: 0644]
mod/quiz/tests/generator/lib.php
mod/quiz/tests/generator_test.php [new file with mode: 0644]
mod/quiz/version.php
mod/scorm/request.js
mod/survey/download.php
pix/i/dragdrop.svg
pix/y/ln.png [deleted file]
question/type/numerical/edit_numerical_form.php
theme/arialist/style/core.css
theme/base/style/filemanager.css
theme/mymobile/renderers.php
version.php

index 2f88126..bbd528d 100644 (file)
@@ -376,6 +376,10 @@ if (during_initial_install()) {
         }
     }
 
+    // Cleanup SESSION to make sure other code does not complain in the future.
+    unset($SESSION->has_timed_out);
+    unset($SESSION->wantsurl);
+
     // at this stage there can be only one admin unless more were added by install - users may change username, so do not rely on that
     $adminids = explode(',', $CFG->siteadmins);
     $adminuser = get_complete_user_data('id', reset($adminids));
index c3b47c6..4fdf62e 100644 (file)
@@ -642,7 +642,7 @@ class moodle1_converter extends base_converter {
         }
         foreach ($matches[2] as $match) {
             $file = str_replace(array('$@FILEPHP@$', '$@SLASH@$', '$@FORCEDOWNLOAD@$'), array('', '/', ''), $match);
-            $files[] = urldecode($file);
+            $files[] = rawurldecode($file);
         }
 
         return array_unique($files);
@@ -659,9 +659,16 @@ class moodle1_converter extends base_converter {
     public static function rewrite_filephp_usage($text, array $files) {
 
         foreach ($files as $file) {
+            // Expect URLs properly encoded by default.
+            $parts   = explode('/', $file);
+            $encoded = implode('/', array_map('rawurlencode', $parts));
+            $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $encoded);
+            $text    = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
+            $text    = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
+            // Add support for URLs without any encoding.
             $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $file);
-            $text    = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$file.'?forcedownload=1', $text);
-            $text    = str_replace($fileref, '@@PLUGINFILE@@'.$file, $text);
+            $text    = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
+            $text    = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
         }
 
         return $text;
index 20aca62..bf4935c 100644 (file)
@@ -443,23 +443,37 @@ as it is parsed from the backup file. <br /><br /><img border="0" width="110" vs
         $this->assertTrue(in_array('/pics/news.gif', $files));
         $this->assertTrue(in_array('/MANUAL.DOC', $files));
 
-        $text = moodle1_converter::rewrite_filephp_usage($text, array('/pics/news.gif', '/another/file/notused.txt'), $files);
+        $text = moodle1_converter::rewrite_filephp_usage($text, array('/pics/news.gif', '/another/file/notused.txt'));
         $this->assertEquals($text, 'This is a text containing links to file.php
 as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="@@PLUGINFILE@@/pics/news.gif" /><a href="@@PLUGINFILE@@/pics/news.gif?forcedownload=1">download image</a><br />
     <br /><a href=\'$@FILEPHP@$$@SLASH@$MANUAL.DOC$@FORCEDOWNLOAD@$\'>download manual</a><br />');
     }
 
     public function test_referenced_files_urlencoded() {
-        // This test covers MDL-36204
+
         $text = 'This is a text containing links to file.php
 as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif" /><a href="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif$@FORCEDOWNLOAD@$">no space</a><br />
-    <br /><a href=\'$@FILEPHP@$$@SLASH@$pics$@SLASH@$news%20with%20spaces.gif$@FORCEDOWNLOAD@$\'>with urlencoded spaces</a><br />';
+    <br /><a href=\'$@FILEPHP@$$@SLASH@$pics$@SLASH@$news%20with%20spaces.gif$@FORCEDOWNLOAD@$\'>with urlencoded spaces</a><br />
+<a href="$@FILEPHP@$$@SLASH@$illegal%20pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (space and plus encoded)</a>
+<a href="$@FILEPHP@$$@SLASH@$illegal pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (none encoded)</a>
+<a href="$@FILEPHP@$$@SLASH@$illegal%20pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (only space encoded)</a>
+<a href="$@FILEPHP@$$@SLASH@$illegal pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (only plus)</a>';
 
         $files = moodle1_converter::find_referenced_files($text);
         $this->assertEquals(gettype($files), 'array');
-        $this->assertEquals(2, count($files));
+        $this->assertEquals(3, count($files));
         $this->assertTrue(in_array('/pics/news.gif', $files));
         $this->assertTrue(in_array('/pics/news with spaces.gif', $files));
+        $this->assertTrue(in_array('/illegal pics+movies/romeo+juliet.avi', $files));
+
+        $text = moodle1_converter::rewrite_filephp_usage($text, $files);
+        $this->assertEquals('This is a text containing links to file.php
+as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="@@PLUGINFILE@@/pics/news.gif" /><a href="@@PLUGINFILE@@/pics/news.gif?forcedownload=1">no space</a><br />
+    <br /><a href=\'@@PLUGINFILE@@/pics/news%20with%20spaces.gif?forcedownload=1\'>with urlencoded spaces</a><br />
+<a href="@@PLUGINFILE@@/illegal%20pics%2Bmovies/romeo%2Bjuliet.avi">Download the full AVI for free! (space and plus encoded)</a>
+<a href="@@PLUGINFILE@@/illegal%20pics%2Bmovies/romeo%2Bjuliet.avi">Download the full AVI for free! (none encoded)</a>
+<a href="$@FILEPHP@$$@SLASH@$illegal%20pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (only space encoded)</a>
+<a href="$@FILEPHP@$$@SLASH@$illegal pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (only plus)</a>', $text);
     }
 
     public function test_question_bank_conversion() {
index 408e014..fc8309d 100644 (file)
@@ -89,6 +89,7 @@ The following points highlight things you should know about stores.
 ** Data guarantee - Data is guaranteed to exist in the cache once it is set there. It is never cleaned up to free space or because it has not been recently used.
 ** Multiple identifiers - Rather than a single string key, the parts that make up the key are passed as an array.
 ** Native TTL support - When required, the store supports native ttl and doesn't require the cache API to manage ttl of things given to the store.
+* There are two reserved store names, base and dummy. These are both used internally.
 
 ### Definition
 _Definitions were not a part of the previous proposal._
index da3b966..e6d3416 100644 (file)
@@ -182,7 +182,7 @@ class cache_config {
             if (!class_exists($class)) {
                 continue;
             }
-            if (!array_key_exists('cache_store', class_implements($class))) {
+            if (!array_key_exists('cache_store', class_parents($class))) {
                 continue;
             }
             if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) {
index d44dc8c..9a69d67 100644 (file)
@@ -37,7 +37,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_dummy implements cache_store {
+class cachestore_dummy extends cache_store {
 
     /**
      * The name of this store.
@@ -135,30 +135,6 @@ class cachestore_dummy implements cache_store {
         return true;
     }
 
-    /**
-     * Returns true if this store supports data guarantee.
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return false;
-    }
-
-    /**
-     * Returns true if this store supports multiple identifiers.
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if this store supports a native ttl.
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Returns the data for the given key
      * @param string $key
index 983bc6b..d548349 100644 (file)
@@ -230,227 +230,6 @@ interface cache_loader_with_locking {
     public function release_lock($key);
 }
 
-/**
- * Cache store.
- *
- * This interface outlines the requirements for a cache store plugin.
- * It must be implemented by all such plugins and provides a reference to interacting with cache stores.
- *
- * Must be implemented by all cache store plugins.
- *
- * @package    core
- * @category   cache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-interface cache_store {
-
-    /**#@+
-     * Constants for features a cache store can support
-     */
-    /**
-     * Supports multi-part keys
-     */
-    const SUPPORTS_MULTIPLE_IDENTIFIERS = 1;
-    /**
-     * Ensures data remains in the cache once set.
-     */
-    const SUPPORTS_DATA_GUARANTEE = 2;
-    /**
-     * Supports a native ttl system.
-     */
-    const SUPPORTS_NATIVE_TTL = 4;
-    /**#@-*/
-
-    /**#@+
-     * Constants for the modes of a cache store
-     */
-    /**
-     * Application caches. These are shared caches.
-     */
-    const MODE_APPLICATION = 1;
-    /**
-     * Session caches. Just access to the PHP session.
-     */
-    const MODE_SESSION = 2;
-    /**
-     * Request caches. Static caches really.
-     */
-    const MODE_REQUEST = 4;
-    /**#@-*/
-
-    /**
-     * Static method to check if the store requirements are met.
-     *
-     * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise.
-     */
-    public static function are_requirements_met();
-
-    /**
-     * Static method to check if a store is usable with the given mode.
-     *
-     * @param int $mode One of cache_store::MODE_*
-     */
-    public static function is_supported_mode($mode);
-
-    /**
-     * Returns the supported features as a binary flag.
-     *
-     * @param array $configuration The configuration of a store to consider specifically.
-     * @return int The supported features.
-     */
-    public static function get_supported_features(array $configuration = array());
-
-    /**
-     * Returns the supported modes as a binary flag.
-     *
-     * @param array $configuration The configuration of a store to consider specifically.
-     * @return int The supported modes.
-     */
-    public static function get_supported_modes(array $configuration = array());
-
-    /**
-     * Returns true if this cache store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers();
-
-    /**
-     * Returns true if this cache store instance promotes data guarantee.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee();
-
-    /**
-     * Returns true if this cache store instance supports ttl natively.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl();
-
-    /**
-     * Used to control the ability to add an instance of this store through the admin interfaces.
-     *
-     * @return bool True if the user can add an instance, false otherwise.
-     */
-    public static function can_add_instance();
-
-    /**
-     * Constructs an instance of the cache store.
-     *
-     * This method should not create connections or perform and processing, it should be used
-     *
-     * @param string $name The name of the cache store
-     * @param array $configuration The configuration for this store instance.
-     */
-    public function __construct($name, array $configuration = array());
-
-    /**
-     * Returns the name of this store instance.
-     * @return string
-     */
-    public function my_name();
-
-    /**
-     * Initialises a new instance of the cache store given the definition the instance is to be used for.
-     *
-     * This function should prepare any given connections etc.
-     *
-     * @param cache_definition $definition
-     */
-    public function initialise(cache_definition $definition);
-
-    /**
-     * Returns true if this cache store instance has been initialised.
-     * @return bool
-     */
-    public function is_initialised();
-
-    /**
-     * Returns true if this cache store instance is ready to use.
-     * @return bool
-     */
-    public function is_ready();
-
-    /**
-     * Retrieves an item from the cache store given its key.
-     *
-     * @param string $key The key to retrieve
-     * @return mixed The data that was associated with the key, or false if the key did not exist.
-     */
-    public function get($key);
-
-    /**
-     * Retrieves several items from the cache store in a single transaction.
-     *
-     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
-     *
-     * @param array $keys The array of keys to retrieve
-     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
-     *      be set to false.
-     */
-    public function get_many($keys);
-
-    /**
-     * Sets an item in the cache given its key and data value.
-     *
-     * @param string $key The key to use.
-     * @param mixed $data The data to set.
-     * @return bool True if the operation was a success false otherwise.
-     */
-    public function set($key, $data);
-
-    /**
-     * Sets many items in the cache in a single transaction.
-     *
-     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
-     *      keys, 'key' and 'value'.
-     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
-     *      sent ... if they care that is.
-     */
-    public function set_many(array $keyvaluearray);
-
-    /**
-     * Deletes an item from the cache store.
-     *
-     * @param string $key The key to delete.
-     * @return bool Returns true if the operation was a success, false otherwise.
-     */
-    public function delete($key);
-
-    /**
-     * Deletes several keys from the cache in a single action.
-     *
-     * @param array $keys The keys to delete
-     * @return int The number of items successfully deleted.
-     */
-    public function delete_many(array $keys);
-
-    /**
-     * Purges the cache deleting all items within it.
-     *
-     * @return boolean True on success. False otherwise.
-     */
-    public function purge();
-
-    /**
-     * Performs any necessary clean up when the store instance is being deleted.
-     */
-    public function cleanup();
-
-    /**
-     * Generates an instance of the cache store that can be used for testing.
-     *
-     * Returns an instance of the cache store, or false if one cannot be created.
-     *
-     * @param cache_definition $definition
-     * @return cache_store|false
-     */
-    public static function initialise_test_instance(cache_definition $definition);
-}
-
 /**
  * Cache store feature: locking
  *
@@ -559,6 +338,35 @@ interface cache_is_key_aware {
     public function has_all(array $keys);
 }
 
+/**
+ * Cache store feature: configurable.
+ *
+ * This feature should be implemented by all cache stores that are configurable when adding an instance.
+ * It requires the implementation of methods required to convert form data into the a configuration array for the
+ * store instance, and then the reverse converting configuration data into an array that can be used to set the
+ * data for the edit form.
+ *
+ * Can be implemented by classes already implementing cache_store.
+ */
+interface cache_is_configurable {
+
+    /**
+     * Given the data from the add instance form this function creates a configuration array.
+     *
+     * @param stdClass $data
+     * @return array
+     */
+    public static function config_get_configuration_array($data);
+
+    /**
+     * Allows the cache store to set its data against the edit form before it is shown to the user.
+     *
+     * @param moodleform $editform
+     * @param array $config
+     */
+    public static function config_set_edit_form_data(moodleform $editform, array $config);
+}
+
 /**
  * Cache Data Source.
  *
index 433e9b7..74014c4 100644 (file)
@@ -638,19 +638,22 @@ class cache implements cache_loader {
     public function has($key, $tryloadifpossible = false) {
         $parsedkey = $this->parse_key($key);
         if ($this->is_in_persist_cache($parsedkey)) {
+            // Hoorah, that was easy. It exists in the persist cache so we definitely have it.
             return true;
         }
-        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
-            if ($this->store_supports_key_awareness() && !$this->store->has($parsedkey)) {
-                return false;
-            }
+        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
+            // The data has a TTL and the store doesn't support it natively.
+            // We must fetch the data and expect a ttl wrapper.
             $data = $this->store->get($parsedkey);
-            if (!$this->store_supports_native_ttl()) {
-                $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired());
-            } else {
-                $has = ($data !== false);
-            }
+            $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired());
+        } else if (!$this->store_supports_key_awareness()) {
+            // The store doesn't support key awareness, get the data and check it manually... puke.
+            // Either no TTL is set of the store supports its handling natively.
+            $data = $this->store->get($parsedkey);
+            $has = ($data !== false);
         } else {
+            // The store supports key awareness, this is easy!
+            // Either no TTL is set of the store supports its handling natively.
             $has = $this->store->has($parsedkey);
         }
         if (!$has && $tryloadifpossible) {
diff --git a/cache/classes/store.php b/cache/classes/store.php
new file mode 100644 (file)
index 0000000..75cd8d8
--- /dev/null
@@ -0,0 +1,266 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache store - base class
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are required in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache store interface.
+ *
+ * This interface defines the static methods that must be implemented by every cache store plugin.
+ * To ensure plugins implement this class the abstract cache_store class implements this interface.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface cache_store_interface {
+    /**
+     * Static method to check if the store requirements are met.
+     *
+     * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise.
+     */
+    public static function are_requirements_met();
+
+    /**
+     * Static method to check if a store is usable with the given mode.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     */
+    public static function is_supported_mode($mode);
+
+    /**
+     * Returns the supported features as a binary flag.
+     *
+     * @param array $configuration The configuration of a store to consider specifically.
+     * @return int The supported features.
+     */
+    public static function get_supported_features(array $configuration = array());
+
+    /**
+     * Returns the supported modes as a binary flag.
+     *
+     * @param array $configuration The configuration of a store to consider specifically.
+     * @return int The supported modes.
+     */
+    public static function get_supported_modes(array $configuration = array());
+
+    /**
+     * Generates an instance of the cache store that can be used for testing.
+     *
+     * Returns an instance of the cache store, or false if one cannot be created.
+     *
+     * @param cache_definition $definition
+     * @return cache_store|false
+     */
+    public static function initialise_test_instance(cache_definition $definition);
+}
+
+/**
+ * Abstract cache store class.
+ *
+ * All cache store plugins must extend this base class.
+ * It lays down the foundation for what is required of a cache store plugin.
+ *
+ * @since 2.4
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class cache_store implements cache_store_interface {
+
+    // Constants for features a cache store can support
+
+    /**
+     * Supports multi-part keys
+     */
+    const SUPPORTS_MULTIPLE_IDENTIFIERS = 1;
+    /**
+     * Ensures data remains in the cache once set.
+     */
+    const SUPPORTS_DATA_GUARANTEE = 2;
+    /**
+     * Supports a native ttl system.
+     */
+    const SUPPORTS_NATIVE_TTL = 4;
+
+    // Constants for the modes of a cache store
+
+    /**
+     * Application caches. These are shared caches.
+     */
+    const MODE_APPLICATION = 1;
+    /**
+     * Session caches. Just access to the PHP session.
+     */
+    const MODE_SESSION = 2;
+    /**
+     * Request caches. Static caches really.
+     */
+    const MODE_REQUEST = 4;
+
+    /**
+     * Constructs an instance of the cache store.
+     *
+     * This method should not create connections or perform and processing, it should be used
+     *
+     * @param string $name The name of the cache store
+     * @param array $configuration The configuration for this store instance.
+     */
+    abstract public function __construct($name, array $configuration = array());
+
+    /**
+     * Returns the name of this store instance.
+     * @return string
+     */
+    abstract public function my_name();
+
+    /**
+     * Initialises a new instance of the cache store given the definition the instance is to be used for.
+     *
+     * This function should prepare any given connections etc.
+     *
+     * @param cache_definition $definition
+     */
+    abstract public function initialise(cache_definition $definition);
+
+    /**
+     * Returns true if this cache store instance has been initialised.
+     * @return bool
+     */
+    abstract public function is_initialised();
+
+    /**
+     * Returns true if this cache store instance is ready to use.
+     * @return bool
+     */
+    abstract public function is_ready();
+
+    /**
+     * Retrieves an item from the cache store given its key.
+     *
+     * @param string $key The key to retrieve
+     * @return mixed The data that was associated with the key, or false if the key did not exist.
+     */
+    abstract public function get($key);
+
+    /**
+     * Retrieves several items from the cache store in a single transaction.
+     *
+     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
+     *
+     * @param array $keys The array of keys to retrieve
+     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
+     *      be set to false.
+     */
+    abstract public function get_many($keys);
+
+    /**
+     * Sets an item in the cache given its key and data value.
+     *
+     * @param string $key The key to use.
+     * @param mixed $data The data to set.
+     * @return bool True if the operation was a success false otherwise.
+     */
+    abstract public function set($key, $data);
+
+    /**
+     * Sets many items in the cache in a single transaction.
+     *
+     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
+     *      keys, 'key' and 'value'.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
+     *      sent ... if they care that is.
+     */
+    abstract public function set_many(array $keyvaluearray);
+
+    /**
+     * Deletes an item from the cache store.
+     *
+     * @param string $key The key to delete.
+     * @return bool Returns true if the operation was a success, false otherwise.
+     */
+    abstract public function delete($key);
+
+    /**
+     * Deletes several keys from the cache in a single action.
+     *
+     * @param array $keys The keys to delete
+     * @return int The number of items successfully deleted.
+     */
+    abstract public function delete_many(array $keys);
+
+    /**
+     * Purges the cache deleting all items within it.
+     *
+     * @return boolean True on success. False otherwise.
+     */
+    abstract public function purge();
+
+    /**
+     * Performs any necessary clean up when the store instance is being deleted.
+     */
+    abstract public function cleanup();
+
+    /**
+     * Returns true if the user can add an instance of the store plugin.
+     *
+     * @return bool
+     */
+    public static function can_add_instance() {
+        return true;
+    }
+
+    /**
+     * Returns true if the store instance guarantees data.
+     *
+     * @return bool
+     */
+    public function supports_data_guarantee() {
+        return $this::get_supported_features() & self::SUPPORTS_DATA_GUARANTEE;
+    }
+
+    /**
+     * Returns true if the store instance supports multiple identifiers.
+     *
+     * @return bool
+     */
+    public function supports_multiple_identifiers() {
+        return $this::get_supported_features() & self::SUPPORTS_MULTIPLE_IDENTIFIERS;
+    }
+
+    /**
+     * Returns true if the store instance supports native ttl.
+     *
+     * @return bool
+     */
+    public function supports_native_ttl() {
+        return $this::supports_data_guarantee() & self::SUPPORTS_NATIVE_TTL;
+    }
+}
index b5e23ad..001cd43 100644 (file)
@@ -36,6 +36,7 @@ require_once($CFG->dirroot.'/cache/classes/config.php');
 require_once($CFG->dirroot.'/cache/classes/helper.php');
 require_once($CFG->dirroot.'/cache/classes/factory.php');
 require_once($CFG->dirroot.'/cache/classes/loaders.php');
+require_once($CFG->dirroot.'/cache/classes/store.php');
 require_once($CFG->dirroot.'/cache/classes/definition.php');
 
 /**
index e8e43a3..2b8087c 100644 (file)
@@ -137,7 +137,7 @@ class cache_config_writer extends cache_config {
             }
         }
         $reflection = new ReflectionClass($class);
-        if (!$reflection->implementsInterface('cache_store')) {
+        if (!$reflection->isSubclassOf('cache_store')) {
             throw new cache_exception('Invalid cache plugin specified. The plugin does not extend the required class.');
         }
         if (!$class::are_requirements_met()) {
@@ -790,7 +790,7 @@ abstract class cache_administration_helper extends cache_helper {
         // If it has a customised add instance form then it is going to want to.
         $storeclass = 'cachestore_'.$plugin;
         $storedata = $stores[$store];
-        if (array_key_exists('configuration', $storedata) && method_exists($storeclass, 'config_set_edit_form_data')) {
+        if (array_key_exists('configuration', $storedata) && array_key_exists('cache_is_configurable', class_implements($storeclass))) {
             $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
         }
         return $editform;
@@ -848,12 +848,11 @@ abstract class cache_administration_helper extends cache_helper {
         }
         require_once($file);
         $class = 'cachestore_'.$data->plugin;
-        $method = 'config_get_configuration_array';
         if (!class_exists($class)) {
             throw new coding_exception('Invalid cache plugin provided.');
         }
-        if (method_exists($class, $method)) {
-            return call_user_func(array($class, $method), $data);
+        if (array_key_exists('cache_is_configurable', class_implements($class))) {
+            return $class::config_get_configuration_array($data);
         }
         return array();
     }
index 19fdfb6..1d9bb42 100644 (file)
@@ -37,7 +37,7 @@
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_file implements cache_store, cache_is_key_aware {
+class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable  {
 
     /**
      * The name of the store.
@@ -204,33 +204,6 @@ class cachestore_file implements cache_store, cache_is_key_aware {
         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
     }
 
-    /**
-     * Returns true if the store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance guarantees data.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return true;
-    }
-
-    /**
-     * Returns true if the store instance supports native ttl.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Initialises the cache.
      *
@@ -593,15 +566,6 @@ class cachestore_file implements cache_store, cache_is_key_aware {
         return true;
     }
 
-    /**
-     * Returns true if the user can add an instance of the store plugin.
-     *
-     * @return bool
-     */
-    public static function can_add_instance() {
-        return true;
-    }
-
     /**
      * Performs any necessary clean up when the store instance is being deleted.
      *
index d62fac7..80c5a33 100644 (file)
@@ -37,7 +37,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_memcache implements cache_store {
+class cachestore_memcache extends cache_store implements cache_is_configurable {
 
     /**
      * The name of the store
@@ -173,33 +173,6 @@ class cachestore_memcache implements cache_store {
         return self::SUPPORTS_NATIVE_TTL;
     }
 
-    /**
-     * Returns true if the store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance guarantees data.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance supports native ttl.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Returns the supported modes as a combined int.
      *
@@ -343,15 +316,6 @@ class cachestore_memcache implements cache_store {
         $editform->set_data($data);
     }
 
-    /**
-     * Returns true if the user can add an instance of the store plugin.
-     *
-     * @return bool
-     */
-    public static function can_add_instance() {
-        return true;
-    }
-
     /**
      * Performs any necessary clean up when the store instance is being deleted.
      */
index 419e640..44a11b9 100644 (file)
@@ -43,8 +43,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_memcached implements cache_store {
-
+class cachestore_memcached extends cache_store implements cache_is_configurable {
     /**
      * The name of the store
      * @var store
@@ -199,33 +198,6 @@ class cachestore_memcached implements cache_store {
         return self::SUPPORTS_NATIVE_TTL;
     }
 
-    /**
-     * Returns true if the store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance guarantees data.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance supports native ttl.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Returns the supported modes as a combined int.
      *
@@ -426,15 +398,6 @@ class cachestore_memcached implements cache_store {
         $editform->set_data($data);
     }
 
-    /**
-     * Returns true if the user can add an instance of the store plugin.
-     *
-     * @return bool
-     */
-    public static function can_add_instance() {
-        return true;
-    }
-
     /**
      * Performs any necessary clean up when the store instance is being deleted.
      */
index 1931bdb..4ee7752 100644 (file)
@@ -37,7 +37,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_mongodb implements cache_store {
+class cachestore_mongodb extends cache_store implements cache_is_configurable {
 
     /**
      * The name of the store
@@ -141,14 +141,6 @@ class cachestore_mongodb implements cache_store {
         return class_exists('Mongo');
     }
 
-    /**
-     * Returns true if the user can add an instance of this store.
-     * @return bool
-     */
-    public static function can_add_instance() {
-        return true;
-    }
-
     /**
      * Returns the supported features.
      * @param array $configuration
@@ -218,14 +210,6 @@ class cachestore_mongodb implements cache_store {
         return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
     }
 
-    /**
-     * Returns true if this store guarantees its data is there once set.
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return true;
-    }
-
     /**
      * Returns true if this store is making use of multiple identifiers.
      * @return bool
@@ -234,14 +218,6 @@ class cachestore_mongodb implements cache_store {
         return $this->extendedmode;
     }
 
-    /**
-     * Returns true if this store supports native TTL.
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return false;
-    }
-
     /**
      * Retrieves an item from the cache store given its key.
      *
index ba82c17..2ec9a94 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * The session data store class.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class session_data_store extends cache_store {
+
+    /**
+     * Used for the actual storage.
+     * @var array
+     */
+    private static $sessionstore = null;
+
+    /**
+     * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
+     *
+     * @param string $id
+     * @return array
+     */
+    protected static function &register_store_id($id) {
+        if (is_null(self::$sessionstore)) {
+            global $SESSION;
+            if (!isset($SESSION->cachestore_session)) {
+                $SESSION->cachestore_session = array();
+            }
+            self::$sessionstore =& $SESSION->cachestore_session;
+        }
+        if (!array_key_exists($id, self::$sessionstore)) {
+            self::$sessionstore[$id] = array();
+        }
+        return self::$sessionstore[$id];
+    }
+
+    /**
+     * Flushes the data belong to the given store id.
+     * @param string $id
+     */
+    protected static function flush_store_by_id($id) {
+        unset(self::$sessionstore[$id]);
+        self::$sessionstore[$id] = array();
+    }
+
+    /**
+     * Flushes the store of all data.
+     */
+    protected static function flush_store() {
+        $ids = array_keys(self::$sessionstore);
+        unset(self::$sessionstore);
+        self::$sessionstore = array();
+        foreach ($ids as $id) {
+            self::$sessionstore[$id] = array();
+        }
+    }
+}
+
 /**
  * The Session store class.
  *
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_session extends session_data_store implements cache_store, cache_is_key_aware {
+class cachestore_session extends session_data_store implements cache_is_key_aware {
 
     /**
      * The name of the store
@@ -113,33 +169,6 @@ class cachestore_session extends session_data_store implements cache_store, cach
         return ($mode === self::MODE_SESSION);
     }
 
-    /**
-     * Returns true if the store instance guarantees data.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return true;
-    }
-
-    /**
-     * Returns true if the store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance supports native ttl.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Initialises the cache.
      *
@@ -370,60 +399,4 @@ class cachestore_session extends session_data_store implements cache_store, cach
     public function my_name() {
         return $this->name;
     }
-}
-
-/**
- * The session data store class.
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class session_data_store {
-
-    /**
-     * Used for the actual storage.
-     * @var array
-     */
-    private static $sessionstore = null;
-
-    /**
-     * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
-     *
-     * @param string $id
-     * @return array
-     */
-    protected static function &register_store_id($id) {
-        if (is_null(self::$sessionstore)) {
-            global $SESSION;
-            if (!isset($SESSION->cachestore_session)) {
-                $SESSION->cachestore_session = array();
-            }
-            self::$sessionstore =& $SESSION->cachestore_session;
-        }
-        if (!array_key_exists($id, self::$sessionstore)) {
-            self::$sessionstore[$id] = array();
-        }
-        return self::$sessionstore[$id];
-    }
-
-    /**
-     * Flushes the data belong to the given store id.
-     * @param string $id
-     */
-    protected static function flush_store_by_id($id) {
-        unset(self::$sessionstore[$id]);
-        self::$sessionstore[$id] = array();
-    }
-
-    /**
-     * Flushes the store of all data.
-     */
-    protected static function flush_store() {
-        $ids = array_keys(self::$sessionstore);
-        unset(self::$sessionstore);
-        self::$sessionstore = array();
-        foreach ($ids as $id) {
-            self::$sessionstore[$id] = array();
-        }
-    }
 }
\ No newline at end of file
index 7a7bd5a..dcbdbd7 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * The static data store class
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class static_data_store extends cache_store {
+
+    /**
+     * An array for storage.
+     * @var array
+     */
+    private static $staticstore = array();
+
+    /**
+     * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
+     *
+     * @param string $id
+     * @return array
+     */
+    protected static function &register_store_id($id) {
+        if (!array_key_exists($id, self::$staticstore)) {
+            self::$staticstore[$id] = array();
+        }
+        return self::$staticstore[$id];
+    }
+
+    /**
+     * Flushes the store of all values for belonging to the store with the given id.
+     * @param string $id
+     */
+    protected static function flush_store_by_id($id) {
+        unset(self::$staticstore[$id]);
+        self::$staticstore[$id] = array();
+    }
+
+    /**
+     * Flushes all of the values from all stores.
+     *
+     * @copyright  2012 Sam Hemelryk
+     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+     */
+    protected static function flush_store() {
+        $ids = array_keys(self::$staticstore);
+        unset(self::$staticstore);
+        self::$staticstore = array();
+        foreach ($ids as $id) {
+            self::$staticstore[$id] = array();
+        }
+    }
+}
+
 /**
  * The static store class.
  *
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_static extends static_data_store implements cache_store, cache_is_key_aware {
+class cachestore_static extends static_data_store implements cache_is_key_aware {
 
     /**
      * The name of the store
@@ -113,33 +165,6 @@ class cachestore_static extends static_data_store implements cache_store, cache_
         return ($mode === self::MODE_REQUEST);
     }
 
-    /**
-     * Returns true if the store instance guarantees data.
-     *
-     * @return bool
-     */
-    public function supports_data_guarantee() {
-        return true;
-    }
-
-    /**
-     * Returns true if the store instance supports multiple identifiers.
-     *
-     * @return bool
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns true if the store instance supports native ttl.
-     *
-     * @return bool
-     */
-    public function supports_native_ttl() {
-        return true;
-    }
-
     /**
      * Initialises the cache.
      *
@@ -370,56 +395,4 @@ class cachestore_static extends static_data_store implements cache_store, cache_
     public function my_name() {
         return $this->name;
     }
-}
-
-/**
- * The static data store class
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class static_data_store {
-
-    /**
-     * An array for storage.
-     * @var array
-     */
-    private static $staticstore = array();
-
-    /**
-     * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
-     *
-     * @param string $id
-     * @return array
-     */
-    protected static function &register_store_id($id) {
-        if (!array_key_exists($id, self::$staticstore)) {
-            self::$staticstore[$id] = array();
-        }
-        return self::$staticstore[$id];
-    }
-
-    /**
-     * Flushes the store of all values for belonging to the store with the given id.
-     * @param string $id
-     */
-    protected static function flush_store_by_id($id) {
-        unset(self::$staticstore[$id]);
-        self::$staticstore[$id] = array();
-    }
-
-    /**
-     * Flushes all of the values from all stores.
-     *
-     * @copyright  2012 Sam Hemelryk
-     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-     */
-    protected static function flush_store() {
-        $ids = array_keys(self::$staticstore);
-        unset(self::$staticstore);
-        self::$staticstore = array();
-        foreach ($ids as $id) {
-            self::$staticstore[$id] = array();
-        }
-    }
-}
+}
\ No newline at end of file
index 63921cf..a5ac01f 100644 (file)
@@ -69,7 +69,7 @@ class calendar_addsubscription_form extends moodleform {
         $mform->setType('pollinterval', PARAM_INT);
 
         // Import file
-        $mform->addElement('filepicker', 'importfile', get_string('importfromfile', 'calendar'));
+        $mform->addElement('filepicker', 'importfile', get_string('importfromfile', 'calendar'), null, array('accepted_types' => '.ics'));
 
         // Disable appropriate elements depending on import from value.
         $mform->disabledIf('pollinterval', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
index 56a4d0f..d0f976b 100644 (file)
@@ -45,7 +45,7 @@ class completion_criteria_grade extends completion_criteria {
     /**
      * Finds and returns a data_object instance based on params.
      *
-     * @param array $params associative array varname => value of various 
+     * @param array $params associative array varname => value of various
      * parameters used to fetch data_object
      * @return data_object data_object instance or false if none found.
      */
index 43b1132..33bc709 100644 (file)
@@ -439,8 +439,11 @@ if (!$courses) {
         $movetocategories[$category->id] = get_string('moveselectedcoursesto');
         echo '<tr><td colspan="3" align="right">';
         echo html_writer::label(get_string('moveselectedcoursesto'), 'movetoid', false, array('class' => 'accesshide'));
-        echo html_writer::select($movetocategories, 'moveto', $category->id, null, array('id'=>'movetoid'));
-        $PAGE->requires->js_init_call('M.util.init_select_autosubmit', array('movecourses', 'movetoid', false));
+        echo html_writer::select($movetocategories, 'moveto', $category->id, null, array('id'=>'movetoid', 'class' => 'autosubmit'));
+        $PAGE->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => 'movetoid', 'nothing' => $category->id))
+        );
         echo '<input type="hidden" name="id" value="'.$category->id.'" />';
         echo '</td></tr>';
     }
index 0f783e1..f26cafd 100644 (file)
@@ -213,8 +213,12 @@ class format_topics extends format_base {
         }
         if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) {
             $courseconfig = get_config('moodlecourse');
+            $max = $courseconfig->maxsections;
+            if (!isset($max) || !is_numeric($max)) {
+                $max = 52;
+            }
             $sectionmenu = array();
-            for ($i = 0; $i <= $courseconfig->maxsections; $i++) {
+            for ($i = 0; $i <= $max; $i++) {
                 $sectionmenu[$i] = "$i";
             }
             $courseformatoptionsedit = array(
index ba1f54f..a5c1153 100644 (file)
@@ -219,7 +219,11 @@ class format_weeks extends format_base {
         if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) {
             $courseconfig = get_config('moodlecourse');
             $sectionmenu = array();
-            for ($i = 0; $i <= $courseconfig->maxsections; $i++) {
+            $max = $courseconfig->maxsections;
+            if (!isset($max) || !is_numeric($max)) {
+                $max = 52;
+            }
+            for ($i = 0; $i <= $max; $i++) {
                 $sectionmenu[$i] = "$i";
             }
             $courseformatoptionsedit = array(
index 393ab0b..5f91966 100644 (file)
@@ -378,8 +378,11 @@ if ($courses) {
         echo "<input type=\"button\" onclick=\"checknone()\" value=\"$strdeselectall\" />\n";
         // Select box should only show categories in which user has min capability to move course.
         echo html_writer::label(get_string('moveselectedcoursesto'), 'movetoid', false, array('class' => 'accesshide'));
-        echo html_writer::select($usercatlist, 'moveto', '', array(''=>get_string('moveselectedcoursesto')), array('id'=>'movetoid'));
-        $PAGE->requires->js_init_call('M.util.init_select_autosubmit', array('movecourses', 'movetoid', false));
+        echo html_writer::select($usercatlist, 'moveto', '', array(''=>get_string('moveselectedcoursesto')), array('id'=>'movetoid', 'class' => 'autosubmit'));
+        $PAGE->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => 'movetoid', 'nothing' => false))
+        );
         echo "</td>\n</tr>\n";
         echo "</table>\n</form>";
 
@@ -432,4 +435,4 @@ function print_navigation_bar($totalcount, $page, $perpage, $encodedsearch, $mod
         echo "<a href=\"search.php?search=$encodedsearch".$modulelink."&amp;perpage=".$defaultperpage."\">".get_string("showperpage", "", $defaultperpage)."</a>";
         echo "</p></center>";
     }
-}
\ No newline at end of file
+}
index eca0983..5c8c896 100644 (file)
@@ -479,7 +479,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 .setAttribute('href', newlink)
                 .setAttribute('title', left_string);
             anchor.appendChild(newicon);
-            anchor.on('click', this.move_left, this);
             moveright.insert(anchor, 'before');
         },
         /**
index 8d7c34e..2a5e351 100644 (file)
@@ -220,9 +220,9 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fm-content-wrapper">
             <div class="fp-content"></div>
             <div class="fm-empty-container">
-                <span class="dndupload-message">'.$strdndenabledinbox.'<br/><span class="dndupload-arrow"></span></span>
+                <div class="dndupload-message">'.$strdndenabledinbox.'<br/><div class="dndupload-arrow"></div></div>
             </div>
-            <div class="dndupload-target">'.$strdroptoupload.'<br/><span class="dndupload-arrow"></span></div>
+            <div class="dndupload-target">'.$strdroptoupload.'<br/><div class="dndupload-arrow"></div></div>
             <div class="dndupload-uploadinprogress">'.$icon_progress.'</div>
         </div>
         <div class="filemanager-updating">'.$icon_progress.'</div>
index 7de314e..f129f7e 100644 (file)
@@ -48,12 +48,12 @@ $string['environmenthead'] = 'Comprobando su entorno';
 $string['environmentsub2'] = 'Cada versión de Moodle tiene algún requisito mínimo de la versión de PHP y un número obligatorio de extensiones de PHP. Una comprobación del entorno completo se realiza antes de cada instalación y actualización. Por favor, póngase en contacto con el administrador del servidor si no sabe cómo instalar la nueva versión o habilitar las extensiones PHP.';
 $string['errorsinenvironment'] = '¡La comprobación del entorno falló!';
 $string['installation'] = 'Instalación';
-$string['langdownloaderror'] = 'El idioma "{$a}" no pudo ser instalado. El proceso de instalación continuará en inglés.';
+$string['langdownloaderror'] = 'El idioma "{$a}" no pudo ser instalado. El proceso de instalación continuará en Inglés.';
 $string['memorylimithelp'] = '<p>El límite de memoria PHP en su servidor es actualmente {$a}.</p>
 
 <p>Esto puede ocasionar que Moodle tenga problemas de memoria más adelante, especialmente si usted tiene activados muchos módulos y/o muchos usuarios.</p>
 
-<p>Recomendamos que configure PHP con el límite más alto posible, e.g. 40M.
+<p>Recomendamos que configure PHP con el límite más alto posible, por ejemplo: 40M.
 Hay varias formas de hacer esto:</p>
 <ol>
 <li>Si puede hacerlo, recompile PHP con <i>--enable-memory-limit</i>.
index 9bb8e77..8f185f2 100644 (file)
@@ -3818,7 +3818,7 @@ class admin_settings_num_course_sections extends admin_setting_configselect {
     /** Lazy-load the available choices for the select box */
     public function load_choices() {
         $max = get_config('moodlecourse', 'maxsections');
-        if (empty($max)) {
+        if (!isset($max) || !is_numeric($max)) {
             $max = 52;
         }
         for ($i = 0; $i <= $max; $i++) {
index e5e1830..d64b619 100644 (file)
@@ -1631,7 +1631,7 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                     $value = '';
                     // If we have a default value then export it.
                     if (isset($this->_defaultValues[$varname])) {
-                        $value = array($varname => $this->_defaultValues[$varname]);
+                        $value = $this->prepare_fixed_value($varname, $this->_defaultValues[$varname]);
                     }
                 } else {
                     $value = $this->_elements[$key]->exportValue($this->_submitValues, true);
@@ -1662,6 +1662,29 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         return $unfiltered;
     }
 
+    /**
+     * This is a bit of a hack, and it duplicates the code in
+     * HTML_QuickForm_element::_prepareValue, but I could not think of a way or
+     * reliably calling that code. (Think about date selectors, for example.)
+     * @param string $name the element name.
+     * @param mixed $value the fixed value to set.
+     * @return mixed the appropriate array to add to the $unfiltered array.
+     */
+    protected function prepare_fixed_value($name, $value) {
+        if (null === $value) {
+            return null;
+        } else {
+            if (!strpos($name, '[')) {
+                return array($name => $value);
+            } else {
+                $valueAry = array();
+                $myIndex  = "['" . str_replace(array(']', '['), array('', "']['"), $name) . "']";
+                eval("\$valueAry$myIndex = \$value;");
+                return $valueAry;
+            }
+        }
+    }
+
     /**
      * Adds a validation rule for the given field
      *
index 6044ea1..5591399 100644 (file)
@@ -340,11 +340,16 @@ M.util.init_maximised_embed = function(Y, id) {
         if (Y.Lang.isString(el)) {
             el = Y.one('#' + el);
         }
-        var val = el.getStyle(prop);
-        if (val == 'auto') {
-            val = el.getComputedStyle(prop);
+        // Ensure element exists.
+        if (el) {
+            var val = el.getStyle(prop);
+            if (val == 'auto') {
+                val = el.getComputedStyle(prop);
+            }
+            return parseInt(val);
+        } else {
+            return 0;
         }
-        return parseInt(val);
     };
 
     var resize_object = function() {
@@ -376,8 +381,15 @@ M.util.init_maximised_embed = function(Y, id) {
 
 /**
  * Attach handler to single_select
+ *
+ * This code was deprecated in Moodle 2.4 and will be removed in Moodle 2.6
+ *
+ * Please see lib/yui/formautosubmit/formautosubmit.js for its replacement
  */
 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
+    if (M.cfg.developerdebug) {
+        Y.log("You are using a deprecated function call (M.util.init_select_autosubmit). Please look at rewriting your call to use moodle-core-formautosubmit");
+    }
     Y.use('event-key', function() {
         var select = Y.one('#'+selectid);
         if (select) {
@@ -451,6 +463,9 @@ M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
  * This function has accessability issues and also does not use the formid passed through as a parameter.
  */
 M.util.init_url_select = function(Y, formid, selectid, nothing) {
+    if (M.cfg.developerdebug) {
+        Y.log("You are using a deprecated function call (M.util.init_url_select). Please look at rewriting your call to use moodle-core-formautosubmit");
+    }
     YUI().use('node', function(Y) {
         Y.on('change', function() {
             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
@@ -1891,7 +1906,7 @@ M.util.load_flowplayer = function() {
                                 object.width = width;
                                 object.height = height;
                             }
-                               }
+                        }
                     }
                 });
             }
index 62648b8..9236a11 100644 (file)
@@ -2810,7 +2810,7 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $
     }
 
     // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
-    if (!empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
+    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
         if ($setwantsurltome) {
             $SESSION->wantsurl = qualified_me();
         }
index 55d38f1..616865b 100644 (file)
@@ -1476,6 +1476,11 @@ class core_renderer extends renderer_base {
             $select->attributes['title'] = $select->tooltip;
         }
 
+        $select->attributes['class'] = 'autosubmit';
+        if ($select->class) {
+            $select->attributes['class'] .= ' ' . $select->class;
+        }
+
         if ($select->label) {
             $output .= html_writer::label($select->label, $select->attributes['id'], false, $select->labelattributes);
         }
@@ -1491,7 +1496,10 @@ class core_renderer extends renderer_base {
         $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('class' => 'inline'));
 
         $nothing = empty($select->nothing) ? false : key($select->nothing);
-        $this->page->requires->js_init_call('M.util.init_select_autosubmit', array($select->formid, $select->attributes['id'], $nothing));
+        $this->page->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => $select->attributes['id'], 'nothing' => $nothing))
+        );
 
         // then div wrapper for xhtml strictness
         $output = html_writer::tag('div', $output);
@@ -1557,6 +1565,11 @@ class core_renderer extends renderer_base {
             $output .= html_writer::label($select->label, $select->attributes['id'], false, $select->labelattributes);
         }
 
+        $select->attributes['class'] = 'autosubmit';
+        if ($select->class) {
+            $select->attributes['class'] .= ' ' . $select->class;
+        }
+
         if ($select->helpicon instanceof help_icon) {
             $output .= $this->render($select->helpicon);
         } else if ($select->helpicon instanceof old_help_icon) {
@@ -1614,7 +1627,10 @@ class core_renderer extends renderer_base {
             $go = html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('go')));
             $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('class' => 'inline'));
             $nothing = empty($select->nothing) ? false : key($select->nothing);
-            $output .= $this->page->requires->js_init_call('M.util.init_select_autosubmit', array($select->formid, $select->attributes['id'], $nothing));
+            $this->page->requires->yui_module('moodle-core-formautosubmit',
+                'M.core.init_formautosubmit',
+                array(array('selectid' => $select->attributes['id'], 'nothing' => $nothing))
+            );
         } else {
             $output .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>$select->showbutton));
         }
index aedd344..123c70e 100644 (file)
@@ -41,6 +41,12 @@ function session_get_instance() {
 
     static $session = null;
 
+    if (!defined('NO_MOODLE_COOKIES')) {
+        // Moodle session was not initialised yet in lib/setup.php.
+        $session = new emergency_session();
+        return $session;
+    }
+
     if (is_null($session)) {
         if (empty($CFG->sessiontimeout)) {
             $CFG->sessiontimeout = 7200;
index 3bee4ba..783300b 100644 (file)
@@ -21,6 +21,8 @@ information provided here is intended especially for developers.
 YUI changes:
 * moodle-enrol-notification has been renamed to moodle-core-notification
 * YUI2 code must now use 2in3, see http://yuilibrary.com/yui/docs/yui/yui-yui2.html
+* M.util.init_select_autosubmit() and M.util.init_url_select() have been deprecated. Code using this should be updated
+  to use moodle-core-formautosubmit
 
 Unit testing changes:
 * output debugging() is not sent to standard output any more,
diff --git a/lib/yui/formautosubmit/formautosubmit.js b/lib/yui/formautosubmit/formautosubmit.js
new file mode 100644 (file)
index 0000000..01d6668
--- /dev/null
@@ -0,0 +1,108 @@
+YUI.add('moodle-core-formautosubmit',
+    function(Y) {
+        // The CSS selectors we use
+        var CSS = {
+            AUTOSUBMIT : 'autosubmit'
+        };
+
+        var FORMAUTOSUBMITNAME = 'core-formautosubmit';
+
+        var FORMAUTOSUBMIT = function() {
+            FORMAUTOSUBMIT.superclass.constructor.apply(this, arguments);
+        }
+
+        //  We only want to initialize the module fully once
+        var INITIALIZED = false;
+
+        Y.extend(FORMAUTOSUBMIT, Y.Base, {
+
+            /**
+              * Initialize the module
+              */
+            initializer : function(config) {
+                // We only apply the delegation once
+                if (!INITIALIZED) {
+                    INITIALIZED = true;
+                    var applyto = Y.one('body');
+
+                    // We don't listen for change events by default as using the keyboard triggers these too.
+                    applyto.delegate('key', this.process_changes, 'press:13', 'select.' + CSS.AUTOSUBMIT, this);
+                    applyto.delegate('click', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+
+                    if (Y.UA.os == 'macintosh' && Y.UA.webkit) {
+                        // Macintosh webkit browsers like change events, but non-macintosh webkit browsers don't.
+                        applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+                    }
+                    if (Y.UA.ios) {
+                        // IOS doesn't trigger click events because it's touch-based.
+                        applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+                    }
+                }
+
+                // Assign this select items 'nothing' value and lastindex (current value)
+                var thisselect = Y.one('select#' + this.get('selectid'));
+                thisselect.setData('nothing', this.get('nothing'));
+                thisselect.setData('startindex', thisselect.get('selectedIndex'));
+            },
+
+            /**
+             * Check whether the select element was changed
+             */
+            check_changed : function(e) {
+                var select = e.target.ancestor('select.' + CSS.AUTOSUBMIT, true);
+                if (!select) {
+                    return false;
+                }
+
+                var nothing = select.getData('nothing');
+                var startindex = select.getData('startindex');
+                var currentindex = select.get('selectedIndex');
+
+                var previousindex = select.getAttribute('data-previousindex');
+                select.setAttribute('data-previousindex', currentindex);
+                if (!previousindex) {
+                    previousindex = startindex;
+                }
+
+                // Check whether the field has changed, and is not the 'nothing' value
+                if ((nothing===false || select.get('value') != nothing) && startindex != select.get('selectedIndex') && currentindex != previousindex) {
+                    return select;
+                }
+                return false;
+            },
+
+            /**
+             * Process any changes
+             */
+            process_changes : function(e) {
+                var select = this.check_changed(e);
+                if (select) {
+                    var form = select.ancestor('form', true);
+                    form.submit();
+                }
+            }
+        },
+        {
+            NAME : FORMAUTOSUBMITNAME,
+            ATTRS : {
+                selectid : {
+                    'value' : ''
+                },
+                nothing : {
+                    'value' : ''
+                },
+                ignorechangeevent : {
+                    'value' : false
+                }
+            }
+        });
+
+        M.core = M.core || {};
+        M.core.init_formautosubmit = M.core.init_formautosubmit || function(config) {
+            return new FORMAUTOSUBMIT(config);
+        };
+    },
+    '@VERSION@', {
+        requires : ['base', 'event-key']
+    }
+);
index f921401..6ed295e 100644 (file)
@@ -194,8 +194,12 @@ class assign_feedback_file extends assign_feedback_plugin {
     public function save(stdClass $grade, stdClass $data) {
         $fileoptions = $this->get_file_options();
 
-        $userid = $grade->userid;
-        $elementname = 'files_' . $userid;
+        // The element name may have been for a different user.
+        foreach ($data as $key => $value) {
+            if (strpos($key, 'files_') === 0 && strpos($key, '_filemanager')) {
+                $elementname = substr($key, 0, strpos($key, '_filemanager'));
+            }
+        }
 
         $data = file_postupdate_standard_filemanager($data,
                                                      $elementname,
index d0985e8..91a8957 100644 (file)
@@ -101,6 +101,7 @@ $string['download all submissions'] = 'Download all submissions in a zip file.';
 $string['duedate'] = 'Due date';
 $string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date but any assignments submitted after this date are marked as late. To prevent submissions after a certain date - set the assignment cut off date.';
 $string['duedateno'] = 'No due date';
+$string['submissionempty'] = 'Nothing was submitted';
 $string['duedatereached'] = 'The due date for this assignment has now passed';
 $string['duedatevalidation'] = 'Due date must be after the allow submissions from date.';
 $string['editsubmission'] = 'Edit my submission';
@@ -222,6 +223,8 @@ $string['quickgradingchangessaved'] = 'The grade changes were saved';
 $string['quickgrading_help'] = 'Quick grading allows you to assign grades (and outcomes) directly in the submissions table. Quick grading is not compatible with advanced grading and is not recommended when there are multiple markers.';
 $string['requiresubmissionstatement'] = 'Require that students accept the submission statement';
 $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all assignment submissions for this entire Moodle installation. If this setting is not enabled, then submission statements can be enabled or disabled in the settings for each assignment.';
+$string['requiresubmissionstatementassignment'] = 'Require that students accept the submission statement';
+$string['requiresubmissionstatementassignment_help'] = 'Require that students accept the submission statement for all submissions to this assignment.';
 $string['requireallteammemberssubmit'] = 'Require all group members submit';
 $string['requireallteammemberssubmit_help'] = 'If enabled, all members of the student group must click the submit button for this assignment before the group submission will be considered as submitted. If disabled, the group submission will be considered as submitted as soon as any member of the student group clicks the submit button.';
 $string['recordid'] = 'Identifier';
@@ -270,6 +273,7 @@ $string['submissionsclosed'] = 'Submissions closed';
 $string['submissionsettings'] = 'Submission settings';
 $string['submissionstatement'] = 'Submission statement';
 $string['submissionstatement_help'] = 'Assignment submission confirmation statement';
+$string['submissionstatementdefault'] = 'This assignment is my own work, except where I have acknowledged the use of the works of other people.';
 $string['submissionstatementacceptedlog'] = 'Submission statement accepted by user {$a}';
 $string['submissionstatus_draft'] = 'Draft (not submitted)';
 $string['submissionstatusheading'] = 'Submission status';
index 7a8dcae..fed73c7 100644 (file)
@@ -320,11 +320,12 @@ class assign {
 
         $o = '';
         $mform = null;
+        $notices = array();
 
-        // handle form submissions first
+        // Handle form submissions first.
         if ($action == 'savesubmission') {
             $action = 'editsubmission';
-            if ($this->process_save_submission($mform)) {
+            if ($this->process_save_submission($mform, $notices)) {
                 $action = 'view';
             }
         } else if ($action == 'lock') {
@@ -384,7 +385,7 @@ class assign {
         $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT));
         $this->register_return_link($action, $returnparams);
 
-        // now show the right view page
+        // Now show the right view page.
         if ($action == 'previousgrade') {
             $mform = null;
             $o .= $this->view_single_grade_page($mform, -1);
@@ -401,7 +402,7 @@ class assign {
         } else if ($action == 'viewpluginassignsubmission') {
             $o .= $this->view_plugin_content('assignsubmission');
         } else if ($action == 'editsubmission') {
-            $o .= $this->view_edit_submission_page($mform);
+            $o .= $this->view_edit_submission_page($mform, $notices);
         } else if ($action == 'grading') {
             $o .= $this->view_grading_page();
         } else if ($action == 'downloadall') {
@@ -2400,9 +2401,10 @@ class assign {
      * View edit submissions page.
      *
      * @param moodleform $mform
+     * @param array $notices A list of notices to display at the top of the edit submission form (e.g. from plugins).
      * @return void
      */
-    private function view_edit_submission_page($mform) {
+    private function view_edit_submission_page($mform, $notices) {
         global $CFG;
 
         $o = '';
@@ -2426,6 +2428,10 @@ class assign {
             $mform = new mod_assign_submission_form(null, array($this, $data));
         }
 
+        foreach ($notices as $notice) {
+            $o .= $this->get_renderer()->notification($notice);
+        }
+
         $o .= $this->get_renderer()->render(new assign_form('editsubmissionform',$mform));
 
         $o .= $this->view_footer();
@@ -3699,15 +3705,16 @@ class assign {
      * save assignment submission
      *
      * @param  moodleform $mform
+     * @param  array $notices Any error messages that should be shown to the user at the top of the edit submission form.
      * @return bool
      */
-    private function process_save_submission(&$mform) {
+    private function process_save_submission(&$mform, &$notices) {
         global $USER, $CFG;
 
-        // Include submission form
+        // Include submission form.
         require_once($CFG->dirroot . '/mod/assign/submission_form.php');
 
-        // Need submit permission to submit an assignment
+        // Need submit permission to submit an assignment.
         require_capability('mod/assign:submit', $this->context);
         require_sesskey();
 
@@ -3735,12 +3742,24 @@ class assign {
             }
 
 
+            $allempty = true;
+            $pluginerror = false;
             foreach ($this->submissionplugins as $plugin) {
                 if ($plugin->is_enabled()) {
                     if (!$plugin->save($submission, $data)) {
-                        print_error($plugin->get_error());
+                        $notices[] = $plugin->get_error();
+                        $pluginerror = true;
                     }
+                    if (!$allempty || !$plugin->is_empty($submission)) {
+                        $allempty = false;
+                    }
+                }
+            }
+            if ($pluginerror || $allempty) {
+                if ($allempty) {
+                    $notices[] = get_string('submissionempty', 'mod_assign');
                 }
+                return false;
             }
 
             $this->update_submission($submission, $USER->id, true, $this->get_instance()->teamsubmission);
@@ -4312,9 +4331,11 @@ class assign {
                 foreach ($members as $member) {
                     // User may exist in multple groups (which should put them in the default group).
                     $this->apply_grade_to_user($formdata, $member->id);
+                    $this->process_outcomes($member->id, $formdata);
                 }
             } else {
                 $this->apply_grade_to_user($formdata, $userid);
+                $this->process_outcomes($userid, $formdata);
             }
         } else {
             return false;
index a1f177d..45d6c6d 100644 (file)
@@ -92,10 +92,12 @@ class mod_assign_mod_form extends moodleform_mod {
         $mform->addHelpButton('submissiondrafts', 'submissiondrafts', 'assign');
         $mform->setDefault('submissiondrafts', 0);
         // submission statement
-        if (empty($config->requiresubmissionstatement)) {
+        if (empty($config->submissionstatement)) {
+            $mform->addElement('hidden', 'requiresubmissionstatement', 0);
+        } else if (empty($config->requiresubmissionstatement)) {
             $mform->addElement('selectyesno', 'requiresubmissionstatement', get_string('requiresubmissionstatement', 'assign'));
             $mform->setDefault('requiresubmissionstatement', 0);
-            $mform->addHelpButton('requiresubmissionstatement', 'requiresubmissionstatement', 'assign');
+            $mform->addHelpButton('requiresubmissionstatement', 'requiresubmissionstatementassignment', 'assign');
         } else {
             $mform->addElement('hidden', 'requiresubmissionstatement', 1);
         }
index fd29d76..f5ade68 100644 (file)
@@ -60,7 +60,7 @@ if ($ADMIN->fulltree) {
                        new lang_string('sendsubmissionreceipts_help', 'mod_assign'), 1));
     $settings->add(new admin_setting_configtextarea('assign/submissionstatement',
                        new lang_string('submissionstatement', 'mod_assign'),
-                       new lang_string('submissionstatement_help', 'mod_assign'), ''));
+                       new lang_string('submissionstatement_help', 'mod_assign'), get_string('submissionstatementdefault', 'mod_assign')));
     $settings->add(new admin_setting_configcheckbox('assign/requiresubmissionstatement',
                        new lang_string('requiresubmissionstatement', 'mod_assign'),
                        new lang_string('requiresubmissionstatement_help', 'mod_assign'), 0));
index b10da50..48368cf 100644 (file)
@@ -499,8 +499,11 @@ function prepare_choice_show_results($choice, $course, $cm, $allresponses, $forc
                 echo '<a href="javascript:deselect_all_in(\'DIV\',null,\'tablecontainer\');">'.get_string('deselectall').'</a> ';
                 echo '&nbsp;&nbsp;';
                 echo html_writer::label(get_string('withselected', 'choice'), 'menuaction');
-                echo html_writer::select(array('delete' => get_string('delete')), 'action', '', array(''=>get_string('withselectedusers')), array('id'=>'menuaction'));
-                $PAGE->requires->js_init_call('M.util.init_select_autosubmit', array('attemptsform', 'menuaction', ''));
+                echo html_writer::select(array('delete' => get_string('delete')), 'action', '', array(''=>get_string('withselectedusers')), array('id'=>'menuaction', 'class' => 'autosubmit'));
+                $PAGE->requires->yui_module('moodle-core-formautosubmit',
+                    'M.core.init_formautosubmit',
+                    array(array('selectid' => 'menuaction'))
+                );
                 echo '<noscript id="noscriptmenuaction" style="display:inline">';
                 echo '<div>';
                 echo '<input type="submit" value="'.get_string('go').'" /></div></noscript>';
index 54b28bf..2d120ca 100644 (file)
@@ -2108,17 +2108,25 @@ abstract class data_preset_importer {
      * @param stored_file $fileobj the directory to look in. null if using a conventional directory
      * @param string $dir the directory to look in. null if using the Moodle file storage
      * @param string $filename the name of the file we want
-     * @return string the contents of the file
+     * @return string the contents of the file or null if the file doesn't exist.
      */
     public function data_preset_get_file_contents(&$filestorage, &$fileobj, $dir, $filename) {
         if(empty($filestorage) || empty($fileobj)) {
             if (substr($dir, -1)!='/') {
                 $dir .= '/';
             }
-            return file_get_contents($dir.$filename);
+            if (file_exists($dir.$filename)) {
+                return file_get_contents($dir.$filename);
+            } else {
+                return null;
+            }
         } else {
-            $file = $filestorage->get_file(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $fileobj->get_filepath(), $filename);
-            return $file->get_content();
+            if ($filestorage->file_exists(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $fileobj->get_filepath(), $filename)) {
+                $file = $filestorage->get_file(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $fileobj->get_filepath(), $filename);
+                return $file->get_content();
+            } else {
+                return null;
+            }
         }
 
     }
@@ -2138,7 +2146,8 @@ abstract class data_preset_importer {
             $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA);
 
             //preset name to find will be the final element of the directory
-            $presettofind = end(explode('/',$this->directory));
+            $explodeddirectory = explode('/', $this->directory);
+            $presettofind = end($explodeddirectory);
 
             //now go through the available files available and see if we can find it
             foreach ($files as $file) {
@@ -2219,15 +2228,9 @@ abstract class data_preset_importer {
         $result->settings->rsstitletemplate   = $this->data_preset_get_file_contents($fs, $fileobj,$this->directory,"rsstitletemplate.html");
         $result->settings->csstemplate        = $this->data_preset_get_file_contents($fs, $fileobj,$this->directory,"csstemplate.css");
         $result->settings->jstemplate         = $this->data_preset_get_file_contents($fs, $fileobj,$this->directory,"jstemplate.js");
+        $result->settings->asearchtemplate    = $this->data_preset_get_file_contents($fs, $fileobj,$this->directory,"asearchtemplate.html");
 
-        //optional
-        if (file_exists($this->directory."/asearchtemplate.html")) {
-            $result->settings->asearchtemplate = $this->data_preset_get_file_contents($fs, $fileobj,$this->directory,"asearchtemplate.html");
-        } else {
-            $result->settings->asearchtemplate = NULL;
-        }
         $result->settings->instance = $this->module->id;
-
         return $result;
     }
 
index 5087c94..dcf186f 100644 (file)
@@ -185,10 +185,12 @@ if ($courseitemfilter > 0) {
 
          echo ' '. html_writer::label(get_string('filter_by_course', 'feedback'), 'coursefilterid'). ': ';
          echo html_writer::select($courses, 'coursefilter', $coursefilter,
-                                  null, array('id'=>'coursefilterid'));
+                                  null, array('id'=>'coursefilterid', 'class' => 'autosubmit'));
 
-         $PAGE->requires->js_init_call('M.util.init_select_autosubmit',
-                                        array('analysis-form', 'coursefilterid', false));
+        $PAGE->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => 'coursefilterid', 'nothing' => false))
+        );
     }
     echo '<hr />';
     $itemnr = 0;
index 2d44aff..9929473 100644 (file)
@@ -317,8 +317,11 @@ if ($action === 'delete') {
         $checklinks  = '<a href="javascript: checkall();">'.get_string('selectall').'</a> / ';
         $checklinks .= '<a href="javascript: checknone();">'.get_string('deselectall').'</a>';
         $checklinks .= html_writer::label('action', 'menuaction', false, array('class' => 'accesshide'));
-        $checklinks .= html_writer::select(array('delete' => get_string('deleteselected')), 'action', 0, array(''=>'choosedots'), array('id'=>'actionid'));
-        $PAGE->requires->js_init_call('M.util.init_select_autosubmit', array('theform', 'actionid', ''));
+        $checklinks .= html_writer::select(array('delete' => get_string('deleteselected')), 'action', 0, array(''=>'choosedots'), array('id'=>'actionid', 'class' => 'autosubmit'));
+        $PAGE->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => 'actionid', 'nothing' => false))
+        );
         echo $OUTPUT->box($checklinks, 'center');
         echo '</form>';
     }
index 11efebd..fe0f6cc 100644 (file)
@@ -392,17 +392,35 @@ class quiz_access_manager {
     }
 
     /**
-     * Compute how much time is left before this attempt must be submitted.
+     * Compute when the attempt must be submitted.
+     *
+     * @param object $attempt the data from the relevant quiz_attempts row.
+     * @return int|false the attempt close time.
+     *      False if there is no limit.
+     */
+    public function get_end_time($attempt) {
+        $timeclose = false;
+        foreach ($this->rules as $rule) {
+            $ruletimeclose = $rule->end_time($attempt);
+            if ($ruletimeclose !== false && ($timeclose === false || $ruletimeclose < $timeclose)) {
+                $timeclose = $ruletimeclose;
+            }
+        }
+        return $timeclose;
+    }
+
+    /**
+     * Compute what should be displayed to the user for time remaining in this attempt.
      *
      * @param object $attempt the data from the relevant quiz_attempts row.
      * @param int $timenow the time to consider as 'now'.
      * @return int|false the number of seconds remaining for this attempt.
-     *      False if there is no limit.
+     *      False if no limit should be displayed.
      */
-    public function get_time_left($attempt, $timenow) {
+    public function get_time_left_display($attempt, $timenow) {
         $timeleft = false;
         foreach ($this->rules as $rule) {
-            $ruletimeleft = $rule->time_left($attempt, $timenow);
+            $ruletimeleft = $rule->time_left_display($attempt, $timenow);
             if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) {
                 $timeleft = $ruletimeleft;
             }
index a3d9fa1..4b585cd 100644 (file)
@@ -180,14 +180,28 @@ abstract class quiz_access_rule_base {
 
     /**
      * If, because of this rule, the user has to finish their attempt by a certain time,
-     * you should override this method to return the amount of time left in seconds.
+     * you should override this method to return the attempt end time.
+     * @param object $attempt the current attempt
+     * @return mixed the attempt close time, or false if there is no close time.
+     */
+    public function end_time($attempt) {
+        return false;
+    }
+
+    /**
+     * If the user should be shown a different amount of time than $timenow - $this->end_time(), then
+     * override this method.  This is useful if the time remaining is large enough to be omitted.
      * @param object $attempt the current attempt
      * @param int $timenow the time now. We don't use $this->timenow, so we can
      * give the user a more accurate indication of how much time is left.
-     * @return mixed false if there is no deadline, of the time left in seconds if there is one.
+     * @return mixed the time left in seconds (can be negative) or false if there is no limit.
      */
-    public function time_left($attempt, $timenow) {
-        return false;
+    public function time_left_display($attempt, $timenow) {
+        $endtime = $this->end_time($attempt);
+        if ($endtime === false) {
+            return false;
+        }
+        return $endtime - $timenow;
     }
 
     /**
index 1dcc62f..dc27fca 100644 (file)
@@ -55,7 +55,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->prevent_new_attempt(3, $attempt));
@@ -89,7 +90,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->prevent_new_attempt(5, $attempt));
@@ -128,7 +130,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->prevent_new_attempt(5, $attempt));
@@ -179,7 +182,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $attempt->timefinish = 13000;
         $this->assertEquals($rule->prevent_new_attempt(1, $attempt),
@@ -236,7 +240,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->prevent_new_attempt(5, $attempt));
index 007000a..3f8f520 100644 (file)
@@ -56,7 +56,8 @@ class quizaccess_ipaddress_testcase extends basic_testcase {
             $this->assertFalse($rule->description());
             $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
             $this->assertFalse($rule->is_finished(0, $attempt));
-            $this->assertFalse($rule->time_left($attempt, 1));
+            $this->assertFalse($rule->end_time($attempt));
+            $this->assertFalse($rule->time_left_display($attempt, 0));
         }
 
         $quiz->subnet = '0.0.0.0';
@@ -68,6 +69,7 @@ class quizaccess_ipaddress_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 1));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 }
index e99b219..5ab1985 100644 (file)
@@ -64,6 +64,7 @@ class quizaccess_numattempts_testcase extends basic_testcase {
         $this->assertTrue($rule->is_finished(666, $attempt));
 
         $this->assertFalse($rule->prevent_access());
-        $this->assertFalse($rule->time_left($attempt, 1));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 }
index 2a33a1f..639bb1a 100644 (file)
@@ -93,20 +93,24 @@ class quizaccess_openclosedate extends quiz_access_rule_base {
         return $this->quiz->timeclose && $this->timenow > $this->quiz->timeclose;
     }
 
-    public function time_left($attempt, $timenow) {
+    public function end_time($attempt) {
+        if ($this->quiz->timeclose) {
+            return $this->quiz->timeclose;
+        }
+        return false;
+    }
+
+    public function time_left_display($attempt, $timenow) {
         // If this is a teacher preview after the close date, do not show
         // the time.
         if ($attempt->preview && $timenow > $this->quiz->timeclose) {
             return false;
         }
-
-        // Otherwise, return to the time left until the close date, providing
-        // that is less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
-        if ($this->quiz->timeclose) {
-            $timeleft = $this->quiz->timeclose - $timenow;
-            if ($timeleft < QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
-                return $timeleft;
-            }
+        // Otherwise, return to the time left until the close date, providing that is
+        // less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
+        $endtime = $this->end_time($attempt);
+        if ($endtime !== false && $timenow > $endtime - QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
+            return $endtime - $timenow;
         }
         return false;
     }
index 0dea09c..4d85d33 100644 (file)
@@ -55,15 +55,17 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 10000));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 10000));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $rule = new quizaccess_openclosedate($quizobj, 0);
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 
     public function test_start_date() {
@@ -85,7 +87,8 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
             get_string('notavailable', 'quizaccess_openclosedate'));
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
 
         $rule = new quizaccess_openclosedate($quizobj, 10000);
         $this->assertEquals($rule->description(),
@@ -93,7 +96,8 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 0));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 
     public function test_close_date() {
@@ -114,10 +118,12 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
-        $this->assertEquals($rule->time_left($attempt, 19900), 100);
-        $this->assertEquals($rule->time_left($attempt, 20000), 0);
-        $this->assertEquals($rule->time_left($attempt, 20100), -100);
+
+        $this->assertEquals($rule->end_time($attempt), 20000);
+        $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+        $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+        $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+        $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
 
         $rule = new quizaccess_openclosedate($quizobj, 20001);
         $this->assertEquals($rule->description(),
@@ -126,10 +132,11 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
             get_string('notavailable', 'quizaccess_openclosedate'));
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertTrue($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
-        $this->assertEquals($rule->time_left($attempt, 19900), 100);
-        $this->assertEquals($rule->time_left($attempt, 20000), 0);
-        $this->assertEquals($rule->time_left($attempt, 20100), -100);
+        $this->assertEquals($rule->end_time($attempt), 20000);
+        $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+        $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+        $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+        $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
     }
 
     public function test_both_dates() {
@@ -176,10 +183,11 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertTrue($rule->is_finished(0, $attempt));
 
-        $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
-        $this->assertEquals($rule->time_left($attempt, 19900), 100);
-        $this->assertEquals($rule->time_left($attempt, 20000), 0);
-        $this->assertEquals($rule->time_left($attempt, 20100), -100);
+        $this->assertEquals($rule->end_time($attempt), 20000);
+        $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+        $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+        $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+        $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
     }
 
     public function test_close_date_with_overdue() {
index eac66b7..5fede6f 100644 (file)
@@ -53,6 +53,7 @@ class quizaccess_password_testcase extends basic_testcase {
             get_string('requirepasswordmessage', 'quizaccess_password'));
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 1));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 }
index 6922e99..470ecc9 100644 (file)
@@ -58,6 +58,7 @@ class quizaccess_safebrowser_testcase extends basic_testcase {
             $rule->description());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 1));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 }
index 3bfe25d..4ee3c74 100644 (file)
@@ -54,6 +54,7 @@ class quizaccess_securewindow_testcase extends basic_testcase {
         $this->assertEmpty($rule->description());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
         $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->time_left($attempt, 1));
+        $this->assertFalse($rule->end_time($attempt));
+        $this->assertFalse($rule->time_left_display($attempt, 0));
     }
 }
index eff0dbb..5955840 100644 (file)
@@ -52,7 +52,17 @@ class quizaccess_timelimit extends quiz_access_rule_base {
                 format_time($this->quiz->timelimit));
     }
 
-    public function time_left($attempt, $timenow) {
-        return $attempt->timestart + $this->quiz->timelimit - $timenow;
+    public function end_time($attempt) {
+        return $attempt->timestart + $this->quiz->timelimit;
+    }
+
+    public function time_left_display($attempt, $timenow) {
+        // If this is a teacher preview after the time limit expires, don't show the time_left
+        $endtime = $this->end_time($attempt);
+        if ($attempt->preview && $timenow > $endtime) {
+            return false;
+        }
+        return $endtime - $timenow;
+
     }
 }
index 4175190..65de692 100644 (file)
@@ -51,9 +51,11 @@ class quizaccess_timelimit_testcase extends basic_testcase {
             get_string('quiztimelimit', 'quizaccess_timelimit', format_time(3600)));
 
         $attempt->timestart = 10000;
-        $this->assertEquals($rule->time_left($attempt, 10000), 3600);
-        $this->assertEquals($rule->time_left($attempt, 12000), 1600);
-        $this->assertEquals($rule->time_left($attempt, 14000), -400);
+        $attempt->preview = 0;
+        $this->assertEquals($rule->end_time($attempt), 13600);
+        $this->assertEquals($rule->time_left_display($attempt, 10000), 3600);
+        $this->assertEquals($rule->time_left_display($attempt, 12000), 1600);
+        $this->assertEquals($rule->time_left_display($attempt, 14000), -400);
 
         $this->assertFalse($rule->prevent_access());
         $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
index 12f117b..a8d94ff 100644 (file)
@@ -13,3 +13,7 @@ Overview of this plugin type at http://docs.moodle.org/dev/Quiz_access_rules
 * This plugin type now supports cron in the standard way. If required, Create a
   lib.php file containing
 function quizaccess_mypluginname_cron() {};
+
+=== 2.4 ===
+
+* Replaced time_left() with new time_left_display() and end_time() functions.
\ No newline at end of file
index 4be51af..bf5d542 100644 (file)
@@ -1002,13 +1002,14 @@ class quiz_attempt {
      * @return int|false the number of seconds remaining for this attempt.
      *      False if there is no limit.
      */
-    public function get_time_left($timenow) {
+    public function get_time_left_display($timenow) {
         if ($this->attempt->state != self::IN_PROGRESS) {
             return false;
         }
-        return $this->get_access_manager($timenow)->get_time_left($this->attempt, $timenow);
+        return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow);
     }
 
+
     /**
      * @return int the time when this attempt was submitted. 0 if it has not been
      * submitted yet.
@@ -1269,30 +1270,39 @@ class quiz_attempt {
 
     /**
      * Check this attempt, to see if there are any state transitions that should
-     * happen automatically.
+     * happen automatically.  This function will update the attempt checkstatetime.
      * @param int $timestamp the timestamp that should be stored as the modifed
      * @param bool $studentisonline is the student currently interacting with Moodle?
      */
     public function handle_if_time_expired($timestamp, $studentisonline) {
         global $DB;
 
-        $timeleft = $this->get_access_manager($timestamp)->get_time_left($this->attempt, $timestamp);
+        $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
 
-        if ($timeleft === false || $timeleft > 0) {
+        if ($timeclose === false || $this->is_preview()) {
+            $this->update_timecheckstate(null);
+            return; // No time limit
+        }
+        if ($timestamp < $timeclose) {
+            $this->update_timecheckstate($timeclose);
             return; // Time has not yet expired.
         }
 
         // If the attempt is already overdue, look to see if it should be abandoned ...
         if ($this->attempt->state == self::OVERDUE) {
-            $timeoverdue = -$timeleft;
-            if ($timeoverdue > $this->quizobj->get_quiz()->graceperiod) {
+            $timeoverdue = $timestamp - $timeclose;
+            $graceperiod = $this->quizobj->get_quiz()->graceperiod;
+            if ($timeoverdue >= $graceperiod) {
                 $this->process_abandon($timestamp, $studentisonline);
+            } else {
+                // Overdue time has not yet expired
+                $this->update_timecheckstate($timeclose + $graceperiod);
             }
-
             return; // ... and we are done.
         }
 
         if ($this->attempt->state != self::IN_PROGRESS) {
+            $this->update_timecheckstate(null);
             return; // Attempt is already in a final state.
         }
 
@@ -1311,6 +1321,10 @@ class quiz_attempt {
                 $this->process_abandon($timestamp, $studentisonline);
                 return;
         }
+
+        // This is an overdue attempt with no overdue handling defined, so just abandon.
+        $this->process_abandon($timestamp, $studentisonline);
+        return;
     }
 
     /**
@@ -1373,6 +1387,7 @@ class quiz_attempt {
         $this->attempt->timefinish = $timestamp;
         $this->attempt->sumgrades = $this->quba->get_total_mark();
         $this->attempt->state = self::FINISHED;
+        $this->attempt->timecheckstate = null;
         $DB->update_record('quiz_attempts', $this->attempt);
 
         if (!$this->is_preview()) {
@@ -1388,6 +1403,18 @@ class quiz_attempt {
         $transaction->allow_commit();
     }
 
+    /**
+     * Update this attempt timecheckstate if necessary.
+     * @param int|null the timecheckstate
+     */
+    public function update_timecheckstate($time) {
+        global $DB;
+        if ($this->attempt->timecheckstate !== $time) {
+            $this->attempt->timecheckstate = $time;
+            $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id'=>$this->attempt->id));
+        }
+    }
+
     /**
      * Mark this attempt as now overdue.
      * @param int $timestamp the time to deem as now.
@@ -1399,6 +1426,9 @@ class quiz_attempt {
         $transaction = $DB->start_delegated_transaction();
         $this->attempt->timemodified = $timestamp;
         $this->attempt->state = self::OVERDUE;
+        // If we knew the attempt close time, we could compute when the graceperiod ends.
+        // Instead we'll just fix it up through cron.
+        $this->attempt->timecheckstate = $timestamp;
         $DB->update_record('quiz_attempts', $this->attempt);
 
         $this->fire_state_transition_event('quiz_attempt_overdue', $timestamp);
@@ -1417,6 +1447,7 @@ class quiz_attempt {
         $transaction = $DB->start_delegated_transaction();
         $this->attempt->timemodified = $timestamp;
         $this->attempt->state = self::ABANDONED;
+        $this->attempt->timecheckstate = null;
         $DB->update_record('quiz_attempts', $this->attempt);
 
         $this->fire_state_transition_event('quiz_attempt_abandoned', $timestamp);
index 80f3e6e..63a897a 100644 (file)
@@ -79,7 +79,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
 
         $attempt = new backup_nested_element('attempt', array('id'), array(
             'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview',
-            'state', 'timestart', 'timefinish', 'timemodified', 'sumgrades'));
+            'state', 'timestart', 'timefinish', 'timemodified', 'timecheckstate', 'sumgrades'));
 
         // This module is using questions, so produce the related question states and sessions
         // attaching them to the $attempt element based in 'uniqueid' matching.
index 58a38d1..8014bc1 100644 (file)
@@ -306,6 +306,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
         $data->timestart = $this->apply_date_offset($data->timestart);
         $data->timefinish = $this->apply_date_offset($data->timefinish);
         $data->timemodified = $this->apply_date_offset($data->timemodified);
+        $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
 
         // Deals with up-grading pre-2.3 back-ups to 2.3+.
         if (!isset($data->state)) {
index 9e4e6a4..c8776de 100644 (file)
@@ -40,15 +40,13 @@ class mod_quiz_overdue_attempt_updater {
     /**
      * Do the processing required.
      * @param int $timenow the time to consider as 'now' during the processing.
-     * @param int $processfrom the value of $processupto the last time update_overdue_attempts was
-     *      called called and completed successfully.
-     * @param int $processto only process attempt modifed longer ago than this.
+     * @param int $processto only process attempt with timecheckstate longer ago than this.
      * @return array with two elements, the number of attempt considered, and how many different quizzes that was.
      */
-    public function update_overdue_attempts($timenow, $processfrom, $processto) {
+    public function update_overdue_attempts($timenow, $processto) {
         global $DB;
 
-        $attemptstoprocess = $this->get_list_of_overdue_attempts($processfrom, $processto);
+        $attemptstoprocess = $this->get_list_of_overdue_attempts($processto);
 
         $course = null;
         $quiz = null;
@@ -97,61 +95,27 @@ class mod_quiz_overdue_attempt_updater {
      * @return moodle_recordset of quiz_attempts that need to be processed because time has
      *     passed. The array is sorted by courseid then quizid.
      */
-    protected function get_list_of_overdue_attempts($processfrom, $processto) {
+    public function get_list_of_overdue_attempts($processto) {
         global $DB;
 
+
+        // SQL to compute timeclose and timelimit for each attempt:
+        $quizausersql = quiz_get_attempt_usertime_sql();
+
         // This query should have all the quiz_attempts columns.
         return $DB->get_recordset_sql("
          SELECT quiza.*,
-                group_by_results.usertimeclose,
-                group_by_results.usertimelimit
-
-           FROM (
-
-         SELECT iquiza.id AS attemptid,
-                quiz.course,
-                quiz.graceperiod,
-                COALESCE(quo.timeclose, MAX(qgo.timeclose), quiz.timeclose) AS usertimeclose,
-                COALESCE(quo.timelimit, MAX(qgo.timelimit), quiz.timelimit) AS usertimelimit
-
-           FROM {quiz_attempts} iquiza
-           JOIN {quiz} quiz ON quiz.id = iquiza.quiz
-      LEFT JOIN {quiz_overrides} quo ON quo.quiz = quiz.id AND quo.userid = iquiza.userid
-      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
-      LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id AND qgo.groupid = gm.groupid
-
-          WHERE iquiza.state IN ('inprogress', 'overdue')
-            AND iquiza.timemodified >= :processfrom
-            AND iquiza.timemodified < :processto
-
-       GROUP BY iquiza.id,
-                quiz.course,
-                quiz.timeclose,
-                quiz.timelimit,
-                quiz.graceperiod,
-                quo.timeclose,
-                quo.timelimit
-        ) group_by_results
-           JOIN {quiz_attempts} quiza ON quiza.id = group_by_results.attemptid
-
-          WHERE (
-                state = 'inprogress' AND (
-                    (usertimeclose > 0 AND :timenow1 > usertimeclose) OR
-                    (usertimelimit > 0 AND :timenow2 > quiza.timestart + usertimelimit)
-                )
-            )
-          OR
-            (
-                state = 'overdue' AND (
-                    (usertimeclose > 0 AND :timenow3 > graceperiod + usertimeclose) OR
-                    (usertimelimit > 0 AND :timenow4 > graceperiod + quiza.timestart + usertimelimit)
-                )
-            )
-
-       ORDER BY course, quiz",
-
-                array('processfrom' => $processfrom, 'processto' => $processto,
-                    'timenow1' => $processto, 'timenow2' => $processto,
-                    'timenow3' => $processto, 'timenow4' => $processto));
+                quizauser.usertimeclose,
+                quizauser.usertimelimit
+
+           FROM {quiz_attempts} quiza
+           JOIN {quiz} quiz ON quiz.id = quiza.quiz
+           JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+
+          WHERE quiza.state IN ('inprogress', 'overdue')
+            AND quiza.timecheckstate <= :processto
+       ORDER BY quiz.course, quiza.quiz",
+
+                array('processto' => $processto));
     }
 }
index 0ff77f5..f2deac1 100644 (file)
@@ -43,6 +43,29 @@ $handlers = array(
         'handlerfunction' => 'quiz_attempt_overdue_handler',
         'schedule'        => 'cron',
     ),
+
+    // Handle group events, so that open quiz attempts with group overrides get
+    // updated check times.
+    'groups_member_added' => array (
+        'handlerfile'     => '/mod/quiz/locallib.php',
+        'handlerfunction' => 'quiz_groups_member_added_handler',
+        'schedule'        => 'instant',
+    ),
+    'groups_member_removed' => array (
+        'handlerfile'     => '/mod/quiz/locallib.php',
+        'handlerfunction' => 'quiz_groups_member_removed_handler',
+        'schedule'        => 'instant',
+    ),
+    'groups_members_removed' => array (
+        'handlerfile'     => '/mod/quiz/locallib.php',
+        'handlerfunction' => 'quiz_groups_members_removed_handler',
+        'schedule'        => 'instant',
+    ),
+    'groups_group_deleted' => array (
+        'handlerfile'     => '/mod/quiz/locallib.php',
+        'handlerfunction' => 'quiz_groups_group_deleted_handler',
+        'schedule'        => 'instant',
+    ),
 );
 
 /* List of events generated by the quiz module, with the fields on the event object.
index dede549..fa3186d 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/quiz/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/quiz"
+<XMLDB PATH="mod/quiz/db" VERSION="20121006" COMMENT="XMLDB file for Moodle mod/quiz"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -9,7 +9,7 @@
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Standard Moodle primary key." NEXT="course"/>
         <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the course this quiz is part of." PREVIOUS="id" NEXT="name"/>
         <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz name." PREVIOUS="course" NEXT="intro"/>
-        <FIELD NAME="intro" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz introduction text." PREVIOUS="name" NEXT="introformat"/>
+        <FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz introduction text." PREVIOUS="name" NEXT="introformat"/>
         <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Quiz intro text format." PREVIOUS="intro" NEXT="timeopen"/>
         <FIELD NAME="timeopen" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz opens. (0 = no restriction.)" PREVIOUS="introformat" NEXT="timeclose"/>
         <FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz closes. (0 = no restriction.)" PREVIOUS="timeopen" NEXT="timelimit"/>
@@ -33,7 +33,7 @@
         <FIELD NAME="navmethod" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="free" SEQUENCE="false" COMMENT="Any constraints on how the user is allowed to navigate around the quiz. Currently recognised values are 'free' and 'seq'." PREVIOUS="questionsperpage" NEXT="shufflequestions"/>
         <FIELD NAME="shufflequestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the question order should be shuffled for each attempt." PREVIOUS="navmethod" NEXT="shuffleanswers"/>
         <FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the parts of the question should be shuffled, in those question types that support it." PREVIOUS="shufflequestions" NEXT="questions"/>
-        <FIELD NAME="questions" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table." PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
+        <FIELD NAME="questions" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table." PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
         <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total of all the question instance maxmarks." PREVIOUS="questions" NEXT="grade"/>
         <FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total that the quiz overall grade is scaled to be out of." PREVIOUS="sumgrades" NEXT="timecreated"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when the quiz was added to the course." PREVIOUS="grade" NEXT="timemodified"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the user whose attempt this is." PREVIOUS="quiz" NEXT="attempt"/>
         <FIELD NAME="attempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Sequentially numbers this student's attempts at this quiz." PREVIOUS="userid" NEXT="uniqueid"/>
         <FIELD NAME="uniqueid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the question_usage that holds the details of the the question_attempts that make up this quiz attempt." PREVIOUS="attempt" NEXT="layout"/>
-        <FIELD NAME="layout" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="currentpage"/>
+        <FIELD NAME="layout" TYPE="text" NOTNULL="true" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="currentpage"/>
         <FIELD NAME="currentpage" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="layout" NEXT="preview"/>
         <FIELD NAME="preview" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="currentpage" NEXT="state"/>
         <FIELD NAME="state" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="inprogress" SEQUENCE="false" COMMENT="The current state of the attempts. 'inprogress', 'overdue', 'finished' or 'abandoned'." PREVIOUS="preview" NEXT="timestart"/>
         <FIELD NAME="timestart" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was started." PREVIOUS="state" NEXT="timefinish"/>
         <FIELD NAME="timefinish" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was submitted. 0 if the attempt has not been submitted yet." PREVIOUS="timestart" NEXT="timemodified"/>
-        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timefinish" NEXT="sumgrades"/>
-        <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt." PREVIOUS="timemodified" NEXT="needsupgradetonewqe"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timefinish" NEXT="timecheckstate"/>
+        <FIELD NAME="timecheckstate" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Next time quiz cron should check attempt for state changes.  NULL means never check." PREVIOUS="timemodified" NEXT="sumgrades"/>
+        <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt." PREVIOUS="timecheckstate" NEXT="needsupgradetonewqe"/>
         <FIELD NAME="needsupgradetonewqe" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used during the upgrade from Moodle 2.0 to 2.1. This will be removed in the future." PREVIOUS="sumgrades"/>
       </FIELDS>
       <KEYS>
@@ -77,7 +78,8 @@
         <KEY NAME="uniqueid" TYPE="foreign-unique" FIELDS="uniqueid" REFTABLE="question_usages" REFFIELDS="id" PREVIOUS="userid"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="quiz-userid-attempt" UNIQUE="true" FIELDS="quiz, userid, attempt"/>
+        <INDEX NAME="quiz-userid-attempt" UNIQUE="true" FIELDS="quiz, userid, attempt" NEXT="state-timecheckstate"/>
+        <INDEX NAME="state-timecheckstate" UNIQUE="false" FIELDS="state, timecheckstate" PREVIOUS="quiz-userid-attempt"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="quiz_grades" COMMENT="Stores the overall grade for each user on the quiz, based on their various attempts and the quiz.grademethod setting." PREVIOUS="quiz_attempts" NEXT="quiz_question_instances">
       </INDEXES>
     </TABLE>
   </TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
index cb8e76d..b8bb709 100644 (file)
@@ -360,6 +360,38 @@ function xmldb_quiz_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2012061703, 'quiz');
     }
 
+    if ($oldversion < 2012100801) {
+
+        // Define field timecheckstate to be added to quiz_attempts
+        $table = new xmldb_table('quiz_attempts');
+        $field = new xmldb_field('timecheckstate', XMLDB_TYPE_INTEGER, '10', null, null, null, '0', 'timemodified');
+
+        // Conditionally launch add field timecheckstate
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define index state-timecheckstate (not unique) to be added to quiz_attempts
+        $table = new xmldb_table('quiz_attempts');
+        $index = new xmldb_index('state-timecheckstate', XMLDB_INDEX_NOTUNIQUE, array('state', 'timecheckstate'));
+
+        // Conditionally launch add index state-timecheckstate
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Overdue cron no longer needs these
+        unset_config('overduelastrun', 'quiz');
+        unset_config('overduedoneto', 'quiz');
+
+        // Update timecheckstate on all open attempts
+        require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+        quiz_update_open_attempts(array());
+
+        // quiz savepoint reached
+        upgrade_mod_savepoint(true, 2012100801, 'quiz');
+    }
+
     return true;
 }
 
index 8d7b28d..ad918bd 100644 (file)
@@ -138,6 +138,14 @@ function quiz_update_instance($quiz, $mform) {
         quiz_update_grades($quiz);
     }
 
+    $updateattempts = $oldquiz->timelimit != $quiz->timelimit
+                   || $oldquiz->timeclose != $quiz->timeclose
+                   || $oldquiz->graceperiod != $quiz->graceperiod;
+    if ($updateattempts) {
+        require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+        quiz_update_open_attempts(array('quizid'=>$quiz->id));
+    }
+
     // Delete any previous preview attempts.
     quiz_delete_previews($quiz);
 
@@ -284,13 +292,25 @@ function quiz_update_effective_access($quiz, $userid) {
             $override->timeopen = min($opens);
         }
         if (is_null($override->timeclose) && count($closes)) {
-            $override->timeclose = max($closes);
+            if (in_array(0, $closes)) {
+                $override->timeclose = 0;
+            } else {
+                $override->timeclose = max($closes);
+            }
         }
         if (is_null($override->timelimit) && count($limits)) {
-            $override->timelimit = max($limits);
+            if (in_array(0, $limits)) {
+                $override->timelimit = 0;
+            } else {
+                $override->timelimit = max($limits);
+            }
         }
         if (is_null($override->attempts) && count($attempts)) {
-            $override->attempts = max($attempts);
+            if (in_array(0, $attempts)) {
+                $override->attempts = 0;
+            } else {
+                $override->attempts = max($attempts);
+            }
         }
         if (is_null($override->password) && count($passwords)) {
             $override->password = array_shift($passwords);
@@ -446,32 +466,20 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
  */
 function quiz_cron() {
     global $CFG;
-    mtrace('');
 
-    // Since the quiz specifies $module->cron = 60, so that the subplugins can
-    // have frequent cron if they need it, we now need to do our own scheduling.
-    $quizconfig = get_config('quiz');
-    if (!isset($quizconfig->overduelastrun)) {
-        $quizconfig->overduelastrun = 0;
-        $quizconfig->overduedoneto  = 0;
-    }
+    require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
+    mtrace('');
 
     $timenow = time();
-    if ($timenow > $quizconfig->overduelastrun + 3600) {
-        require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
-        $overduehander = new mod_quiz_overdue_attempt_updater();
+    $overduehander = new mod_quiz_overdue_attempt_updater();
 
-        $processto = $timenow - $quizconfig->graceperiodmin;
+    $processto = $timenow - get_config('quiz', 'graceperiodmin');
 
-        mtrace('  Looking for quiz overdue quiz attempts between ' .
-                userdate($quizconfig->overduedoneto) . ' and ' . userdate($processto) . '...');
+    mtrace('  Looking for quiz overdue quiz attempts...');
 
-        list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
-        set_config('overduelastrun', $timenow, 'quiz');
-        set_config('overduedoneto', $processto, 'quiz');
+    list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $processto);
 
-        mtrace('  Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
-    }
+    mtrace('  Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
 
     // Run cron for our sub-plugin types.
     cron_execute_plugin_type('quiz', 'quiz reports');
index 4f2c271..ea82013 100644 (file)
@@ -65,7 +65,7 @@ define('QUIZ_MIN_TIME_TO_CONTINUE', '2');
  * user starting at the current time. The ->id field is not set. The object is
  * NOT written to the database.
  *
- * @param object $quiz the quiz to create an attempt for.
+ * @param object $quizobj the quiz object to create an attempt for.
  * @param int $attemptnumber the sequence number for the attempt.
  * @param object $lastattempt the previous attempt by this user, if any. Only needed
  *         if $attemptnumber > 1 and $quiz->attemptonlast is true.
@@ -74,9 +74,10 @@ define('QUIZ_MIN_TIME_TO_CONTINUE', '2');
  *
  * @return object the newly created attempt object.
  */
-function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
+function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
     global $USER;
 
+    $quiz = $quizobj->get_quiz();
     if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) {
         throw new moodle_exception('cannotstartgradesmismatch', 'quiz',
                 new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id)),
@@ -112,6 +113,13 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
         $attempt->preview = 1;
     }
 
+    $timeclose = $quizobj->get_access_manager($timenow)->get_end_time($attempt);
+    if ($timeclose === false || $ispreview) {
+        $attempt->timecheckstate = null;
+    } else {
+        $attempt->timecheckstate = $timeclose;
+    }
+
     return $attempt;
 }
 
@@ -754,6 +762,142 @@ function quiz_update_all_final_grades($quiz) {
     }
 }
 
+/**
+ * Efficiently update check state time on all open attempts
+ *
+ * @param array $conditions optional restrictions on which attempts to update
+ *                    Allowed conditions:
+ *                      courseid => (array|int) attempts in given course(s)
+ *                      userid   => (array|int) attempts for given user(s)
+ *                      quizid   => (array|int) attempts in given quiz(s)
+ *                      groupid  => (array|int) quizzes with some override for given group(s)
+ *
+ */
+function quiz_update_open_attempts(array $conditions) {
+    global $DB;
+
+    foreach ($conditions as &$value) {
+        if (!is_array($value)) {
+            $value = array($value);
+        }
+    }
+
+    $params = array();
+    $coursecond = '';
+    $usercond = '';
+    $quizcond = '';
+    $groupcond = '';
+
+    if (isset($conditions['courseid'])) {
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid');
+        $params = array_merge($params, $inparams);
+        $coursecond = "AND quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
+    }
+    if (isset($conditions['userid'])) {
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid');
+        $params = array_merge($params, $inparams);
+        $usercond = "AND quiza.userid $incond";
+    }
+    if (isset($conditions['quizid'])) {
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid');
+        $params = array_merge($params, $inparams);
+        $quizcond = "AND quiza.quiz $incond";
+    }
+    if (isset($conditions['groupid'])) {
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid');
+        $params = array_merge($params, $inparams);
+        $groupcond = "AND quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
+    }
+
+    // SQL to compute timeclose and timelimit for each attempt:
+    $quizausersql = quiz_get_attempt_usertime_sql();
+
+    // SQL to compute the new timecheckstate
+    $timecheckstatesql = "
+          CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL
+               WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose
+               WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit
+               WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit
+               ELSE quizauser.usertimeclose END +
+          CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END";
+
+    // SQL to select which attempts to process
+    $attemptselect = " quiza.state IN ('inprogress', 'overdue')
+                       $coursecond
+                       $usercond
+                       $quizcond
+                       $groupcond";
+
+   /*
+    * Each database handles updates with inner joins differently:
+    *  - mysql does not allow a FROM clause
+    *  - postgres and mssql allow FROM but handle table aliases differently
+    *  - oracle requires a subquery
+    *
+    * Different code for each database.
+    */
+
+    $dbfamily = $DB->get_dbfamily();
+    if ($dbfamily == 'mysql') {
+        $updatesql = "UPDATE {quiz_attempts} quiza
+                        JOIN {quiz} quiz ON quiz.id = quiza.quiz
+                        JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+                         SET quiza.timecheckstate = $timecheckstatesql
+                       WHERE $attemptselect";
+    } else if ($dbfamily == 'postgres') {
+        $updatesql = "UPDATE {quiz_attempts} quiza
+                         SET timecheckstate = $timecheckstatesql
+                        FROM {quiz} quiz, ( $quizausersql ) quizauser
+                       WHERE quiz.id = quiza.quiz
+                         AND quizauser.id = quiza.id
+                         AND $attemptselect";
+    } else if ($dbfamily == 'mssql') {
+        $updatesql = "UPDATE quiza
+                         SET timecheckstate = $timecheckstatesql
+                        FROM {quiz_attempts} quiza
+                        JOIN {quiz} quiz ON quiz.id = quiza.quiz
+                        JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+                       WHERE $attemptselect";
+    } else {
+        // oracle, sqlite and others
+        $updatesql = "UPDATE {quiz_attempts} quiza
+                         SET timecheckstate = (
+                           SELECT $timecheckstatesql
+                             FROM {quiz} quiz, ( $quizausersql ) quizauser
+                            WHERE quiz.id = quiza.quiz
+                              AND quizauser.id = quiza.id
+                         )
+                         WHERE $attemptselect";
+    }
+
+    $DB->execute($updatesql, $params);
+}
+
+/**
+ * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides.
+ *
+ * @return string         SQL select with columns attempt.id, usertimeclose, usertimelimit
+ */
+function quiz_get_attempt_usertime_sql() {
+    // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede
+    // any other group override
+    $quizausersql = "
+          SELECT iquiza.id,
+           COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose,
+           COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit
+
+           FROM {quiz_attempts} iquiza
+           JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz
+      LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid
+      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
+      LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0
+      LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0
+      LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0
+      LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0
+       GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit";
+    return $quizausersql;
+}
+
 /**
  * Return the attempt with the best grade for a quiz
  *
@@ -1445,6 +1589,58 @@ function quiz_attempt_overdue_handler($event) {
             context_module::instance($cm->id), $cm);
 }
 
+/**
+ * Handle groups_member_added event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_member_added_handler($event) {
+    quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
+}
+
+/**
+ * Handle groups_member_removed event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_member_removed_handler($event) {
+    quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
+}
+
+/**
+ * Handle groups_group_deleted event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_group_deleted_handler($event) {
+    global $DB;
+
+    // It would be nice if we got the groupid that was deleted.
+    // Instead, we just update all quizzes with orphaned group overrides
+    $sql = "SELECT o.id, o.quiz
+              FROM {quiz_overrides} o
+              JOIN {quiz} quiz ON quiz.id = o.quiz
+         LEFT JOIN {groups} grp ON grp.id = o.groupid
+             WHERE quiz.course = :courseid AND grp.id IS NULL";
+    $params = array('courseid'=>$event->courseid);
+    $records = $DB->get_records_sql_menu($sql, $params);
+    $DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
+    quiz_update_open_attempts(array('quizid'=>array_unique(array_values($records))));
+}
+
+/**
+ * Handle groups_members_removed event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_members_removed_handler($event) {
+    if ($event->userid == 0) {
+        quiz_update_open_attempts(array('courseid'=>$event->courseid));
+    } else {
+        quiz_update_open_attempts(array('courseid'=>$event->courseid, 'userid'=>$event->userid));
+    }
+}
+
 /**
  * Get the information about the standard quiz JavaScript module.
  * @return array a standard jsmodule structure.
index 70332fa..5e505f9 100644 (file)
@@ -51,17 +51,22 @@ M.mod_quiz.timer = {
     // Timestamp at which time runs out, according to the student's computer's clock.
     endtime: 0,
 
+    // Is this a quiz preview?
+    preview: 0,
+
     // This records the id of the timeout that updates the clock periodically,
     // so we can cancel.
     timeoutid: null,
 
     /**
      * @param Y the YUI object
-     * @param timeleft, the time remaining, in seconds.
+     * @param start, the timer starting time, in seconds.
+     * @param preview, is this a quiz preview?
      */
-    init: function(Y, timeleft) {
+    init: function(Y, start, preview) {
         M.mod_quiz.timer.Y = Y;
-        M.mod_quiz.timer.endtime = new Date().getTime() + timeleft*1000;
+        M.mod_quiz.timer.endtime = new Date().getTime() + start*1000;
+        M.mod_quiz.timer.preview = preview;
         M.mod_quiz.timer.update();
         Y.one('#quiz-timer').setStyle('display', 'block');
     },
@@ -90,6 +95,12 @@ M.mod_quiz.timer = {
     update: function() {
         var Y = M.mod_quiz.timer.Y;
         var secondsleft = Math.floor((M.mod_quiz.timer.endtime - new Date().getTime())/1000);
+        
+        // If this is a preview and time expired, display timeleft 0 and don't renew the timer.
+        if (M.mod_quiz.timer.preview && secondsleft < 0) {
+            Y.one('#quiz-time-left').setContent('0:00:00');
+            return;
+        }
 
         // If time has expired, Set the hidden form field that says time has expired.
         if (secondsleft < 0) {
index fa0c53d..b298780 100644 (file)
@@ -169,6 +169,7 @@ if ($mform->is_cancelled()) {
         $fromform->id = $DB->insert_record('quiz_overrides', $fromform);
     }
 
+    quiz_update_open_attempts(array('quizid'=>$quiz->id));
     quiz_update_events($quiz, $fromform);
 
     add_to_log($cm->course, 'quiz', 'edit override',
index aa4a171..b66d2ff 100644 (file)
@@ -67,12 +67,17 @@ if ($page == -1) {
 // to show the student another page of the quiz. Just finish now.
 $graceperiodmin = null;
 $accessmanager = $attemptobj->get_access_manager($timenow);
-$timeleft = $accessmanager->get_time_left($attemptobj->get_attempt(), $timenow);
+$timeclose = $accessmanager->get_end_time($attemptobj->get_attempt());
+
+// Don't enforce timeclose for previews
+if ($attemptobj->is_preview()) {
+    $timeclose = false;
+}
 $toolate = false;
-if ($timeleft !== false && $timeleft < QUIZ_MIN_TIME_TO_CONTINUE) {
+if ($timeclose !== false && $timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) {
     $timeup = true;
     $graceperiodmin = get_config('quiz', 'graceperiodmin');
-    if ($timeleft < -$graceperiodmin) {
+    if ($timenow > $timeclose + $graceperiodmin) {
         $toolate = true;
     }
 }
@@ -105,7 +110,7 @@ if ($timeup) {
         if (is_null($graceperiodmin)) {
             $graceperiodmin = get_config('quiz', 'graceperiodmin');
         }
-        if ($timeleft < -$attemptobj->get_quiz()->graceperiod - $graceperiodmin) {
+        if ($timenow > $timeclose + $attemptobj->get_quiz()->graceperiod + $graceperiodmin) {
             // Grace period has run out.
             $finishattempt = true;
             $becomingabandoned = true;
index 329ff32..759cd76 100644 (file)
@@ -265,12 +265,16 @@ class mod_quiz_renderer extends plugin_renderer_base {
      */
     public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
 
-        $timeleft = $attemptobj->get_time_left($timenow);
+        $timeleft = $attemptobj->get_time_left_display($timenow);
         if ($timeleft !== false) {
-            // Make sure the timer starts just above zero. If $timeleft was <= 0, then
-            // this will just have the effect of causing the quiz to be submitted immediately.
-            $timerstartvalue = max($timeleft, 1);
-            $this->initialise_timer($timerstartvalue);
+            $ispreview = $attemptobj->is_preview();
+            $timerstartvalue = $timeleft;
+            if (!$ispreview) {
+                // Make sure the timer starts just above zero. If $timeleft was <= 0, then
+                // this will just have the effect of causing the quiz to be submitted immediately.
+                $timerstartvalue = max($timerstartvalue, 1);
+            }
+            $this->initialise_timer($timerstartvalue, $ispreview);
         }
 
         return html_writer::tag('div', get_string('timeleft', 'quiz') . ' ' .
@@ -487,9 +491,9 @@ class mod_quiz_renderer extends plugin_renderer_base {
      * Output the JavaScript required to initialise the countdown timer.
      * @param int $timerstartvalue time remaining, in seconds.
      */
-    public function initialise_timer($timerstartvalue) {
-        $this->page->requires->js_init_call('M.mod_quiz.timer.init',
-                array($timerstartvalue), false, quiz_get_js_module());
+    public function initialise_timer($timerstartvalue, $ispreview) {
+        $options = array($timerstartvalue, (bool)$ispreview);
+        $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module());
     }
 
     /**
index 8cead80..374db3f 100644 (file)
@@ -165,7 +165,7 @@ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 
 // Create the new attempt and initialize the question sessions
 $timenow = time(); // Update time now, in case the server is running really slowly.
-$attempt = quiz_create_attempt($quizobj->get_quiz(), $attemptnumber, $lastattempt, $timenow,
+$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow,
         $quizobj->is_preview_user());
 
 if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
diff --git a/mod/quiz/tests/attempts_test.php b/mod/quiz/tests/attempts_test.php
new file mode 100644 (file)
index 0000000..b70c26c
--- /dev/null
@@ -0,0 +1,388 @@
+<?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/>.
+
+/**
+ * Quiz attempt overdue handling tests
+ *
+ * @package    mod_quiz
+ * @category   phpunit
+ * @copyright  2012 Matt Petro
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/group/lib.php');
+
+/**
+ * Unit tests for quiz attempt overdue handling
+ *
+ * @package    mod_quiz
+ * @category   phpunit
+ * @copyright  2012 Matt Petro
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_attempt_overdue_testcase extends advanced_testcase {
+    /**
+     * Test the functions quiz_update_open_attempts() and get_list_of_overdue_attempts()
+     */
+    public function test_bulk_update_functions() {
+        global $DB,$CFG;
+
+        require_once($CFG->dirroot.'/mod/quiz/cronlib.php');
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        // Setup course, user and groups
+
+        $course = $this->getDataGenerator()->create_course();
+        $user1 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $this->assertTrue(enrol_try_internal_enrol($course->id, $user1->id, $studentrole->id));
+        $group1 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+        $group3 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+        $this->assertTrue(groups_add_member($group1, $user1));
+        $this->assertTrue(groups_add_member($group2, $user1));
+
+        $uniqueid = 0;
+        $usertimes = array();
+
+        $quiz_generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+        // Basic quiz settings
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, 'message'=>'Test1A');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>1800));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>1800, 'message'=>'Test1B');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>0, 'message'=>'Test1C');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>600, 'message'=>'Test1D');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>0, 'message'=>'Test1E');
+
+        // Group overrides
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>0, 'message'=>'Test2A');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1100, 'timelimit'=>null));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1100, 'timelimit'=>0, 'message'=>'Test2B');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>700));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>700, 'message'=>'Test2C');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>500));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>500, 'message'=>'Test2D');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>0, '', 'message'=>'Test2E');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>500, '', 'message'=>'Test2F');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>500, '', 'message'=>'Test2G');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group3->id, 'timeclose'=>1300, 'timelimit'=>500)); // user not in group
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test2H');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test2I');
+
+        // Multiple group overrides
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>501));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>500));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3A');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3B');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1301, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1300, 'timelimit'=>501));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3C');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3D');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1301, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1300, 'timelimit'=>501));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group3->id, 'timeclose'=>1500, 'timelimit'=>1000)); // user not in group
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3E');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3F');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>null, 'timelimit'=>501));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>501, '', 'message'=>'Test3G');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>501, '', 'message'=>'Test3H');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>null));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>500, '', 'message'=>'Test3I');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>500, '', 'message'=>'Test3J');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>0, '', 'message'=>'Test3K');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>0, '', 'message'=>'Test3L');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>0, 'timelimit'=>501));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>501, '', 'message'=>'Test3M');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>501, '', 'message'=>'Test3N');
+
+        // User overrides
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>601));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>601, '', 'message'=>'Test4A');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>601, '', 'message'=>'Test4B');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>0, 'timelimit'=>601));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>601, '', 'message'=>'Test4C');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>601, '', 'message'=>'Test4D');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>0));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>0, '', 'message'=>'Test4E');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>0, '', 'message'=>'Test4F');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>null, 'timelimit'=>601));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>601, '', 'message'=>'Test4G');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>601, '', 'message'=>'Test4H');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>null, 'timelimit'=>601));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>601, '', 'message'=>'Test4I');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>601, '', 'message'=>'Test4J');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>null));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>700, '', 'message'=>'Test4K');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>700, '', 'message'=>'Test4L');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>null));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>600, '', 'message'=>'Test4M');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>600, '', 'message'=>'Test4N');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>0, 'timeclose'=>1201, 'timelimit'=>601)); // not user
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>700, '', 'message'=>'Test4O');
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+        $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>700, '', 'message'=>'Test4P');
+
+        // Attempt state overdue
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600, 'overduehandling'=>'graceperiod', 'graceperiod'=>250));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'overdue', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test5A');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600, 'overduehandling'=>'graceperiod', 'graceperiod'=>250));
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'overdue', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+        $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>600, '', 'message'=>'Test5B');
+
+        //
+        // Test quiz_update_open_attempts()
+        //
+
+        quiz_update_open_attempts(array('courseid'=>$course->id));
+        foreach ($usertimes as $attemptid=>$times) {
+            $attempt = $DB->get_record('quiz_attempts', array('id'=>$attemptid));
+            $this->assertTrue(false !== $attempt, $times['message']);
+
+            if ($attempt->state == 'overdue') {
+                $graceperiod = $DB->get_field('quiz', 'graceperiod', array('id'=>$attempt->quiz));
+            } else {
+                $graceperiod = 0;
+            }
+            if ($times['timeclose'] > 0 and $times['timelimit'] > 0) {
+                $this->assertEquals(min($times['timeclose'], $attempt->timestart + $times['timelimit']) + $graceperiod, $attempt->timecheckstate, $times['message']);
+            } else if ($times['timeclose'] > 0) {
+                $this->assertEquals($times['timeclose'] + $graceperiod, $attempt->timecheckstate <= $times['timeclose'], $times['message']);
+            } else if ($times['timelimit'] > 0) {
+                $this->assertEquals($attempt->timestart + $times['timelimit'] + $graceperiod, $attempt->timecheckstate, $times['message']);
+            } else {
+                $this->assertNull($attempt->timecheckstate, $times['message']);
+            }
+        }
+
+        //
+        // Test get_list_of_overdue_attempts()
+        //
+
+        $overduehander = new mod_quiz_overdue_attempt_updater();
+
+        $attempts = $overduehander->get_list_of_overdue_attempts(100000); // way in the future
+        $count = 0;
+        foreach ($attempts as $attempt) {
+            $this->assertTrue(isset($usertimes[$attempt->id]));
+            $times = $usertimes[$attempt->id];
+            $this->assertEquals($times['timeclose'], $attempt->usertimeclose, $times['message']);
+            $this->assertEquals($times['timelimit'], $attempt->usertimelimit, $times['message']);
+            $count++;
+
+        }
+        $this->assertEquals($DB->count_records_select('quiz_attempts', 'timecheckstate IS NOT NULL'), $count);
+
+        $attempts = $overduehander->get_list_of_overdue_attempts(0); // before all attempts
+        $count = 0;
+        foreach ($attempts as $attempt) {
+            $count++;
+        }
+        $this->assertEquals(0, $count);
+
+    }
+
+    /**
+     * Test the group event handlers
+     */
+    public function test_group_event_handlers() {
+        global $DB,$CFG;
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        // Setup course, user and groups
+
+        $course = $this->getDataGenerator()->create_course();
+        $user1 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $this->assertTrue(enrol_try_internal_enrol($course->id, $user1->id, $studentrole->id));
+        $group1 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+        $this->assertTrue(groups_add_member($group1, $user1));
+        $this->assertTrue(groups_add_member($group2, $user1));
+
+        $uniqueid = 0;
+
+        $quiz_generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+        $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+
+        // add a group1 override
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null));
+
+        // add an attempt
+        $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+
+        // update timecheckstate
+        quiz_update_open_attempts(array('quizid'=>$quiz->id));
+        $this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // remove from group
+        $this->assertTrue(groups_remove_member($group1, $user1));
+        $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // add back to group
+        $this->assertTrue(groups_add_member($group1, $user1));
+        $this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // delete group
+        groups_delete_group($group1);
+        $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+        $this->assertEquals(0, $DB->count_records('quiz_overrides', array('quiz'=>$quiz->id)));
+
+        // add a group2 override
+        $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1400, 'timelimit'=>null));
+        quiz_update_open_attempts(array('quizid'=>$quiz->id));
+        $this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // delete user1 from all groups
+        groups_delete_group_members($course->id, $user1->id);
+        $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // add back to group2
+        $this->assertTrue(groups_add_member($group2, $user1));
+        $this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+
+        // delete everyone from all groups
+        groups_delete_group_members($course->id);
+        $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
+    }
+}
\ No newline at end of file
index 24ed645..b889d3d 100644 (file)
@@ -54,7 +54,7 @@ class mod_quiz_generator extends phpunit_module_generator {
                 mod_quiz_display_options::LATER_WHILE_OPEN | mod_quiz_display_options::AFTER_CLOSE;
 
         $defaultquizsettings = array(
-            'name'                   => get_string('pluginname', 'data').' '.$i,
+            'name'                   => get_string('pluginname', 'quiz').' '.$i,
             'intro'                  => 'Test quiz ' . $i,
             'introformat'            => FORMAT_MOODLE,
             'timeopen'               => 0,
diff --git a/mod/quiz/tests/generator_test.php b/mod/quiz/tests/generator_test.php
new file mode 100644 (file)
index 0000000..4ae705a
--- /dev/null
@@ -0,0 +1,63 @@
+<?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/>.
+
+/**
+ * PHPUnit data generator tests
+ *
+ * @package    mod_quiz
+ * @category   phpunit
+ * @copyright  2012 Matt Petro
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * PHPUnit data generator testcase
+ *
+ * @package    mod_quiz
+ * @category   phpunit
+ * @copyright  2012 Matt Petro
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_generator_testcase extends advanced_testcase {
+    public function test_generator() {
+        global $DB, $SITE;
+
+        $this->resetAfterTest(true);
+
+        $this->assertEquals(0, $DB->count_records('quiz'));
+
+        /** @var mod_quiz_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+        $this->assertInstanceOf('mod_quiz_generator', $generator);
+        $this->assertEquals('quiz', $generator->get_modulename());
+
+        $generator->create_instance(array('course'=>$SITE->id));
+        $generator->create_instance(array('course'=>$SITE->id));
+        $quiz = $generator->create_instance(array('course'=>$SITE->id));
+        $this->assertEquals(3, $DB->count_records('quiz'));
+
+        $cm = get_coursemodule_from_instance('quiz', $quiz->id);
+        $this->assertEquals($quiz->id, $cm->instance);
+        $this->assertEquals('quiz', $cm->modname);
+        $this->assertEquals($SITE->id, $cm->course);
+
+        $context = context_module::instance($cm->id);
+        $this->assertEquals($quiz->cmid, $context->instanceid);
+    }
+}
index 59c7b3c..cbd42c2 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2012061703;       // The current module version (Date: YYYYMMDDXX).
+$module->version   = 2012100801;       // The current module version (Date: YYYYMMDDXX).
 $module->requires  = 2012061700;    // Requires this Moodle version.
 $module->component = 'mod_quiz';       // Full name of the plugin (used for diagnostics).
 $module->cron      = 60;
index 3511ae4..9895d49 100644 (file)
@@ -1,4 +1,18 @@
-<!--
+// 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/>.
+
 function NewHttpReq() {
     var httpReq = false;
     if (typeof XMLHttpRequest != 'undefined') {
@@ -42,4 +56,3 @@ function popupwin(content) {
     op.document.write(content);
     op.document.close();
 }
-//-->
index d3ba158..276c7ef 100644 (file)
@@ -343,7 +343,7 @@ foreach ($results as $user => $rest) {
     foreach ($nestedorder as $key => $nestedquestions) {
         foreach ($nestedquestions as $key2 => $qid) {
             $question = $questions[$qid];
-        
+
             if ($question->type == "0" || $question->type == "1" || $question->type == "3" || $question->type == "-1")  {
                 echo $results[$user][$qid]["answer1"]."    ";
             }
@@ -355,4 +355,4 @@ foreach ($results as $user => $rest) {
     echo "\n";
 }
 
-exit;
\ No newline at end of file
+exit;
index 56a0be2..e9c4abf 100644 (file)
@@ -6,7 +6,7 @@
 <svg version="1.1"\r
         xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
         x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
-        xml:space="preserve">\r
+        xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
 <defs>\r
 </defs>\r
 <path style="fill:#999999;" d="M15.7,7.3l-2.5-2.5c-0.4-0.4-0.7-0.3-0.7,0.3v1.6h-3v-3H11c0.5,0,0.7-0.3,0.3-0.7L8.7,0.3\r
diff --git a/pix/y/ln.png b/pix/y/ln.png
deleted file mode 100644 (file)
index d2ac508..0000000
Binary files a/pix/y/ln.png and /dev/null differ
index bd94e5b..5328c9c 100644 (file)
@@ -159,7 +159,7 @@ class qtype_numerical_edit_form extends question_edit_form {
         if ($mform->elementExists('multiplier[0]')) {
             $firstunit = $mform->getElement('multiplier[0]');
             $firstunit->freeze();
-            $firstunit->setValue('1.0');
+            $mform->setDefault('multiplier[0]', '1.0');
             $mform->addHelpButton('multiplier[0]', 'numericalmultiplier', 'qtype_numerical');
         }
     }
index adb51af..020fc16 100644 (file)
@@ -102,7 +102,7 @@ fieldset#general legend {
     padding-bottom:5px;
 }
 
-/* Courses 
+/* Courses
 ----------------------------*/
 
 h2.headingblock {
@@ -162,7 +162,7 @@ h2.headingblock {
 .course-content .current h3.weekdates {
     color: #f25f0f !important;
 }
-/* Forum 
+/* Forum
 --------------------------*/
 
 .forumpost .topic {
index ed52934..9fc9fe4 100644 (file)
@@ -345,7 +345,7 @@ a.ygtvspacer:hover {color: transparent;text-decoration: none;}
 .dndsupported .dndupload-ready .dndupload-target {display:block;}
 .dndupload-uploadinprogress {display:none;text-align:center;}
 .dndupload-uploading .dndupload-uploadinprogress {display:block;}
-.dndupload-arrow {background:url([[pix:theme|fp/dnd_arrow]]) center no-repeat;width:60px;height:80px;position:absolute;margin-left: -28px;top: 5px;}
+.dndupload-arrow {background:url([[pix:theme|fp/dnd_arrow]]) center no-repeat;width:100%;height:80px;position:absolute;margin-left: -28px;top: 5px;}
 .fitem.disabled .filepicker-container, .fitem.disabled .fm-empty-container {display:none;}
 
 /*
index 98bc7e6..f3d3e9e 100644 (file)
@@ -749,6 +749,11 @@ class theme_mymobile_core_renderer extends core_renderer {
             $select->attributes['title'] = $select->tooltip;
         }
 
+        $select->attributes['class'] = 'autosubmit';
+        if ($select->class) {
+            $select->attributes['class'] .= ' ' . $select->class;
+        }
+
         if ($select->label) {
             $output .= html_writer::label($select->label, $select->attributes['id']);
         }
@@ -767,7 +772,10 @@ class theme_mymobile_core_renderer extends core_renderer {
         $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('style' => 'inline'));
 
         $nothing = empty($select->nothing) ? false : key($select->nothing);
-        $this->page->requires->js_init_call('M.util.init_select_autosubmit', array($select->formid, $select->attributes['id'], $nothing));
+        $this->page->requires->yui_module('moodle-core-formautosubmit',
+            'M.core.init_formautosubmit',
+            array(array('selectid' => $select->attributes['id'], 'nothing' => $nothing))
+        );
 
         // then div wrapper for xhtml strictness
         $output = html_writer::tag('div', $output);
index 302f06b..e3e533b 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012112100.00;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012112301.00;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes
 
-$release  = '2.4beta+ (Build: 20121121)'; // Human-friendly version name
+$release  = '2.4beta+ (Build: 20121123)'; // Human-friendly version name
 
 $branch   = '24';                       // this version's branch
 $maturity = MATURITY_BETA;             // this version's maturity level