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/>.
16
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 */
25
26
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->libdir . '/filelib.php');
30require_once(__DIR__ . '/questionusage.php');
31require_once(__DIR__ . '/questionattempt.php');
32require_once(__DIR__ . '/questionattemptstep.php');
33require_once(__DIR__ . '/states.php');
34require_once(__DIR__ . '/datalib.php');
35require_once(__DIR__ . '/renderer.php');
36require_once(__DIR__ . '/bank.php');
37require_once(__DIR__ . '/../type/questiontypebase.php');
38require_once(__DIR__ . '/../type/questionbase.php');
39require_once(__DIR__ . '/../type/rendererbase.php');
40require_once(__DIR__ . '/../behaviour/behaviourtypebase.php');
41require_once(__DIR__ . '/../behaviour/behaviourbase.php');
42require_once(__DIR__ . '/../behaviour/rendererbase.php');
43require_once($CFG->libdir . '/questionlib.php');
44
45
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 */
56abstract class question_engine {
57    /** @var array behaviour name => 1. Records which behaviours have been loaded. */
58    private static $loadedbehaviours = array();
59
60    /** @var array behaviour name => question_behaviour_type for this behaviour. */
61    private static $behaviourtypes = array();
62
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 context $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    }
75
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    }
86
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    }
103
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    }
111
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    }
120
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    }
133
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    }
150
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    }
164
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    }
176
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        }
190
191        self::load_behaviour_class($preferredbehaviour);
192        $class = 'qbehaviour_' . $preferredbehaviour;
193        return new $class($qa, $preferredbehaviour);
194    }
195
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    }
204
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    }
214
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    }
238
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);
255
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        }
261
262        self::$loadedbehaviours[$behaviour] = 1;
263    }
264
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) {
279
280        if (array_key_exists($behaviour, self::$behaviourtypes)) {
281            return self::$behaviourtypes[$behaviour];
282        }
283
284        self::load_behaviour_type_class($behaviour);
285
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        }
294
295        return self::$behaviourtypes[$behaviour];
296    }
297
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    }
315
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    }
335
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    }
343
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) {
355
356        // Get disabled behaviours
357        if ($disabledlist) {
358            $disabled = explode(',', $disabledlist);
359        } else {
360            $disabled = array();
361        }
362
363        if ($orderlist) {
364            $order = explode(',', $orderlist);
365        } else {
366            $order = array();
367        }
368
369        foreach ($disabled as $behaviour) {
370            if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
371                unset($archetypes[$behaviour]);
372            }
373        }
374
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);
385
386        // Set up the final order to be displayed
387        return $behaviourorder + $leftover;
388    }
389
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();
400
401        // If no admin setting return all behavious
402        if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
403            return $archetypes;
404        }
405
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        }
416
417        return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
418    }
419
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    }
428
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 = array_merge($variables, $qtype->response_file_areas());
436        }
437
438        $areas = array();
439        foreach (array_unique($variables) as $variable) {
440            $areas[] = 'response_' . $variable;
441        }
442        return $areas;
443    }
444
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    }
453
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    }
462}
463
464
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 */
478class 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    /**#@-*/
484
485    /**#@+ @var integer named constants for the {@link $marks} option. */
486    const MAX_ONLY = 1;
487    const MARK_AND_MAX = 2;
488    /**#@-*/
489
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;
496
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;
502
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;
508
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;
518
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;
525
526    /** @var number of decimal places to use when formatting marks for output. */
527    public $markdp = 2;
528
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;
536
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;
543
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;
552
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;
559
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;
567
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;
576
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;
583
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;
590
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;
597
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 = '';
607
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 = '';
614
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();
628
629    /**
630     * @var context the context the attempt being output belongs to.
631     */
632    public $context;
633
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    }
647
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    }
664}
665
666
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 */
673abstract 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    }
690
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    }
705
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        }
725
726        $dm = new question_engine_data_mapper();
727        $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
728    }
729
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    }
764}
765
766
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 */
775class 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    }
783}
784
785
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 */
792abstract 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;
802
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    }
826
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    }
848
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    }
873
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    }
898
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');
903
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        }
915
916        return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
917                self::$tens[$number / 10 % 10] . self::$units[$number % 10];
918    }
919
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    }
960
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        }
975
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        }
981
982        return clean_param($mark, PARAM_FLOAT);
983    }
984
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    }
994
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    }
1010
1011    /**
1012     * Get the options required to configure the filepicker for one of the editor
1013     * toolbar buttons.
1014     *
1015     * @param mixed $acceptedtypes array of types of '*'.
1016     * @param int $draftitemid the draft area item id.
1017     * @param context $context the context.
1018     * @return object the required options.
1019     */
1020    protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
1021        $filepickeroptions = new stdClass();
1022        $filepickeroptions->accepted_types = $acceptedtypes;
1023        $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
1024        $filepickeroptions->context = $context;
1025        $filepickeroptions->env = 'filepicker';
1026
1027        $options = initialise_filepicker($filepickeroptions);
1028        $options->context = $context;
1029        $options->client_id = uniqid();
1030        $options->env = 'editor';
1031        $options->itemid = $draftitemid;
1032
1033        return $options;
1034    }
1035
1036    /**
1037     * Get filepicker options for question related text areas.
1038     *
1039     * @param context $context the context.
1040     * @param int $draftitemid the draft area item id.
1041     * @return array An array of options
1042     */
1043    public static function get_filepicker_options($context, $draftitemid) {
1044        return [
1045                'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
1046                'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
1047                'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
1048            ];
1049    }
1050
1051    /**
1052     * Get editor options for question related text areas.
1053     *
1054     * @param context $context the context.
1055     * @return array An array of options
1056     */
1057    public static function get_editor_options($context) {
1058        global $CFG;
1059
1060        $editoroptions = [
1061                'subdirs'  => 0,
1062                'context'  => $context,
1063                'maxfiles' => EDITOR_UNLIMITED_FILES,
1064                'maxbytes' => $CFG->maxbytes,
1065                'noclean' => 0,
1066                'trusttext' => 0,
1067                'autosave' => false
1068        ];
1069
1070        return $editoroptions;
1071    }
1072}
1073
1074
1075/**
1076 * The interface for strategies for controlling which variant of each question is used.
1077 *
1078 * @copyright  2011 The Open University
1079 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1080 */
1081interface question_variant_selection_strategy {
1082    /**
1083     * @param int $maxvariants the num
1084     * @param string $seed data that can be used to controls how the variant is selected
1085     *      in a semi-random way.
1086     * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
1087     */
1088    public function choose_variant($maxvariants, $seed);
1089}
1090
1091
1092/**
1093 * A {@link question_variant_selection_strategy} that is completely random.
1094 *
1095 * @copyright  2011 The Open University
1096 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1097 */
1098class question_variant_random_strategy implements question_variant_selection_strategy {
1099    public function choose_variant($maxvariants, $seed) {
1100        return rand(1, $maxvariants);
1101    }
1102}
1103
1104
1105/**
1106 * A {@link question_variant_selection_strategy} that is effectively random
1107 * for the first attempt, and then after that cycles through the available
1108 * variants so that the students will not get a repeated variant until they have
1109 * seen them all.
1110 *
1111 * @copyright  2011 The Open University
1112 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1113 */
1114class question_variant_pseudorandom_no_repeats_strategy
1115        implements question_variant_selection_strategy {
1116
1117    /** @var int the number of attempts this users has had, including the curent one. */
1118    protected $attemptno;
1119
1120    /** @var int the user id the attempt belongs to. */
1121    protected $userid;
1122
1123    /** @var string extra input fed into the pseudo-random code. */
1124    protected $extrarandomness = '';
1125
1126    /**
1127     * Constructor.
1128     * @param int $attemptno The attempt number.
1129     * @param int $userid the user the attempt is for (defaults to $USER->id).
1130     */
1131    public function __construct($attemptno, $userid = null, $extrarandomness = '') {
1132        $this->attemptno = $attemptno;
1133        if (is_null($userid)) {
1134            global $USER;
1135            $this->userid = $USER->id;
1136        } else {
1137            $this->userid = $userid;
1138        }
1139
1140        if ($extrarandomness) {
1141            $this->extrarandomness = '|' . $extrarandomness;
1142        }
1143    }
1144
1145    public function choose_variant($maxvariants, $seed) {
1146        if ($maxvariants == 1) {
1147            return 1;
1148        }
1149
1150        $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
1151        $randint = hexdec(substr($hash, 17, 7));
1152
1153        return ($randint + $this->attemptno) % $maxvariants + 1;
1154    }
1155}
1156
1157/**
1158 * A {@link question_variant_selection_strategy} designed ONLY for testing.
1159 * For selected questions it wil return a specific variants. For the other
1160 * slots it will use a fallback strategy.
1161 *
1162 * @copyright  2013 The Open University
1163 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1164 */
1165class question_variant_forced_choices_selection_strategy
1166    implements question_variant_selection_strategy {
1167
1168    /** @var array seed => variant to select. */
1169    protected $forcedchoices;
1170
1171    /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
1172    protected $basestrategy;
1173
1174    /**
1175     * Constructor.
1176     * @param array $forcedchoices array seed => variant to select.
1177     * @param question_variant_selection_strategy $basestrategy strategy used
1178     *      to make the non-forced choices.
1179     */
1180    public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
1181        $this->forcedchoices = $forcedchoices;
1182        $this->basestrategy  = $basestrategy;
1183    }
1184
1185    public function choose_variant($maxvariants, $seed) {
1186        if (array_key_exists($seed, $this->forcedchoices)) {
1187            if ($this->forcedchoices[$seed] > $maxvariants) {
1188                throw new coding_exception('Forced variant out of range.');
1189            }
1190            return $this->forcedchoices[$seed];
1191        } else {
1192            return $this->basestrategy->choose_variant($maxvariants, $seed);
1193        }
1194    }
1195
1196    /**
1197     * Helper method for preparing the $forcedchoices array.
1198     * @param array                      $variantsbyslot slot number => variant to select.
1199     * @param question_usage_by_activity $quba           the question usage we need a strategy for.
1200     * @throws coding_exception when variant cannot be forced as doesn't work.
1201     * @return array that can be passed to the constructor as $forcedchoices.
1202     */
1203    public static function prepare_forced_choices_array(array $variantsbyslot,
1204                                                        question_usage_by_activity $quba) {
1205
1206        $forcedchoices = array();
1207
1208        foreach ($variantsbyslot as $slot => $varianttochoose) {
1209            $question = $quba->get_question($slot);
1210            $seed = $question->get_variants_selection_seed();
1211            if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
1212                throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
1213            }
1214            if ($varianttochoose > $question->get_num_variants()) {
1215                throw new coding_exception('Forced variant out of range at slot ' . $slot);
1216            }
1217            $forcedchoices[$seed] = $varianttochoose;
1218        }
1219        return $forcedchoices;
1220    }
1221}
1222