MDL-28471 make the question flag smaller, and give it a label.
[moodle.git] / question / engine / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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.
13 //
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/>.
17 /**
18  * This defines the core classes of the Moodle question engine.
19  *
20  * @package    moodlecore
21  * @subpackage questionengine
22  * @copyright  2009 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
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');
44 /**
45  * This static class provides access to the other question engine classes.
46  *
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.
50  *
51  * @copyright  2009 The Open University
52  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53  */
54 abstract class question_engine {
55     /** @var array behaviour name => 1. Records which behaviours have been loaded. */
56     private static $loadedbehaviours = array();
58     /**
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()}.
62      *
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.
66      */
67     public static function make_questions_usage_by_activity($component, $context) {
68         return new question_usage_by_activity($component, $context);
69     }
71     /**
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.
75      */
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);
79     }
81     /**
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.
86      */
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) {
91             $observer->save($dm);
92         } else {
93             $dm->insert_questions_usage_by_activity($quba);
94         }
95     }
97     /**
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.
100      */
101     public static function delete_questions_usage_by_activity($qubaid) {
102         self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
103     }
105     /**
106      * Delete {@link question_usage_by_activity}s from the database.
107      * @param qubaid_condition $qubaids identifies which questions usages to delete.
108      */
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);
112     }
114     /**
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.
120      */
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);
125     }
127     /**
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
131      *      those usages.
132      */
133     public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
134         if (is_null($qubaids)) {
135             return false;
136         }
137         $dm = new question_engine_data_mapper();
138         return $dm->questions_in_use($questionids, $qubaids);
139     }
141     /**
142      * Create an archetypal behaviour for a particular question attempt.
143      * Used by {@link question_definition::make_behaviour()}.
144      *
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.
148      */
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.');
155         }
156         return new $class($qa, $preferredbehaviour);
157     }
159     /**
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.
163      */
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();
169         }
170         return call_user_func(array($class, 'get_unused_display_options'));
171     }
173     /**
174      * Create an behaviour for a particular type. If that type cannot be
175      * found, return an instance of qbehaviour_missing.
176      *
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()}.
180      *
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.
185      */
186     public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
187         try {
188             self::load_behaviour_class($behaviour);
189         } catch (Exception $e) {
190             self::load_behaviour_class('missing');
191             return new qbehaviour_missing($qa, $preferredbehaviour);
192         }
193         $class = 'qbehaviour_' . $behaviour;
194         return new $class($qa, $preferredbehaviour);
195     }
197     /**
198      * Load the behaviour class(es) belonging to a particular model. That is,
199      * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
200      * of checking.
201      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
202      */
203     public static function load_behaviour_class($behaviour) {
204         global $CFG;
205         if (isset(self::$loadedbehaviours[$behaviour])) {
206             return;
207         }
208         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
209         if (!is_readable($file)) {
210             throw new coding_exception('Unknown question behaviour ' . $behaviour);
211         }
212         include_once($file);
213         self::$loadedbehaviours[$behaviour] = 1;
214     }
216     /**
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()}.
221      *
222      * @return array model name => lang string for this behaviour name.
223      */
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);
230             }
231         }
232         asort($archetypes, SORT_LOCALE_STRING);
233         return $archetypes;
234     }
236     /**
237      * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
238      * @return bool whether this is an archetypal behaviour.
239      */
240     public static function is_behaviour_archetypal($behaviour) {
241         self::load_behaviour_class($behaviour);
242         $plugin = 'qbehaviour_' . $behaviour;
243         return constant($plugin . '::IS_ARCHETYPAL');
244     }
246     /**
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.
249      *
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.
255      */
256     public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
258         // Get disabled behaviours
259         if ($disabledlist) {
260             $disabled = explode(',', $disabledlist);
261         } else {
262             $disabled = array();
263         }
265         if ($orderlist) {
266             $order = explode(',', $orderlist);
267         } else {
268             $order = array();
269         }
271         foreach ($disabled as $behaviour) {
272             if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
273                 unset($archetypes[$behaviour]);
274             }
275         }
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];
282             }
283         }
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;
290     }
292     /**
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.
295      *
296      * @param string $currentbehaviour
297      * @return array model name => lang string for this behaviour name.
298      */
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)) {
305             return $archetypes;
306         }
308         if (empty($config->behavioursortorder)) {
309             $order = '';
310         } else {
311             $order = $config->behavioursortorder;
312         }
313         if (empty($config->disabledbehaviours)) {
314             $disabled = '';
315         } else {
316             $disabled = $config->disabledbehaviours;
317         }
319         return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
320     }
322     /**
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.
326      */
327     public static function get_behaviour_name($behaviour) {
328         return get_string('pluginname', 'qbehaviour_' . $behaviour);
329     }
331     /**
332      * Get the translated name of an behaviour, for display in the UI.
333      * @param string $behaviour the internal name of the model.
334      * @return string name from the current language pack.
335      */
336     public static function get_behaviour_required_behaviours($behaviour) {
337         $class = 'qbehaviour_' . $behaviour;
338         return $class::get_required_behaviours();
339     }
341     /**
342      * @return array all the file area names that may contain response files.
343      */
344     public static function get_all_response_file_areas() {
345         $variables = array();
346         foreach (question_bank::get_all_qtypes() as $qtype) {
347             $variables += $qtype->response_file_areas();
348         }
350         $areas = array();
351         foreach (array_unique($variables) as $variable) {
352             $areas[] = 'response_' . $variable;
353         }
354         return $areas;
355     }
357     /**
358      * Returns the valid choices for the number of decimal places for showing
359      * question marks. For use in the user interface.
360      * @return array suitable for passing to {@link choose_from_menu()} or similar.
361      */
362     public static function get_dp_options() {
363         return question_display_options::get_dp_options();
364     }
366     /**
367      * Initialise the JavaScript required on pages where questions will be displayed.
368      */
369     public static function initialise_js() {
370         return question_flags::initialise_js();
371     }
375 /**
376  * This class contains all the options that controls how a question is displayed.
377  *
378  * Normally, what will happen is that the calling code will set up some display
379  * options to indicate what sort of question display it wants, and then before the
380  * question is rendered, the behaviour will be given a chance to modify the
381  * display options, so that, for example, A question that is finished will only
382  * be shown read-only, and a question that has not been submitted will not have
383  * any sort of feedback displayed.
384  *
385  * @copyright  2009 The Open University
386  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
387  */
388 class question_display_options {
389     /**#@+ @var integer named constants for the values that most of the options take. */
390     const HIDDEN = 0;
391     const VISIBLE = 1;
392     const EDITABLE = 2;
393     /**#@-*/
395     /**#@+ @var integer named constants for the {@link $marks} option. */
396     const MAX_ONLY = 1;
397     const MARK_AND_MAX = 2;
398     /**#@-*/
400     /**
401      * @var integer maximum value for the {@link $markpd} option. This is
402      * effectively set by the database structure, which uses NUMBER(12,7) columns
403      * for question marks/fractions.
404      */
405     const MAX_DP = 7;
407     /**
408      * @var boolean whether the question should be displayed as a read-only review,
409      * or in an active state where you can change the answer.
410      */
411     public $readonly = false;
413     /**
414      * @var boolean whether the question type should output hidden form fields
415      * to reset any incorrect parts of the resonse to blank.
416      */
417     public $clearwrong = false;
419     /**
420      * Should the student have what they got right and wrong clearly indicated.
421      * This includes the green/red hilighting of the bits of their response,
422      * whether the one-line summary of the current state of the question says
423      * correct/incorrect or just answered.
424      * @var integer {@link question_display_options::HIDDEN} or
425      * {@link question_display_options::VISIBLE}
426      */
427     public $correctness = self::VISIBLE;
429     /**
430      * The the mark and/or the maximum available mark for this question be visible?
431      * @var integer {@link question_display_options::HIDDEN},
432      * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
433      */
434     public $marks = self::MARK_AND_MAX;
436     /** @var number of decimal places to use when formatting marks for output. */
437     public $markdp = 2;
439     /**
440      * Should the flag this question UI element be visible, and if so, should the
441      * flag state be changable?
442      * @var integer {@link question_display_options::HIDDEN},
443      * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
444      */
445     public $flags = self::VISIBLE;
447     /**
448      * Should the specific feedback be visible.
449      * @var integer {@link question_display_options::HIDDEN} or
450      * {@link question_display_options::VISIBLE}
451      */
452     public $feedback = self::VISIBLE;
454     /**
455      * For questions with a number of sub-parts (like matching, or
456      * multiple-choice, multiple-reponse) display the number of sub-parts that
457      * were correct.
458      * @var integer {@link question_display_options::HIDDEN} or
459      * {@link question_display_options::VISIBLE}
460      */
461     public $numpartscorrect = self::VISIBLE;
463     /**
464      * Should the general feedback be visible?
465      * @var integer {@link question_display_options::HIDDEN} or
466      * {@link question_display_options::VISIBLE}
467      */
468     public $generalfeedback = self::VISIBLE;
470     /**
471      * Should the automatically generated display of what the correct answer is
472      * be visible?
473      * @var integer {@link question_display_options::HIDDEN} or
474      * {@link question_display_options::VISIBLE}
475      */
476     public $rightanswer = self::VISIBLE;
478     /**
479      * Should the manually added marker's comment be visible. Should the link for
480      * adding/editing the comment be there.
481      * @var integer {@link question_display_options::HIDDEN},
482      * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
483      * Editable means that form fields are displayed inline.
484      */
485     public $manualcomment = self::VISIBLE;
487     /**
488      * Should we show a 'Make comment or override grade' link?
489      * @var string base URL for the edit comment script, which will be shown if
490      * $manualcomment = self::VISIBLE.
491      */
492     public $manualcommentlink = null;
494     /**
495      * Used in places like the question history table, to show a link to review
496      * this question in a certain state. If blank, a link is not shown.
497      * @var string base URL for a review question script.
498      */
499     public $questionreviewlink = null;
501     /**
502      * Should the history of previous question states table be visible?
503      * @var integer {@link question_display_options::HIDDEN} or
504      * {@link question_display_options::VISIBLE}
505      */
506     public $history = self::HIDDEN;
508     /**
509      * If not empty, then a link to edit the question will be included in
510      * the info box for the question.
511      *
512      * If used, this array must contain an element courseid or cmid.
513      *
514      * It shoudl also contain a parameter returnurl => moodle_url giving a
515      * sensible URL to go back to when the editing form is submitted or cancelled.
516      *
517      * @var array url parameter for the edit link. id => questiosnid will be
518      * added automatically.
519      */
520     public $editquestionparams = array();
522     /**
523      * @var int the context the attempt being output belongs to.
524      */
525     public $context;
527     /**
528      * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
529      * {@link rightanswer} and {@link manualcomment} to
530      * {@link question_display_options::HIDDEN}.
531      */
532     public function hide_all_feedback() {
533         $this->feedback = self::HIDDEN;
534         $this->numpartscorrect = self::HIDDEN;
535         $this->generalfeedback = self::HIDDEN;
536         $this->rightanswer = self::HIDDEN;
537         $this->manualcomment = self::HIDDEN;
538         $this->correctness = self::HIDDEN;
539     }
541     /**
542      * Returns the valid choices for the number of decimal places for showing
543      * question marks. For use in the user interface.
544      *
545      * Calling code should probably use {@link question_engine::get_dp_options()}
546      * rather than calling this method directly.
547      *
548      * @return array suitable for passing to {@link choose_from_menu()} or similar.
549      */
550     public static function get_dp_options() {
551         $options = array();
552         for ($i = 0; $i <= self::MAX_DP; $i += 1) {
553             $options[$i] = $i;
554         }
555         return $options;
556     }
560 /**
561  * Contains the logic for handling question flags.
562  *
563  * @copyright  2010 The Open University
564  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
565  */
566 abstract class question_flags {
567     /**
568      * Get the checksum that validates that a toggle request is valid.
569      * @param int $qubaid the question usage id.
570      * @param int $questionid the question id.
571      * @param int $sessionid the question_attempt id.
572      * @param object $user the user. If null, defaults to $USER.
573      * @return string that needs to be sent to question/toggleflag.php for it to work.
574      */
575     protected static function get_toggle_checksum($qubaid, $questionid,
576             $qaid, $slot, $user = null) {
577         if (is_null($user)) {
578             global $USER;
579             $user = $USER;
580         }
581         return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
582     }
584     /**
585      * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
586      * You need to append &newstate=0/1 to this.
587      * @return the post data to send.
588      */
589     public static function get_postdata(question_attempt $qa) {
590         $qaid = $qa->get_database_id();
591         $qubaid = $qa->get_usage_id();
592         $qid = $qa->get_question()->id;
593         $slot = $qa->get_slot();
594         $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
595         return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" .
596                 sesskey() . '&newstate=';
597     }
599     /**
600      * If the request seems valid, update the flag state of a question attempt.
601      * Throws exceptions if this is not a valid update request.
602      * @param int $qubaid the question usage id.
603      * @param int $questionid the question id.
604      * @param int $sessionid the question_attempt id.
605      * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
606      *      corresponding to the last three arguments.
607      * @param bool $newstate the new state of the flag. true = flagged.
608      */
609     public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
610         // Check the checksum - it is very hard to know who a question session belongs
611         // to, so we require that checksum parameter is matches an md5 hash of the
612         // three ids and the users username. Since we are only updating a flag, that
613         // probably makes it sufficiently difficult for malicious users to toggle
614         // other users flags.
615         if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
616             throw new moodle_exception('errorsavingflags', 'question');
617         }
619         $dm = new question_engine_data_mapper();
620         $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
621     }
623     public static function initialise_js() {
624         global $CFG, $PAGE, $OUTPUT;
625         static $done = false;
626         if ($done) {
627             return;
628         }
629         $module = array(
630             'name' => 'core_question_flags',
631             'fullpath' => '/question/flags.js',
632             'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
633         );
634         $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
635         $fltext = array(0 => get_string('clickflag', 'question'),
636                         1 => get_string('clickunflag', 'question'));
637         $flagattributes = array(
638             0 => array(
639                 'src' => $OUTPUT->pix_url('i/unflagged') . '',
640                 'title' => get_string('clicktoflag', 'question'),
641                 'alt' => get_string('notflagged', 'question'),
642               //  'text' => get_string('clickflag', 'question'),
643             ),
644             1 => array(
645                 'src' => $OUTPUT->pix_url('i/flagged') . '',
646                 'title' => get_string('clicktounflag', 'question'),
647                 'alt' => get_string('flagged', 'question'),
648                // 'text' => get_string('clickunflag', 'question'),
649             ),
650         );
651         $PAGE->requires->js_init_call('M.core_question_flags.init',
652                 array($actionurl, $flagattributes, $fltext), false, $module);
653         $done = true;
654     }
658 /**
659  * Exception thrown when the system detects that a student has done something
660  * out-of-order to a question. This can happen, for example, if they click
661  * the browser's back button in a quiz, then try to submit a different response.
662  *
663  * @copyright  2010 The Open University
664  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
665  */
666 class question_out_of_sequence_exception extends moodle_exception {
667     public function __construct($qubaid, $slot, $postdata) {
668         if ($postdata == null) {
669             $postdata = data_submitted();
670         }
671         parent::__construct('submissionoutofsequence', 'question', '', null,
672                 "QUBAid: $qubaid, slot: $slot, post data: " . print_r($postdata, true));
673     }
677 /**
678  * Useful functions for writing question types and behaviours.
679  *
680  * @copyright 2010 The Open University
681  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
682  */
683 abstract class question_utils {
684     /**
685      * Tests to see whether two arrays have the same keys, with the same values
686      * (as compared by ===) for each key. However, the order of the arrays does
687      * not have to be the same.
688      * @param array $array1 the first array.
689      * @param array $array2 the second array.
690      * @return bool whether the two arrays have the same keys with the same
691      *      corresponding values.
692      */
693     public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
694         if (count($array1) != count($array2)) {
695             return false;
696         }
697         foreach ($array1 as $key => $value1) {
698             if (!array_key_exists($key, $array2)) {
699                 return false;
700             }
701             if (((string) $value1) !== ((string) $array2[$key])) {
702                 return false;
703             }
704         }
705         return true;
706     }
708     /**
709      * Tests to see whether two arrays have the same value at a particular key.
710      * This method will return true if:
711      * 1. Neither array contains the key; or
712      * 2. Both arrays contain the key, and the corresponding values compare
713      *      identical when cast to strings and compared with ===.
714      * @param array $array1 the first array.
715      * @param array $array2 the second array.
716      * @param string $key an array key.
717      * @return bool whether the two arrays have the same value (or lack of
718      *      one) for a given key.
719      */
720     public static function arrays_same_at_key(array $array1, array $array2, $key) {
721         if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
722             return ((string) $array1[$key]) === ((string) $array2[$key]);
723         }
724         if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
725             return true;
726         }
727         return false;
728     }
730     /**
731      * Tests to see whether two arrays have the same value at a particular key.
732      * Missing values are replaced by '', and then the values are cast to
733      * strings and compared with ===.
734      * @param array $array1 the first array.
735      * @param array $array2 the second array.
736      * @param string $key an array key.
737      * @return bool whether the two arrays have the same value (or lack of
738      *      one) for a given key.
739      */
740     public static function arrays_same_at_key_missing_is_blank(
741             array $array1, array $array2, $key) {
742         if (array_key_exists($key, $array1)) {
743             $value1 = $array1[$key];
744         } else {
745             $value1 = '';
746         }
747         if (array_key_exists($key, $array2)) {
748             $value2 = $array2[$key];
749         } else {
750             $value2 = '';
751         }
752         return ((string) $value1) === ((string) $value2);
753     }
755     /**
756      * Tests to see whether two arrays have the same value at a particular key.
757      * Missing values are replaced by 0, and then the values are cast to
758      * integers and compared with ===.
759      * @param array $array1 the first array.
760      * @param array $array2 the second array.
761      * @param string $key an array key.
762      * @return bool whether the two arrays have the same value (or lack of
763      *      one) for a given key.
764      */
765     public static function arrays_same_at_key_integer(
766             array $array1, array $array2, $key) {
767         if (array_key_exists($key, $array1)) {
768             $value1 = $array1[$key];
769         } else {
770             $value1 = 0;
771         }
772         if (array_key_exists($key, $array2)) {
773             $value2 = $array2[$key];
774         } else {
775             $value2 = 0;
776         }
777         return ((integer) $value1) === ((integer) $value2);
778     }
780     private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
781     private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
782     private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
783     private static $thousands = array('', 'm', 'mm', 'mmm');
785     /**
786      * Convert an integer to roman numerals.
787      * @param int $number an integer between 1 and 3999 inclusive. Anything else
788      *      will throw an exception.
789      * @return string the number converted to lower case roman numerals.
790      */
791     public static function int_to_roman($number) {
792         if (!is_integer($number) || $number < 1 || $number > 3999) {
793             throw new coding_exception('Only integers between 0 and 3999 can be ' .
794                     'converted to roman numerals.', $number);
795         }
797         return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
798                 self::$tens[$number / 10 % 10] . self::$units[$number % 10];
799     }
803 /**
804  * The interface for strategies for controlling which variant of each question is used.
805  *
806  * @copyright  2011 The Open University
807  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
808  */
809 interface question_variant_selection_strategy {
810     /**
811      * @param int $maxvariants the num
812      * @param string $seed data that can be used to controls how the variant is selected
813      *      in a semi-random way.
814      * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
815      */
816     public function choose_variant($maxvariants, $seed);
820 /**
821  * A {@link question_variant_selection_strategy} that is completely random.
822  *
823  * @copyright  2011 The Open University
824  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
825  */
826 class question_variant_random_strategy implements question_variant_selection_strategy {
827     public function choose_variant($maxvariants, $seed) {
828         return rand(1, $maxvariants);
829     }
833 /**
834  * A {@link question_variant_selection_strategy} that is effectively random
835  * for the first attempt, and then after that cycles through the available
836  * variants so that the students will not get a repeated variant until they have
837  * seen them all.
838  *
839  * @copyright  2011 The Open University
840  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
841  */
842 class question_variant_pseudorandom_no_repeats_strategy
843         implements question_variant_selection_strategy {
845     /** @var int the number of attempts this users has had, including the curent one. */
846     protected $attemptno;
848     /** @var int the user id the attempt belongs to. */
849     protected $userid;
851     /**
852      * Constructor.
853      * @param int $attemptno The attempt number.
854      * @param int $userid the user the attempt is for (defaults to $USER->id).
855      */
856     public function __construct($attemptno, $userid = null) {
857         $this->attemptno = $attemptno;
858         if (is_null($userid)) {
859             global $USER;
860             $this->userid = $USER->id;
861         } else {
862             $this->userid = $userid;
863         }
864     }
866     public function choose_variant($maxvariants, $seed) {
867         if ($maxvariants == 1) {
868             return 1;
869         }
871         $hash = sha1($seed . '|user' . $this->userid);
872         $randint = hexdec(substr($hash, 17, 7));
874         return ($randint + $this->attemptno) % $maxvariants + 1;
875     }