2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * This defines the core classes of the Moodle question engine.
21 * @subpackage questionengine
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') ||
die();
29 require_once(dirname(__FILE__
) . '/questionusage.php');
30 require_once(dirname(__FILE__
) . '/questionattempt.php');
31 require_once(dirname(__FILE__
) . '/questionattemptstep.php');
32 require_once(dirname(__FILE__
) . '/states.php');
33 require_once(dirname(__FILE__
) . '/datalib.php');
34 require_once(dirname(__FILE__
) . '/renderer.php');
35 require_once(dirname(__FILE__
) . '/bank.php');
36 require_once(dirname(__FILE__
) . '/../type/questiontypebase.php');
37 require_once(dirname(__FILE__
) . '/../type/questionbase.php');
38 require_once(dirname(__FILE__
) . '/../type/rendererbase.php');
39 require_once(dirname(__FILE__
) . '/../behaviour/behaviourbase.php');
40 require_once(dirname(__FILE__
) . '/../behaviour/rendererbase.php');
41 require_once($CFG->libdir
. '/questionlib.php');
45 * This static class provides access to the other question engine classes.
47 * It provides functions for managing question behaviours), and for
48 * creating, loading, saving and deleting {@link question_usage_by_activity}s,
49 * which is the main class that is used by other code that wants to use questions.
51 * @copyright 2009 The Open University
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 abstract class question_engine
{
55 /** @var array behaviour name => 1. Records which behaviours have been loaded. */
56 private static $loadedbehaviours = array();
59 * Create a new {@link question_usage_by_activity}. The usage is
60 * created in memory. If you want it to persist, you will need to call
61 * {@link save_questions_usage_by_activity()}.
63 * @param string $component the plugin creating this attempt. For example mod_quiz.
64 * @param object $context the context this usage belongs to.
65 * @return question_usage_by_activity the newly created object.
67 public static function make_questions_usage_by_activity($component, $context) {
68 return new question_usage_by_activity($component, $context);
72 * Load a {@link question_usage_by_activity} from the database, based on its id.
73 * @param int $qubaid the id of the usage to load.
74 * @return question_usage_by_activity loaded from the database.
76 public static function load_questions_usage_by_activity($qubaid) {
77 $dm = new question_engine_data_mapper();
78 return $dm->load_questions_usage_by_activity($qubaid);
82 * Save a {@link question_usage_by_activity} to the database. This works either
83 * if the usage was newly created by {@link make_questions_usage_by_activity()}
84 * or loaded from the database using {@link load_questions_usage_by_activity()}
85 * @param question_usage_by_activity the usage to save.
87 public static function save_questions_usage_by_activity(question_usage_by_activity
$quba) {
88 $dm = new question_engine_data_mapper();
89 $observer = $quba->get_observer();
90 if ($observer instanceof question_engine_unit_of_work
) {
93 $dm->insert_questions_usage_by_activity($quba);
98 * Delete a {@link question_usage_by_activity} from the database, based on its id.
99 * @param int $qubaid the id of the usage to delete.
101 public static function delete_questions_usage_by_activity($qubaid) {
102 self
::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
106 * Delete {@link question_usage_by_activity}s from the database.
107 * @param qubaid_condition $qubaids identifies which questions usages to delete.
109 public static function delete_questions_usage_by_activities(qubaid_condition
$qubaids) {
110 $dm = new question_engine_data_mapper();
111 $dm->delete_questions_usage_by_activities($qubaids);
115 * Change the maxmark for the question_attempt with number in usage $slot
116 * for all the specified question_attempts.
117 * @param qubaid_condition $qubaids Selects which usages are updated.
118 * @param int $slot the number is usage to affect.
119 * @param number $newmaxmark the new max mark to set.
121 public static function set_max_mark_in_attempts(qubaid_condition
$qubaids,
122 $slot, $newmaxmark) {
123 $dm = new question_engine_data_mapper();
124 $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
128 * @param array $questionids of question ids.
129 * @param qubaid_condition $qubaids ids of the usages to consider.
130 * @return boolean whether any of these questions are being used by any of
133 public static function questions_in_use(array $questionids, qubaid_condition
$qubaids = null) {
134 if (is_null($qubaids)) {
137 $dm = new question_engine_data_mapper();
138 return $dm->questions_in_use($questionids, $qubaids);
142 * Create an archetypal behaviour for a particular question attempt.
143 * Used by {@link question_definition::make_behaviour()}.
145 * @param string $preferredbehaviour the type of model required.
146 * @param question_attempt $qa the question attempt the model will process.
147 * @return question_behaviour an instance of appropriate behaviour class.
149 public static function make_archetypal_behaviour($preferredbehaviour, question_attempt
$qa) {
150 self
::load_behaviour_class($preferredbehaviour);
151 $class = 'qbehaviour_' . $preferredbehaviour;
152 if (!constant($class . '::IS_ARCHETYPAL')) {
153 throw new coding_exception('The requested behaviour is not actually ' .
154 'an archetypal one.');
156 return new $class($qa, $preferredbehaviour);
160 * @param string $behaviour the name of a behaviour.
161 * @return array of {@link question_display_options} field names, that are
162 * not relevant to this behaviour before a 'finish' action.
164 public static function get_behaviour_unused_display_options($behaviour) {
165 self
::load_behaviour_class($behaviour);
166 $class = 'qbehaviour_' . $behaviour;
167 if (!method_exists($class, 'get_unused_display_options')) {
168 return question_behaviour
::get_unused_display_options();
170 return call_user_func(array($class, 'get_unused_display_options'));
174 * Create an behaviour for a particular type. If that type cannot be
175 * found, return an instance of qbehaviour_missing.
177 * Normally you should use {@link make_archetypal_behaviour()}, or
178 * call the constructor of a particular model class directly. This method
179 * is only intended for use by {@link question_attempt::load_from_records()}.
181 * @param string $behaviour the type of model to create.
182 * @param question_attempt $qa the question attempt the model will process.
183 * @param string $preferredbehaviour the preferred behaviour for the containing usage.
184 * @return question_behaviour an instance of appropriate behaviour class.
186 public static function make_behaviour($behaviour, question_attempt
$qa, $preferredbehaviour) {
188 self
::load_behaviour_class($behaviour);
189 } catch (Exception
$e) {
190 self
::load_behaviour_class('missing');
191 return new qbehaviour_missing($qa, $preferredbehaviour);
193 $class = 'qbehaviour_' . $behaviour;
194 return new $class($qa, $preferredbehaviour);
198 * Load the behaviour class(es) belonging to a particular model. That is,
199 * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
201 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
203 public static function load_behaviour_class($behaviour) {
205 if (isset(self
::$loadedbehaviours[$behaviour])) {
208 $file = $CFG->dirroot
. '/question/behaviour/' . $behaviour . '/behaviour.php';
209 if (!is_readable($file)) {
210 throw new coding_exception('Unknown question behaviour ' . $behaviour);
213 self
::$loadedbehaviours[$behaviour] = 1;
217 * Return an array where the keys are the internal names of the archetypal
218 * behaviours, and the values are a human-readable name. An
219 * archetypal behaviour is one that is suitable to pass the name of to
220 * {@link question_usage_by_activity::set_preferred_behaviour()}.
222 * @return array model name => lang string for this behaviour name.
224 public static function get_archetypal_behaviours() {
225 $archetypes = array();
226 $behaviours = get_plugin_list('qbehaviour');
227 foreach ($behaviours as $behaviour => $notused) {
228 if (self
::is_behaviour_archetypal($behaviour)) {
229 $archetypes[$behaviour] = self
::get_behaviour_name($behaviour);
232 asort($archetypes, SORT_LOCALE_STRING
);
237 * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
238 * @return bool whether this is an archetypal behaviour.
240 public static function is_behaviour_archetypal($behaviour) {
241 self
::load_behaviour_class($behaviour);
242 $plugin = 'qbehaviour_' . $behaviour;
243 return constant($plugin . '::IS_ARCHETYPAL');
247 * Return an array where the keys are the internal names of the behaviours
248 * in preferred order and the values are a human-readable name.
250 * @param array $archetypes, array of behaviours
251 * @param string $orderlist, a comma separated list of behaviour names
252 * @param string $disabledlist, a comma separated list of behaviour names
253 * @param string $current, current behaviour name
254 * @return array model name => lang string for this behaviour name.
256 public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
258 // Get disabled behaviours
260 $disabled = explode(',', $disabledlist);
266 $order = explode(',', $orderlist);
271 foreach ($disabled as $behaviour) {
272 if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
273 unset($archetypes[$behaviour]);
277 // Get behaviours in preferred order
278 $behaviourorder = array();
279 foreach ($order as $behaviour) {
280 if (array_key_exists($behaviour, $archetypes)) {
281 $behaviourorder[$behaviour] = $archetypes[$behaviour];
284 // Get the rest of behaviours and sort them alphabetically
285 $leftover = array_diff_key($archetypes, $behaviourorder);
286 asort($leftover, SORT_LOCALE_STRING
);
288 // Set up the final order to be displayed
289 return $behaviourorder +
$leftover;
293 * Return an array where the keys are the internal names of the behaviours
294 * in preferred order and the values are a human-readable name.
296 * @param string $currentbehaviour
297 * @return array model name => lang string for this behaviour name.
299 public static function get_behaviour_options($currentbehaviour) {
300 $config = question_bank
::get_config();
301 $archetypes = self
::get_archetypal_behaviours();
303 // If no admin setting return all behavious
304 if (empty($config->disabledbehaviours
) && empty($config->behavioursortorder
)) {
308 if (empty($config->behavioursortorder
)) {
311 $order = $config->behavioursortorder
;
313 if (empty($config->disabledbehaviours
)) {
316 $disabled = $config->disabledbehaviours
;
319 return self
::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
323 * Get the translated name of an behaviour, for display in the UI.
324 * @param string $behaviour the internal name of the model.
325 * @return string name from the current language pack.
327 public static function get_behaviour_name($behaviour) {
328 return get_string('pluginname', 'qbehaviour_' . $behaviour);
332 * @return array all the file area names that may contain response files.
334 public static function get_all_response_file_areas() {
335 $variables = array();
336 foreach (question_bank
::get_all_qtypes() as $qtype) {
337 $variables +
= $qtype->response_file_areas();
341 foreach (array_unique($variables) as $variable) {
342 $areas[] = 'response_' . $variable;
348 * Returns the valid choices for the number of decimal places for showing
349 * question marks. For use in the user interface.
350 * @return array suitable for passing to {@link choose_from_menu()} or similar.
352 public static function get_dp_options() {
353 return question_display_options
::get_dp_options();
357 * Initialise the JavaScript required on pages where questions will be displayed.
359 public static function initialise_js() {
360 return question_flags
::initialise_js();
366 * This class contains all the options that controls how a question is displayed.
368 * Normally, what will happen is that the calling code will set up some display
369 * options to indicate what sort of question display it wants, and then before the
370 * question is rendered, the behaviour will be given a chance to modify the
371 * display options, so that, for example, A question that is finished will only
372 * be shown read-only, and a question that has not been submitted will not have
373 * any sort of feedback displayed.
375 * @copyright 2009 The Open University
376 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
378 class question_display_options
{
379 /**#@+ @var integer named constants for the values that most of the options take. */
385 /**#@+ @var integer named constants for the {@link $marks} option. */
387 const MARK_AND_MAX
= 2;
391 * @var integer maximum value for the {@link $markpd} option. This is
392 * effectively set by the database structure, which uses NUMBER(12,7) columns
393 * for question marks/fractions.
398 * @var boolean whether the question should be displayed as a read-only review,
399 * or in an active state where you can change the answer.
401 public $readonly = false;
404 * @var boolean whether the question type should output hidden form fields
405 * to reset any incorrect parts of the resonse to blank.
407 public $clearwrong = false;
410 * Should the student have what they got right and wrong clearly indicated.
411 * This includes the green/red hilighting of the bits of their response,
412 * whether the one-line summary of the current state of the question says
413 * correct/incorrect or just answered.
414 * @var integer {@link question_display_options::HIDDEN} or
415 * {@link question_display_options::VISIBLE}
417 public $correctness = self
::VISIBLE
;
420 * The the mark and/or the maximum available mark for this question be visible?
421 * @var integer {@link question_display_options::HIDDEN},
422 * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
424 public $marks = self
::MARK_AND_MAX
;
426 /** @var number of decimal places to use when formatting marks for output. */
430 * Should the flag this question UI element be visible, and if so, should the
431 * flag state be changable?
432 * @var integer {@link question_display_options::HIDDEN},
433 * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
435 public $flags = self
::VISIBLE
;
438 * Should the specific feedback be visible.
439 * @var integer {@link question_display_options::HIDDEN} or
440 * {@link question_display_options::VISIBLE}
442 public $feedback = self
::VISIBLE
;
445 * For questions with a number of sub-parts (like matching, or
446 * multiple-choice, multiple-reponse) display the number of sub-parts that
448 * @var integer {@link question_display_options::HIDDEN} or
449 * {@link question_display_options::VISIBLE}
451 public $numpartscorrect = self
::VISIBLE
;
454 * Should the general feedback be visible?
455 * @var integer {@link question_display_options::HIDDEN} or
456 * {@link question_display_options::VISIBLE}
458 public $generalfeedback = self
::VISIBLE
;
461 * Should the automatically generated display of what the correct answer is
463 * @var integer {@link question_display_options::HIDDEN} or
464 * {@link question_display_options::VISIBLE}
466 public $rightanswer = self
::VISIBLE
;
469 * Should the manually added marker's comment be visible. Should the link for
470 * adding/editing the comment be there.
471 * @var integer {@link question_display_options::HIDDEN},
472 * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
473 * Editable means that form fields are displayed inline.
475 public $manualcomment = self
::VISIBLE
;
478 * Should we show a 'Make comment or override grade' link?
479 * @var string base URL for the edit comment script, which will be shown if
480 * $manualcomment = self::VISIBLE.
482 public $manualcommentlink = null;
485 * Used in places like the question history table, to show a link to review
486 * this question in a certain state. If blank, a link is not shown.
487 * @var string base URL for a review question script.
489 public $questionreviewlink = null;
492 * Should the history of previous question states table be visible?
493 * @var integer {@link question_display_options::HIDDEN} or
494 * {@link question_display_options::VISIBLE}
496 public $history = self
::HIDDEN
;
499 * If not empty, then a link to edit the question will be included in
500 * the info box for the question.
502 * If used, this array must contain an element courseid or cmid.
504 * It shoudl also contain a parameter returnurl => moodle_url giving a
505 * sensible URL to go back to when the editing form is submitted or cancelled.
507 * @var array url parameter for the edit link. id => questiosnid will be
508 * added automatically.
510 public $editquestionparams = array();
513 * @var int the context the attempt being output belongs to.
518 * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
519 * {@link rightanswer} and {@link manualcomment} to
520 * {@link question_display_options::HIDDEN}.
522 public function hide_all_feedback() {
523 $this->feedback
= self
::HIDDEN
;
524 $this->numpartscorrect
= self
::HIDDEN
;
525 $this->generalfeedback
= self
::HIDDEN
;
526 $this->rightanswer
= self
::HIDDEN
;
527 $this->manualcomment
= self
::HIDDEN
;
528 $this->correctness
= self
::HIDDEN
;
532 * Returns the valid choices for the number of decimal places for showing
533 * question marks. For use in the user interface.
535 * Calling code should probably use {@link question_engine::get_dp_options()}
536 * rather than calling this method directly.
538 * @return array suitable for passing to {@link choose_from_menu()} or similar.
540 public static function get_dp_options() {
542 for ($i = 0; $i <= self
::MAX_DP
; $i +
= 1) {
551 * Contains the logic for handling question flags.
553 * @copyright 2010 The Open University
554 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
556 abstract class question_flags
{
558 * Get the checksum that validates that a toggle request is valid.
559 * @param int $qubaid the question usage id.
560 * @param int $questionid the question id.
561 * @param int $sessionid the question_attempt id.
562 * @param object $user the user. If null, defaults to $USER.
563 * @return string that needs to be sent to question/toggleflag.php for it to work.
565 protected static function get_toggle_checksum($qubaid, $questionid,
566 $qaid, $slot, $user = null) {
567 if (is_null($user)) {
571 return md5($qubaid . "_" . $user->secret
. "_" . $questionid . "_" . $qaid . "_" . $slot);
575 * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
576 * You need to append &newstate=0/1 to this.
577 * @return the post data to send.
579 public static function get_postdata(question_attempt
$qa) {
580 $qaid = $qa->get_database_id();
581 $qubaid = $qa->get_usage_id();
582 $qid = $qa->get_question()->id
;
583 $slot = $qa->get_slot();
584 $checksum = self
::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
585 return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" .
586 sesskey() . '&newstate=';
590 * If the request seems valid, update the flag state of a question attempt.
591 * Throws exceptions if this is not a valid update request.
592 * @param int $qubaid the question usage id.
593 * @param int $questionid the question id.
594 * @param int $sessionid the question_attempt id.
595 * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
596 * corresponding to the last three arguments.
597 * @param bool $newstate the new state of the flag. true = flagged.
599 public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
600 // Check the checksum - it is very hard to know who a question session belongs
601 // to, so we require that checksum parameter is matches an md5 hash of the
602 // three ids and the users username. Since we are only updating a flag, that
603 // probably makes it sufficiently difficult for malicious users to toggle
604 // other users flags.
605 if ($checksum != self
::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
606 throw new moodle_exception('errorsavingflags', 'question');
609 $dm = new question_engine_data_mapper();
610 $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
613 public static function initialise_js() {
614 global $CFG, $PAGE, $OUTPUT;
615 static $done = false;
620 'name' => 'core_question_flags',
621 'fullpath' => '/question/flags.js',
622 'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
624 $actionurl = $CFG->wwwroot
. '/question/toggleflag.php';
626 0 => get_string('clickflag', 'question'),
627 1 => get_string('clickunflag', 'question')
629 $flagattributes = array(
631 'src' => $OUTPUT->pix_url('i/unflagged') . '',
632 'title' => get_string('clicktoflag', 'question'),
633 'alt' => get_string('notflagged', 'question'),
634 // 'text' => get_string('clickflag', 'question'),
637 'src' => $OUTPUT->pix_url('i/flagged') . '',
638 'title' => get_string('clicktounflag', 'question'),
639 'alt' => get_string('flagged', 'question'),
640 // 'text' => get_string('clickunflag', 'question'),
643 $PAGE->requires
->js_init_call('M.core_question_flags.init',
644 array($actionurl, $flagattributes, $flagtext), false, $module);
651 * Exception thrown when the system detects that a student has done something
652 * out-of-order to a question. This can happen, for example, if they click
653 * the browser's back button in a quiz, then try to submit a different response.
655 * @copyright 2010 The Open University
656 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
658 class question_out_of_sequence_exception
extends moodle_exception
{
659 public function __construct($qubaid, $slot, $postdata) {
660 if ($postdata == null) {
661 $postdata = data_submitted();
663 parent
::__construct('submissionoutofsequence', 'question', '', null,
664 "QUBAid: $qubaid, slot: $slot, post data: " . print_r($postdata, true));
670 * Useful functions for writing question types and behaviours.
672 * @copyright 2010 The Open University
673 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
675 abstract class question_utils
{
677 * Tests to see whether two arrays have the same keys, with the same values
678 * (as compared by ===) for each key. However, the order of the arrays does
679 * not have to be the same.
680 * @param array $array1 the first array.
681 * @param array $array2 the second array.
682 * @return bool whether the two arrays have the same keys with the same
683 * corresponding values.
685 public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
686 if (count($array1) != count($array2)) {
689 foreach ($array1 as $key => $value1) {
690 if (!array_key_exists($key, $array2)) {
693 if (((string) $value1) !== ((string) $array2[$key])) {
701 * Tests to see whether two arrays have the same value at a particular key.
702 * This method will return true if:
703 * 1. Neither array contains the key; or
704 * 2. Both arrays contain the key, and the corresponding values compare
705 * identical when cast to strings and compared with ===.
706 * @param array $array1 the first array.
707 * @param array $array2 the second array.
708 * @param string $key an array key.
709 * @return bool whether the two arrays have the same value (or lack of
710 * one) for a given key.
712 public static function arrays_same_at_key(array $array1, array $array2, $key) {
713 if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
714 return ((string) $array1[$key]) === ((string) $array2[$key]);
716 if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
723 * Tests to see whether two arrays have the same value at a particular key.
724 * Missing values are replaced by '', and then the values are cast to
725 * strings and compared with ===.
726 * @param array $array1 the first array.
727 * @param array $array2 the second array.
728 * @param string $key an array key.
729 * @return bool whether the two arrays have the same value (or lack of
730 * one) for a given key.
732 public static function arrays_same_at_key_missing_is_blank(
733 array $array1, array $array2, $key) {
734 if (array_key_exists($key, $array1)) {
735 $value1 = $array1[$key];
739 if (array_key_exists($key, $array2)) {
740 $value2 = $array2[$key];
744 return ((string) $value1) === ((string) $value2);
748 * Tests to see whether two arrays have the same value at a particular key.
749 * Missing values are replaced by 0, and then the values are cast to
750 * integers and compared with ===.
751 * @param array $array1 the first array.
752 * @param array $array2 the second array.
753 * @param string $key an array key.
754 * @return bool whether the two arrays have the same value (or lack of
755 * one) for a given key.
757 public static function arrays_same_at_key_integer(
758 array $array1, array $array2, $key) {
759 if (array_key_exists($key, $array1)) {
760 $value1 = $array1[$key];
764 if (array_key_exists($key, $array2)) {
765 $value2 = $array2[$key];
769 return ((integer) $value1) === ((integer) $value2);
772 private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
773 private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
774 private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
775 private static $thousands = array('', 'm', 'mm', 'mmm');
778 * Convert an integer to roman numerals.
779 * @param int $number an integer between 1 and 3999 inclusive. Anything else
780 * will throw an exception.
781 * @return string the number converted to lower case roman numerals.
783 public static function int_to_roman($number) {
784 if (!is_integer($number) ||
$number < 1 ||
$number > 3999) {
785 throw new coding_exception('Only integers between 0 and 3999 can be ' .
786 'converted to roman numerals.', $number);
789 return self
::$thousands[$number / 1000 %
10] . self
::$hundreds[$number / 100 %
10] .
790 self
::$tens[$number / 10 %
10] . self
::$units[$number %
10];
796 * The interface for strategies for controlling which variant of each question is used.
798 * @copyright 2011 The Open University
799 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
801 interface question_variant_selection_strategy
{
803 * @param int $maxvariants the num
804 * @param string $seed data that can be used to controls how the variant is selected
805 * in a semi-random way.
806 * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
808 public function choose_variant($maxvariants, $seed);
813 * A {@link question_variant_selection_strategy} that is completely random.
815 * @copyright 2011 The Open University
816 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
818 class question_variant_random_strategy
implements question_variant_selection_strategy
{
819 public function choose_variant($maxvariants, $seed) {
820 return rand(1, $maxvariants);
826 * A {@link question_variant_selection_strategy} that is effectively random
827 * for the first attempt, and then after that cycles through the available
828 * variants so that the students will not get a repeated variant until they have
831 * @copyright 2011 The Open University
832 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
834 class question_variant_pseudorandom_no_repeats_strategy
835 implements question_variant_selection_strategy
{
837 /** @var int the number of attempts this users has had, including the curent one. */
838 protected $attemptno;
840 /** @var int the user id the attempt belongs to. */
845 * @param int $attemptno The attempt number.
846 * @param int $userid the user the attempt is for (defaults to $USER->id).
848 public function __construct($attemptno, $userid = null) {
849 $this->attemptno
= $attemptno;
850 if (is_null($userid)) {
852 $this->userid
= $USER->id
;
854 $this->userid
= $userid;
858 public function choose_variant($maxvariants, $seed) {
859 if ($maxvariants == 1) {
863 $hash = sha1($seed . '|user' . $this->userid
);
864 $randint = hexdec(substr($hash, 17, 7));
866 return ($randint +
$this->attemptno
) %
$maxvariants +
1;