** 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._
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'])) {
* @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.
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
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
*
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.
*
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) {
--- /dev/null
+<?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;
+ }
+}
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');
/**
}
}
$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()) {
// 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;
}
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();
}
* @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.
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.
*
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.
*
* @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
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.
*
$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.
*/
* @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
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.
*
$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.
*/
* @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
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
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
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.
*
* @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
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.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-abstract class session_data_store {
+abstract class session_data_store extends cache_store {
/**
* Used for the actual storage.
* @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
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.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-abstract class static_data_store {
+abstract class static_data_store extends cache_store {
/**
* An array for storage.
/**
* 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.
*/
$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>';
}
}
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(
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(
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>";
echo "<a href=\"search.php?search=$encodedsearch".$modulelink."&perpage=".$defaultperpage."\">".get_string("showperpage", "", $defaultperpage)."</a>";
echo "</p></center>";
}
-}
\ No newline at end of file
+}
<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>
/** 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++) {
$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);
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
*
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() {
/**
* 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) {
* 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) {
object.width = width;
object.height = height;
}
- }
+ }
}
});
}
$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);
}
$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);
$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) {
$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));
}
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,
--- /dev/null
+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']
+ }
+);
$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';
$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';
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;
$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);
}
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));
echo '<a href="javascript:deselect_all_in(\'DIV\',null,\'tablecontainer\');">'.get_string('deselectall').'</a> ';
echo ' ';
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>';
* @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;
+ }
}
}
$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) {
$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;
}
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;
$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>';
}
}
/**
- * 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;
}
/**
* 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;
}
/**
$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));
$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));
$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));
$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),
$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));
$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';
$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));
}
}
$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));
}
}
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;
}
$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() {
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(),
$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() {
$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(),
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() {
$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() {
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));
}
}
$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));
}
}
$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));
}
}
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;
+
}
}
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));
* 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
* @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.
/**
* 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.
}
$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;
}
/**
$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()) {
$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.
$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);
$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);
$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.
$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)) {
/**
* 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;
* @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));
}
}
'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.
<?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"
>
<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"/>
<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>
<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
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;
}
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);
$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);
*/
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');
* 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.
*
* @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)),
$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;
}
}
}
+/**
+ * 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
*
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.
// 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');
},
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) {
$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',
// 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;
}
}
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;
*/
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') . ' ' .
* 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());
}
/**
// 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)) {
--- /dev/null
+<?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
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,
--- /dev/null
+<?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);
+ }
+}
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;
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"]." ";
}
echo "\n";
}
-exit;
\ No newline at end of file
+exit;
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');
}
}
padding-bottom:5px;
}
-/* Courses
+/* Courses
----------------------------*/
h2.headingblock {
.course-content .current h3.weekdates {
color: #f25f0f !important;
}
-/* Forum
+/* Forum
--------------------------*/
.forumpost .topic {
.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;}
/*
$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']);
}
$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);