Merge branch 'MDL-70326-MOODLE_310_STABLE' of https://github.com/durzo/moodle into...
[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($CFG->libdir . '/filelib.php');
30 require_once(__DIR__ . '/questionusage.php');
31 require_once(__DIR__ . '/questionattempt.php');
32 require_once(__DIR__ . '/questionattemptstep.php');
33 require_once(__DIR__ . '/states.php');
34 require_once(__DIR__ . '/datalib.php');
35 require_once(__DIR__ . '/renderer.php');
36 require_once(__DIR__ . '/bank.php');
37 require_once(__DIR__ . '/../type/questiontypebase.php');
38 require_once(__DIR__ . '/../type/questionbase.php');
39 require_once(__DIR__ . '/../type/rendererbase.php');
40 require_once(__DIR__ . '/../behaviour/behaviourtypebase.php');
41 require_once(__DIR__ . '/../behaviour/behaviourbase.php');
42 require_once(__DIR__ . '/../behaviour/rendererbase.php');
43 require_once($CFG->libdir . '/questionlib.php');
46 /**
47  * This static class provides access to the other question engine classes.
48  *
49  * It provides functions for managing question behaviours), and for
50  * creating, loading, saving and deleting {@link question_usage_by_activity}s,
51  * which is the main class that is used by other code that wants to use questions.
52  *
53  * @copyright  2009 The Open University
54  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55  */
56 abstract class question_engine {
57     /** @var array behaviour name => 1. Records which behaviours have been loaded. */
58     private static $loadedbehaviours = array();
60     /** @var array behaviour name => question_behaviour_type for this behaviour. */
61     private static $behaviourtypes = array();
63     /**
64      * Create a new {@link question_usage_by_activity}. The usage is
65      * created in memory. If you want it to persist, you will need to call
66      * {@link save_questions_usage_by_activity()}.
67      *
68      * @param string $component the plugin creating this attempt. For example mod_quiz.
69      * @param object $context the context this usage belongs to.
70      * @return question_usage_by_activity the newly created object.
71      */
72     public static function make_questions_usage_by_activity($component, $context) {
73         return new question_usage_by_activity($component, $context);
74     }
76     /**
77      * Load a {@link question_usage_by_activity} from the database, based on its id.
78      * @param int $qubaid the id of the usage to load.
79      * @param moodle_database $db a database connectoin. Defaults to global $DB.
80      * @return question_usage_by_activity loaded from the database.
81      */
82     public static function load_questions_usage_by_activity($qubaid, moodle_database $db = null) {
83         $dm = new question_engine_data_mapper($db);
84         return $dm->load_questions_usage_by_activity($qubaid);
85     }
87     /**
88      * Save a {@link question_usage_by_activity} to the database. This works either
89      * if the usage was newly created by {@link make_questions_usage_by_activity()}
90      * or loaded from the database using {@link load_questions_usage_by_activity()}
91      * @param question_usage_by_activity the usage to save.
92      * @param moodle_database $db a database connectoin. Defaults to global $DB.
93      */
94     public static function save_questions_usage_by_activity(question_usage_by_activity $quba, moodle_database $db = null) {
95         $dm = new question_engine_data_mapper($db);
96         $observer = $quba->get_observer();
97         if ($observer instanceof question_engine_unit_of_work) {
98             $observer->save($dm);
99         } else {
100             $dm->insert_questions_usage_by_activity($quba);
101         }
102     }
104     /**
105      * Delete a {@link question_usage_by_activity} from the database, based on its id.
106      * @param int $qubaid the id of the usage to delete.
107      */
108     public static function delete_questions_usage_by_activity($qubaid) {
109         self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
110     }
112     /**
113      * Delete {@link question_usage_by_activity}s from the database.
114      * @param qubaid_condition $qubaids identifies which questions usages to delete.
115      */
116     public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
117         $dm = new question_engine_data_mapper();
118         $dm->delete_questions_usage_by_activities($qubaids);
119     }
121     /**
122      * Change the maxmark for the question_attempt with number in usage $slot
123      * for all the specified question_attempts.
124      * @param qubaid_condition $qubaids Selects which usages are updated.
125      * @param int $slot the number is usage to affect.
126      * @param number $newmaxmark the new max mark to set.
127      */
128     public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
129             $slot, $newmaxmark) {
130         $dm = new question_engine_data_mapper();
131         $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
132     }
134     /**
135      * Validate that the manual grade submitted for a particular question is in range.
136      * @param int $qubaid the question_usage id.
137      * @param int $slot the slot number within the usage.
138      * @return bool whether the submitted data is in range.
139      */
140     public static function is_manual_grade_in_range($qubaid, $slot) {
141         $prefix = 'q' . $qubaid . ':' . $slot . '_';
142         $mark = question_utils::optional_param_mark($prefix . '-mark');
143         $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
144         $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
145         $maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);
146         return $mark === '' ||
147                 ($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark) ||
148                 ($mark === null && $maxmark === null);
149     }
151     /**
152      * @param array $questionids of question ids.
153      * @param qubaid_condition $qubaids ids of the usages to consider.
154      * @return boolean whether any of these questions are being used by any of
155      *      those usages.
156      */
157     public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
158         if (is_null($qubaids)) {
159             return false;
160         }
161         $dm = new question_engine_data_mapper();
162         return $dm->questions_in_use($questionids, $qubaids);
163     }
165     /**
166      * Get the number of times each variant has been used for each question in a list
167      * in a set of usages.
168      * @param array $questionids of question ids.
169      * @param qubaid_condition $qubaids ids of the usages to consider.
170      * @return array questionid => variant number => num uses.
171      */
172     public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {
173         $dm = new question_engine_data_mapper();
174         return $dm->load_used_variants($questionids, $qubaids);
175     }
177     /**
178      * Create an archetypal behaviour for a particular question attempt.
179      * Used by {@link question_definition::make_behaviour()}.
180      *
181      * @param string $preferredbehaviour the type of model required.
182      * @param question_attempt $qa the question attempt the model will process.
183      * @return question_behaviour an instance of appropriate behaviour class.
184      */
185     public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
186         if (!self::is_behaviour_archetypal($preferredbehaviour)) {
187             throw new coding_exception('The requested behaviour is not actually ' .
188                     'an archetypal one.');
189         }
191         self::load_behaviour_class($preferredbehaviour);
192         $class = 'qbehaviour_' . $preferredbehaviour;
193         return new $class($qa, $preferredbehaviour);
194     }
196     /**
197      * @param string $behaviour the name of a behaviour.
198      * @return array of {@link question_display_options} field names, that are
199      * not relevant to this behaviour before a 'finish' action.
200      */
201     public static function get_behaviour_unused_display_options($behaviour) {
202         return self::get_behaviour_type($behaviour)->get_unused_display_options();
203     }
205     /**
206      * With this behaviour, is it possible that a question might finish as the student
207      * interacts with it, without a call to the {@link question_attempt::finish()} method?
208      * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
209      * @return bool whether with this behaviour, questions may finish naturally.
210      */
211     public static function can_questions_finish_during_the_attempt($behaviour) {
212         return self::get_behaviour_type($behaviour)->can_questions_finish_during_the_attempt();
213     }
215     /**
216      * Create a behaviour for a particular type. If that type cannot be
217      * found, return an instance of qbehaviour_missing.
218      *
219      * Normally you should use {@link make_archetypal_behaviour()}, or
220      * call the constructor of a particular model class directly. This method
221      * is only intended for use by {@link question_attempt::load_from_records()}.
222      *
223      * @param string $behaviour the type of model to create.
224      * @param question_attempt $qa the question attempt the model will process.
225      * @param string $preferredbehaviour the preferred behaviour for the containing usage.
226      * @return question_behaviour an instance of appropriate behaviour class.
227      */
228     public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
229         try {
230             self::load_behaviour_class($behaviour);
231         } catch (Exception $e) {
232             self::load_behaviour_class('missing');
233             return new qbehaviour_missing($qa, $preferredbehaviour);
234         }
235         $class = 'qbehaviour_' . $behaviour;
236         return new $class($qa, $preferredbehaviour);
237     }
239     /**
240      * Load the behaviour class(es) belonging to a particular model. That is,
241      * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
242      * of checking.
243      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
244      */
245     public static function load_behaviour_class($behaviour) {
246         global $CFG;
247         if (isset(self::$loadedbehaviours[$behaviour])) {
248             return;
249         }
250         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
251         if (!is_readable($file)) {
252             throw new coding_exception('Unknown question behaviour ' . $behaviour);
253         }
254         include_once($file);
256         $class = 'qbehaviour_' . $behaviour;
257         if (!class_exists($class)) {
258             throw new coding_exception('Question behaviour ' . $behaviour .
259                     ' does not define the required class ' . $class . '.');
260         }
262         self::$loadedbehaviours[$behaviour] = 1;
263     }
265     /**
266      * Create a behaviour for a particular type. If that type cannot be
267      * found, return an instance of qbehaviour_missing.
268      *
269      * Normally you should use {@link make_archetypal_behaviour()}, or
270      * call the constructor of a particular model class directly. This method
271      * is only intended for use by {@link question_attempt::load_from_records()}.
272      *
273      * @param string $behaviour the type of model to create.
274      * @param question_attempt $qa the question attempt the model will process.
275      * @param string $preferredbehaviour the preferred behaviour for the containing usage.
276      * @return question_behaviour_type an instance of appropriate behaviour class.
277      */
278     public static function get_behaviour_type($behaviour) {
280         if (array_key_exists($behaviour, self::$behaviourtypes)) {
281             return self::$behaviourtypes[$behaviour];
282         }
284         self::load_behaviour_type_class($behaviour);
286         $class = 'qbehaviour_' . $behaviour . '_type';
287         if (class_exists($class)) {
288             self::$behaviourtypes[$behaviour] = new $class();
289         } else {
290             debugging('Question behaviour ' . $behaviour .
291                     ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);
292             self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);
293         }
295         return self::$behaviourtypes[$behaviour];
296     }
298     /**
299      * Load the behaviour type class for a particular behaviour. That is,
300      * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').
301      * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.
302      */
303     protected static function load_behaviour_type_class($behaviour) {
304         global $CFG;
305         if (isset(self::$behaviourtypes[$behaviour])) {
306             return;
307         }
308         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';
309         if (!is_readable($file)) {
310             debugging('Question behaviour ' . $behaviour .
311                     ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);
312         }
313         include_once($file);
314     }
316     /**
317      * Return an array where the keys are the internal names of the archetypal
318      * behaviours, and the values are a human-readable name. An
319      * archetypal behaviour is one that is suitable to pass the name of to
320      * {@link question_usage_by_activity::set_preferred_behaviour()}.
321      *
322      * @return array model name => lang string for this behaviour name.
323      */
324     public static function get_archetypal_behaviours() {
325         $archetypes = array();
326         $behaviours = core_component::get_plugin_list('qbehaviour');
327         foreach ($behaviours as $behaviour => $notused) {
328             if (self::is_behaviour_archetypal($behaviour)) {
329                 $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
330             }
331         }
332         asort($archetypes, SORT_LOCALE_STRING);
333         return $archetypes;
334     }
336     /**
337      * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
338      * @return bool whether this is an archetypal behaviour.
339      */
340     public static function is_behaviour_archetypal($behaviour) {
341         return self::get_behaviour_type($behaviour)->is_archetypal();
342     }
344     /**
345      * Return an array where the keys are the internal names of the behaviours
346      * in preferred order and the values are a human-readable name.
347      *
348      * @param array $archetypes, array of behaviours
349      * @param string $orderlist, a comma separated list of behaviour names
350      * @param string $disabledlist, a comma separated list of behaviour names
351      * @param string $current, current behaviour name
352      * @return array model name => lang string for this behaviour name.
353      */
354     public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
356         // Get disabled behaviours
357         if ($disabledlist) {
358             $disabled = explode(',', $disabledlist);
359         } else {
360             $disabled = array();
361         }
363         if ($orderlist) {
364             $order = explode(',', $orderlist);
365         } else {
366             $order = array();
367         }
369         foreach ($disabled as $behaviour) {
370             if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
371                 unset($archetypes[$behaviour]);
372             }
373         }
375         // Get behaviours in preferred order
376         $behaviourorder = array();
377         foreach ($order as $behaviour) {
378             if (array_key_exists($behaviour, $archetypes)) {
379                 $behaviourorder[$behaviour] = $archetypes[$behaviour];
380             }
381         }
382         // Get the rest of behaviours and sort them alphabetically
383         $leftover = array_diff_key($archetypes, $behaviourorder);
384         asort($leftover, SORT_LOCALE_STRING);
386         // Set up the final order to be displayed
387         return $behaviourorder + $leftover;
388     }
390     /**
391      * Return an array where the keys are the internal names of the behaviours
392      * in preferred order and the values are a human-readable name.
393      *
394      * @param string $currentbehaviour
395      * @return array model name => lang string for this behaviour name.
396      */
397     public static function get_behaviour_options($currentbehaviour) {
398         $config = question_bank::get_config();
399         $archetypes = self::get_archetypal_behaviours();
401         // If no admin setting return all behavious
402         if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
403             return $archetypes;
404         }
406         if (empty($config->behavioursortorder)) {
407             $order = '';
408         } else {
409             $order = $config->behavioursortorder;
410         }
411         if (empty($config->disabledbehaviours)) {
412             $disabled = '';
413         } else {
414             $disabled = $config->disabledbehaviours;
415         }
417         return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
418     }
420     /**
421      * Get the translated name of a behaviour, for display in the UI.
422      * @param string $behaviour the internal name of the model.
423      * @return string name from the current language pack.
424      */
425     public static function get_behaviour_name($behaviour) {
426         return get_string('pluginname', 'qbehaviour_' . $behaviour);
427     }
429     /**
430      * @return array all the file area names that may contain response files.
431      */
432     public static function get_all_response_file_areas() {
433         $variables = array();
434         foreach (question_bank::get_all_qtypes() as $qtype) {
435             $variables += $qtype->response_file_areas();
436         }
438         $areas = array();
439         foreach (array_unique($variables) as $variable) {
440             $areas[] = 'response_' . $variable;
441         }
442         return $areas;
443     }
445     /**
446      * Returns the valid choices for the number of decimal places for showing
447      * question marks. For use in the user interface.
448      * @return array suitable for passing to {@link html_writer::select()} or similar.
449      */
450     public static function get_dp_options() {
451         return question_display_options::get_dp_options();
452     }
454     /**
455      * Initialise the JavaScript required on pages where questions will be displayed.
456      *
457      * @return string
458      */
459     public static function initialise_js() {
460         return question_flags::initialise_js();
461     }
465 /**
466  * This class contains all the options that controls how a question is displayed.
467  *
468  * Normally, what will happen is that the calling code will set up some display
469  * options to indicate what sort of question display it wants, and then before the
470  * question is rendered, the behaviour will be given a chance to modify the
471  * display options, so that, for example, A question that is finished will only
472  * be shown read-only, and a question that has not been submitted will not have
473  * any sort of feedback displayed.
474  *
475  * @copyright  2009 The Open University
476  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
477  */
478 class question_display_options {
479     /**#@+ @var integer named constants for the values that most of the options take. */
480     const HIDDEN = 0;
481     const VISIBLE = 1;
482     const EDITABLE = 2;
483     /**#@-*/
485     /**#@+ @var integer named constants for the {@link $marks} option. */
486     const MAX_ONLY = 1;
487     const MARK_AND_MAX = 2;
488     /**#@-*/
490     /**
491      * @var integer maximum value for the {@link $markpd} option. This is
492      * effectively set by the database structure, which uses NUMBER(12,7) columns
493      * for question marks/fractions.
494      */
495     const MAX_DP = 7;
497     /**
498      * @var boolean whether the question should be displayed as a read-only review,
499      * or in an active state where you can change the answer.
500      */
501     public $readonly = false;
503     /**
504      * @var boolean whether the question type should output hidden form fields
505      * to reset any incorrect parts of the resonse to blank.
506      */
507     public $clearwrong = false;
509     /**
510      * Should the student have what they got right and wrong clearly indicated.
511      * This includes the green/red hilighting of the bits of their response,
512      * whether the one-line summary of the current state of the question says
513      * correct/incorrect or just answered.
514      * @var integer {@link question_display_options::HIDDEN} or
515      * {@link question_display_options::VISIBLE}
516      */
517     public $correctness = self::VISIBLE;
519     /**
520      * The the mark and/or the maximum available mark for this question be visible?
521      * @var integer {@link question_display_options::HIDDEN},
522      * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
523      */
524     public $marks = self::MARK_AND_MAX;
526     /** @var number of decimal places to use when formatting marks for output. */
527     public $markdp = 2;
529     /**
530      * Should the flag this question UI element be visible, and if so, should the
531      * flag state be changable?
532      * @var integer {@link question_display_options::HIDDEN},
533      * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
534      */
535     public $flags = self::VISIBLE;
537     /**
538      * Should the specific feedback be visible.
539      * @var integer {@link question_display_options::HIDDEN} or
540      * {@link question_display_options::VISIBLE}
541      */
542     public $feedback = self::VISIBLE;
544     /**
545      * For questions with a number of sub-parts (like matching, or
546      * multiple-choice, multiple-reponse) display the number of sub-parts that
547      * were correct.
548      * @var integer {@link question_display_options::HIDDEN} or
549      * {@link question_display_options::VISIBLE}
550      */
551     public $numpartscorrect = self::VISIBLE;
553     /**
554      * Should the general feedback be visible?
555      * @var integer {@link question_display_options::HIDDEN} or
556      * {@link question_display_options::VISIBLE}
557      */
558     public $generalfeedback = self::VISIBLE;
560     /**
561      * Should the automatically generated display of what the correct answer is
562      * be visible?
563      * @var integer {@link question_display_options::HIDDEN} or
564      * {@link question_display_options::VISIBLE}
565      */
566     public $rightanswer = self::VISIBLE;
568     /**
569      * Should the manually added marker's comment be visible. Should the link for
570      * adding/editing the comment be there.
571      * @var integer {@link question_display_options::HIDDEN},
572      * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
573      * Editable means that form fields are displayed inline.
574      */
575     public $manualcomment = self::VISIBLE;
577     /**
578      * Should we show a 'Make comment or override grade' link?
579      * @var string base URL for the edit comment script, which will be shown if
580      * $manualcomment = self::VISIBLE.
581      */
582     public $manualcommentlink = null;
584     /**
585      * Used in places like the question history table, to show a link to review
586      * this question in a certain state. If blank, a link is not shown.
587      * @var moodle_url base URL for a review question script.
588      */
589     public $questionreviewlink = null;
591     /**
592      * Should the history of previous question states table be visible?
593      * @var integer {@link question_display_options::HIDDEN} or
594      * {@link question_display_options::VISIBLE}
595      */
596     public $history = self::HIDDEN;
598     /**
599      * @since 2.9
600      * @var string extra HTML to include at the end of the outcome (feedback) box
601      * of the question display.
602      *
603      * This field is now badly named. The place it included is was changed
604      * (for the better) but the name was left unchanged for backwards compatibility.
605      */
606     public $extrainfocontent = '';
608     /**
609      * @since 2.9
610      * @var string extra HTML to include in the history box of the question display,
611      * if it is shown.
612      */
613     public $extrahistorycontent = '';
615     /**
616      * If not empty, then a link to edit the question will be included in
617      * the info box for the question.
618      *
619      * If used, this array must contain an element courseid or cmid.
620      *
621      * It shoudl also contain a parameter returnurl => moodle_url giving a
622      * sensible URL to go back to when the editing form is submitted or cancelled.
623      *
624      * @var array url parameter for the edit link. id => questiosnid will be
625      * added automatically.
626      */
627     public $editquestionparams = array();
629     /**
630      * @var int the context the attempt being output belongs to.
631      */
632     public $context;
634     /**
635      * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
636      * {@link rightanswer} and {@link manualcomment} to
637      * {@link question_display_options::HIDDEN}.
638      */
639     public function hide_all_feedback() {
640         $this->feedback = self::HIDDEN;
641         $this->numpartscorrect = self::HIDDEN;
642         $this->generalfeedback = self::HIDDEN;
643         $this->rightanswer = self::HIDDEN;
644         $this->manualcomment = self::HIDDEN;
645         $this->correctness = self::HIDDEN;
646     }
648     /**
649      * Returns the valid choices for the number of decimal places for showing
650      * question marks. For use in the user interface.
651      *
652      * Calling code should probably use {@link question_engine::get_dp_options()}
653      * rather than calling this method directly.
654      *
655      * @return array suitable for passing to {@link html_writer::select()} or similar.
656      */
657     public static function get_dp_options() {
658         $options = array();
659         for ($i = 0; $i <= self::MAX_DP; $i += 1) {
660             $options[$i] = $i;
661         }
662         return $options;
663     }
667 /**
668  * Contains the logic for handling question flags.
669  *
670  * @copyright  2010 The Open University
671  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
672  */
673 abstract class question_flags {
674     /**
675      * Get the checksum that validates that a toggle request is valid.
676      * @param int $qubaid the question usage id.
677      * @param int $questionid the question id.
678      * @param int $sessionid the question_attempt id.
679      * @param object $user the user. If null, defaults to $USER.
680      * @return string that needs to be sent to question/toggleflag.php for it to work.
681      */
682     protected static function get_toggle_checksum($qubaid, $questionid,
683             $qaid, $slot, $user = null) {
684         if (is_null($user)) {
685             global $USER;
686             $user = $USER;
687         }
688         return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
689     }
691     /**
692      * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
693      * You need to append &newstate=0/1 to this.
694      * @return the post data to send.
695      */
696     public static function get_postdata(question_attempt $qa) {
697         $qaid = $qa->get_database_id();
698         $qubaid = $qa->get_usage_id();
699         $qid = $qa->get_question_id();
700         $slot = $qa->get_slot();
701         $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
702         return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .
703                 sesskey() . '&newstate=';
704     }
706     /**
707      * If the request seems valid, update the flag state of a question attempt.
708      * Throws exceptions if this is not a valid update request.
709      * @param int $qubaid the question usage id.
710      * @param int $questionid the question id.
711      * @param int $sessionid the question_attempt id.
712      * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
713      *      corresponding to the last three arguments.
714      * @param bool $newstate the new state of the flag. true = flagged.
715      */
716     public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
717         // Check the checksum - it is very hard to know who a question session belongs
718         // to, so we require that checksum parameter is matches an md5 hash of the
719         // three ids and the users username. Since we are only updating a flag, that
720         // probably makes it sufficiently difficult for malicious users to toggle
721         // other users flags.
722         if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
723             throw new moodle_exception('errorsavingflags', 'question');
724         }
726         $dm = new question_engine_data_mapper();
727         $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
728     }
730     public static function initialise_js() {
731         global $CFG, $PAGE, $OUTPUT;
732         static $done = false;
733         if ($done) {
734             return;
735         }
736         $module = array(
737             'name' => 'core_question_flags',
738             'fullpath' => '/question/flags.js',
739             'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
740         );
741         $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
742         $flagtext = array(
743             0 => get_string('clickflag', 'question'),
744             1 => get_string('clickunflag', 'question')
745         );
746         $flagattributes = array(
747             0 => array(
748                 'src' => $OUTPUT->image_url('i/unflagged') . '',
749                 'title' => get_string('clicktoflag', 'question'),
750                 'alt' => get_string('notflagged', 'question'),
751               //  'text' => get_string('clickflag', 'question'),
752             ),
753             1 => array(
754                 'src' => $OUTPUT->image_url('i/flagged') . '',
755                 'title' => get_string('clicktounflag', 'question'),
756                 'alt' => get_string('flagged', 'question'),
757                // 'text' => get_string('clickunflag', 'question'),
758             ),
759         );
760         $PAGE->requires->js_init_call('M.core_question_flags.init',
761                 array($actionurl, $flagattributes, $flagtext), false, $module);
762         $done = true;
763     }
767 /**
768  * Exception thrown when the system detects that a student has done something
769  * out-of-order to a question. This can happen, for example, if they click
770  * the browser's back button in a quiz, then try to submit a different response.
771  *
772  * @copyright  2010 The Open University
773  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
774  */
775 class question_out_of_sequence_exception extends moodle_exception {
776     public function __construct($qubaid, $slot, $postdata) {
777         if ($postdata == null) {
778             $postdata = data_submitted();
779         }
780         parent::__construct('submissionoutofsequence', 'question', '', null,
781                 "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));
782     }
786 /**
787  * Useful functions for writing question types and behaviours.
788  *
789  * @copyright 2010 The Open University
790  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
791  */
792 abstract class question_utils {
793     /**
794      * @var float tolerance to use when comparing question mark/fraction values.
795      *
796      * When comparing floating point numbers in a computer, the representation is not
797      * necessarily exact. Therefore, we need to allow a tolerance.
798      * Question marks are stored in the database as decimal numbers with 7 decimal places.
799      * Therefore, this is the appropriate tolerance to use.
800      */
801     const MARK_TOLERANCE = 0.00000005;
803     /**
804      * Tests to see whether two arrays have the same keys, with the same values
805      * (as compared by ===) for each key. However, the order of the arrays does
806      * not have to be the same.
807      * @param array $array1 the first array.
808      * @param array $array2 the second array.
809      * @return bool whether the two arrays have the same keys with the same
810      *      corresponding values.
811      */
812     public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
813         if (count($array1) != count($array2)) {
814             return false;
815         }
816         foreach ($array1 as $key => $value1) {
817             if (!array_key_exists($key, $array2)) {
818                 return false;
819             }
820             if (((string) $value1) !== ((string) $array2[$key])) {
821                 return false;
822             }
823         }
824         return true;
825     }
827     /**
828      * Tests to see whether two arrays have the same value at a particular key.
829      * This method will return true if:
830      * 1. Neither array contains the key; or
831      * 2. Both arrays contain the key, and the corresponding values compare
832      *      identical when cast to strings and compared with ===.
833      * @param array $array1 the first array.
834      * @param array $array2 the second array.
835      * @param string $key an array key.
836      * @return bool whether the two arrays have the same value (or lack of
837      *      one) for a given key.
838      */
839     public static function arrays_same_at_key(array $array1, array $array2, $key) {
840         if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
841             return ((string) $array1[$key]) === ((string) $array2[$key]);
842         }
843         if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
844             return true;
845         }
846         return false;
847     }
849     /**
850      * Tests to see whether two arrays have the same value at a particular key.
851      * Missing values are replaced by '', and then the values are cast to
852      * strings and compared with ===.
853      * @param array $array1 the first array.
854      * @param array $array2 the second array.
855      * @param string $key an array key.
856      * @return bool whether the two arrays have the same value (or lack of
857      *      one) for a given key.
858      */
859     public static function arrays_same_at_key_missing_is_blank(
860             array $array1, array $array2, $key) {
861         if (array_key_exists($key, $array1)) {
862             $value1 = $array1[$key];
863         } else {
864             $value1 = '';
865         }
866         if (array_key_exists($key, $array2)) {
867             $value2 = $array2[$key];
868         } else {
869             $value2 = '';
870         }
871         return ((string) $value1) === ((string) $value2);
872     }
874     /**
875      * Tests to see whether two arrays have the same value at a particular key.
876      * Missing values are replaced by 0, and then the values are cast to
877      * integers and compared with ===.
878      * @param array $array1 the first array.
879      * @param array $array2 the second array.
880      * @param string $key an array key.
881      * @return bool whether the two arrays have the same value (or lack of
882      *      one) for a given key.
883      */
884     public static function arrays_same_at_key_integer(
885             array $array1, array $array2, $key) {
886         if (array_key_exists($key, $array1)) {
887             $value1 = (int) $array1[$key];
888         } else {
889             $value1 = 0;
890         }
891         if (array_key_exists($key, $array2)) {
892             $value2 = (int) $array2[$key];
893         } else {
894             $value2 = 0;
895         }
896         return $value1 === $value2;
897     }
899     private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
900     private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
901     private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
902     private static $thousands = array('', 'm', 'mm', 'mmm');
904     /**
905      * Convert an integer to roman numerals.
906      * @param int $number an integer between 1 and 3999 inclusive. Anything else
907      *      will throw an exception.
908      * @return string the number converted to lower case roman numerals.
909      */
910     public static function int_to_roman($number) {
911         if (!is_integer($number) || $number < 1 || $number > 3999) {
912             throw new coding_exception('Only integers between 0 and 3999 can be ' .
913                     'converted to roman numerals.', $number);
914         }
916         return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
917                 self::$tens[$number / 10 % 10] . self::$units[$number % 10];
918     }
920     /**
921      * Convert an integer to a letter of alphabet.
922      * @param int $number an integer between 1 and 26 inclusive.
923      * Anything else will throw an exception.
924      * @return string the number converted to upper case letter of alphabet.
925      */
926     public static function int_to_letter($number) {
927         $alphabet = [
928                 '1' => 'A',
929                 '2' => 'B',
930                 '3' => 'C',
931                 '4' => 'D',
932                 '5' => 'E',
933                 '6' => 'F',
934                 '7' => 'G',
935                 '8' => 'H',
936                 '9' => 'I',
937                 '10' => 'J',
938                 '11' => 'K',
939                 '12' => 'L',
940                 '13' => 'M',
941                 '14' => 'N',
942                 '15' => 'O',
943                 '16' => 'P',
944                 '17' => 'Q',
945                 '18' => 'R',
946                 '19' => 'S',
947                 '20' => 'T',
948                 '21' => 'U',
949                 '22' => 'V',
950                 '23' => 'W',
951                 '24' => 'X',
952                 '25' => 'Y',
953                 '26' => 'Z'
954         ];
955         if (!is_integer($number) || $number < 1 || $number > count($alphabet)) {
956             throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number);
957         }
958         return $alphabet[$number];
959     }
961     /**
962      * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
963      * This method copes with:
964      *  - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.
965      *  - numbers that were typed as either 1.00 or 1,00 form.
966      *  - invalid things, which get turned into null.
967      *
968      * @param string|null $mark raw use input of a mark.
969      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
970      */
971     public static function clean_param_mark($mark) {
972         if ($mark === '' || is_null($mark)) {
973             return $mark;
974         }
976         $mark = str_replace(',', '.', $mark);
977         // This regexp should match the one in validate_param.
978         if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {
979             return null;
980         }
982         return clean_param($mark, PARAM_FLOAT);
983     }
985     /**
986      * Get a sumitted variable (from the GET or POST data) that is a mark.
987      * @param string $parname the submitted variable name.
988      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
989      */
990     public static function optional_param_mark($parname) {
991         return self::clean_param_mark(
992                 optional_param($parname, null, PARAM_RAW_TRIMMED));
993     }
995     /**
996      * Convert part of some question content to plain text.
997      * @param string $text the text.
998      * @param int $format the text format.
999      * @param array $options formatting options. Passed to {@link format_text}.
1000      * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1001      */
1002     public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {
1003         // The following call to html_to_text uses the option that strips out
1004         // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.
1005         // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't
1006         // matter what. We use http://example.com/.
1007         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
1008         return html_to_text(format_text($text, $format, $options), 0, false);
1009     }
1011     /**
1012      * Get the options required to configure the filepicker for one of the editor
1013      * toolbar buttons.
1014      * @param mixed $acceptedtypes array of types of '*'.
1015      * @param int $draftitemid the draft area item id.
1016      * @param object $context the context.
1017      * @return object the required options.
1018      */
1019     protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
1020         $filepickeroptions = new stdClass();
1021         $filepickeroptions->accepted_types = $acceptedtypes;
1022         $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
1023         $filepickeroptions->context = $context;
1024         $filepickeroptions->env = 'filepicker';
1026         $options = initialise_filepicker($filepickeroptions);
1027         $options->context = $context;
1028         $options->client_id = uniqid();
1029         $options->env = 'editor';
1030         $options->itemid = $draftitemid;
1032         return $options;
1033     }
1035     /**
1036      * Get filepicker options for question related text areas.
1037      * @param object $context the context.
1038      * @param int $draftitemid the draft area item id.
1039      * @return array An array of options
1040      */
1041     public static function get_filepicker_options($context, $draftitemid) {
1042         return [
1043                 'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
1044                 'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
1045                 'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
1046             ];
1047     }
1049     /**
1050      * Get editor options for question related text areas.
1051      * @param object $context the context.
1052      * @return array An array of options
1053      */
1054     public static function get_editor_options($context) {
1055         global $CFG;
1057         $editoroptions = [
1058                 'subdirs'  => 0,
1059                 'context'  => $context,
1060                 'maxfiles' => EDITOR_UNLIMITED_FILES,
1061                 'maxbytes' => $CFG->maxbytes,
1062                 'noclean' => 0,
1063                 'trusttext' => 0,
1064                 'autosave' => false
1065         ];
1067         return $editoroptions;
1068     }
1072 /**
1073  * The interface for strategies for controlling which variant of each question is used.
1074  *
1075  * @copyright  2011 The Open University
1076  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1077  */
1078 interface question_variant_selection_strategy {
1079     /**
1080      * @param int $maxvariants the num
1081      * @param string $seed data that can be used to controls how the variant is selected
1082      *      in a semi-random way.
1083      * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
1084      */
1085     public function choose_variant($maxvariants, $seed);
1089 /**
1090  * A {@link question_variant_selection_strategy} that is completely random.
1091  *
1092  * @copyright  2011 The Open University
1093  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1094  */
1095 class question_variant_random_strategy implements question_variant_selection_strategy {
1096     public function choose_variant($maxvariants, $seed) {
1097         return rand(1, $maxvariants);
1098     }
1102 /**
1103  * A {@link question_variant_selection_strategy} that is effectively random
1104  * for the first attempt, and then after that cycles through the available
1105  * variants so that the students will not get a repeated variant until they have
1106  * seen them all.
1107  *
1108  * @copyright  2011 The Open University
1109  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1110  */
1111 class question_variant_pseudorandom_no_repeats_strategy
1112         implements question_variant_selection_strategy {
1114     /** @var int the number of attempts this users has had, including the curent one. */
1115     protected $attemptno;
1117     /** @var int the user id the attempt belongs to. */
1118     protected $userid;
1120     /** @var string extra input fed into the pseudo-random code. */
1121     protected $extrarandomness = '';
1123     /**
1124      * Constructor.
1125      * @param int $attemptno The attempt number.
1126      * @param int $userid the user the attempt is for (defaults to $USER->id).
1127      */
1128     public function __construct($attemptno, $userid = null, $extrarandomness = '') {
1129         $this->attemptno = $attemptno;
1130         if (is_null($userid)) {
1131             global $USER;
1132             $this->userid = $USER->id;
1133         } else {
1134             $this->userid = $userid;
1135         }
1137         if ($extrarandomness) {
1138             $this->extrarandomness = '|' . $extrarandomness;
1139         }
1140     }
1142     public function choose_variant($maxvariants, $seed) {
1143         if ($maxvariants == 1) {
1144             return 1;
1145         }
1147         $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
1148         $randint = hexdec(substr($hash, 17, 7));
1150         return ($randint + $this->attemptno) % $maxvariants + 1;
1151     }
1154 /**
1155  * A {@link question_variant_selection_strategy} designed ONLY for testing.
1156  * For selected questions it wil return a specific variants. For the other
1157  * slots it will use a fallback strategy.
1158  *
1159  * @copyright  2013 The Open University
1160  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1161  */
1162 class question_variant_forced_choices_selection_strategy
1163     implements question_variant_selection_strategy {
1165     /** @var array seed => variant to select. */
1166     protected $forcedchoices;
1168     /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
1169     protected $basestrategy;
1171     /**
1172      * Constructor.
1173      * @param array $forcedchoices array seed => variant to select.
1174      * @param question_variant_selection_strategy $basestrategy strategy used
1175      *      to make the non-forced choices.
1176      */
1177     public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
1178         $this->forcedchoices = $forcedchoices;
1179         $this->basestrategy  = $basestrategy;
1180     }
1182     public function choose_variant($maxvariants, $seed) {
1183         if (array_key_exists($seed, $this->forcedchoices)) {
1184             if ($this->forcedchoices[$seed] > $maxvariants) {
1185                 throw new coding_exception('Forced variant out of range.');
1186             }
1187             return $this->forcedchoices[$seed];
1188         } else {
1189             return $this->basestrategy->choose_variant($maxvariants, $seed);
1190         }
1191     }
1193     /**
1194      * Helper method for preparing the $forcedchoices array.
1195      * @param array                      $variantsbyslot slot number => variant to select.
1196      * @param question_usage_by_activity $quba           the question usage we need a strategy for.
1197      * @throws coding_exception when variant cannot be forced as doesn't work.
1198      * @return array that can be passed to the constructor as $forcedchoices.
1199      */
1200     public static function prepare_forced_choices_array(array $variantsbyslot,
1201                                                         question_usage_by_activity $quba) {
1203         $forcedchoices = array();
1205         foreach ($variantsbyslot as $slot => $varianttochoose) {
1206             $question = $quba->get_question($slot);
1207             $seed = $question->get_variants_selection_seed();
1208             if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
1209                 throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
1210             }
1211             if ($varianttochoose > $question->get_num_variants()) {
1212                 throw new coding_exception('Forced variant out of range at slot ' . $slot);
1213             }
1214             $forcedchoices[$seed] = $varianttochoose;
1215         }
1216         return $forcedchoices;
1217     }