MDL-28471 make the question flag smaller, and give it a label.
[moodle.git] / question / engine / lib.php
CommitLineData
d1b7e03d 1<?php
d1b7e03d
TH
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 *
017bc1d9 20 * @package moodlecore
d1b7e03d 21 * @subpackage questionengine
017bc1d9
TH
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
dcd03928
TH
29require_once(dirname(__FILE__) . '/questionusage.php');
30require_once(dirname(__FILE__) . '/questionattempt.php');
31require_once(dirname(__FILE__) . '/questionattemptstep.php');
d1b7e03d
TH
32require_once(dirname(__FILE__) . '/states.php');
33require_once(dirname(__FILE__) . '/datalib.php');
34require_once(dirname(__FILE__) . '/renderer.php');
35require_once(dirname(__FILE__) . '/bank.php');
afe24f85 36require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
d1b7e03d
TH
37require_once(dirname(__FILE__) . '/../type/questionbase.php');
38require_once(dirname(__FILE__) . '/../type/rendererbase.php');
39require_once(dirname(__FILE__) . '/../behaviour/behaviourbase.php');
40require_once(dirname(__FILE__) . '/../behaviour/rendererbase.php');
41require_once($CFG->libdir . '/questionlib.php');
42
43
44/**
45 * This static class provides access to the other question engine classes.
46 *
47 * It provides functions for managing question behaviours), and for
48 * creating, loading, saving and deleting {@link question_usage_by_activity}s,
49 * which is the main class that is used by other code that wants to use questions.
50 *
017bc1d9
TH
51 * @copyright 2009 The Open University
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
53 */
54abstract class question_engine {
55 /** @var array behaviour name => 1. Records which behaviours have been loaded. */
56 private static $loadedbehaviours = array();
57
58 /**
59 * Create a new {@link question_usage_by_activity}. The usage is
60 * created in memory. If you want it to persist, you will need to call
61 * {@link save_questions_usage_by_activity()}.
62 *
63 * @param string $component the plugin creating this attempt. For example mod_quiz.
64 * @param object $context the context this usage belongs to.
65 * @return question_usage_by_activity the newly created object.
66 */
67 public static function make_questions_usage_by_activity($component, $context) {
68 return new question_usage_by_activity($component, $context);
69 }
70
71 /**
72 * Load a {@link question_usage_by_activity} from the database, based on its id.
f7970e3c 73 * @param int $qubaid the id of the usage to load.
d1b7e03d
TH
74 * @return question_usage_by_activity loaded from the database.
75 */
76 public static function load_questions_usage_by_activity($qubaid) {
77 $dm = new question_engine_data_mapper();
78 return $dm->load_questions_usage_by_activity($qubaid);
79 }
80
81 /**
82 * Save a {@link question_usage_by_activity} to the database. This works either
83 * if the usage was newly created by {@link make_questions_usage_by_activity()}
84 * or loaded from the database using {@link load_questions_usage_by_activity()}
85 * @param question_usage_by_activity the usage to save.
86 */
87 public static function save_questions_usage_by_activity(question_usage_by_activity $quba) {
88 $dm = new question_engine_data_mapper();
89 $observer = $quba->get_observer();
90 if ($observer instanceof question_engine_unit_of_work) {
91 $observer->save($dm);
92 } else {
93 $dm->insert_questions_usage_by_activity($quba);
94 }
95 }
96
97 /**
98 * Delete a {@link question_usage_by_activity} from the database, based on its id.
f7970e3c 99 * @param int $qubaid the id of the usage to delete.
d1b7e03d
TH
100 */
101 public static function delete_questions_usage_by_activity($qubaid) {
6b5f24d3 102 self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
d1b7e03d
TH
103 }
104
105 /**
6b5f24d3
TH
106 * Delete {@link question_usage_by_activity}s from the database.
107 * @param qubaid_condition $qubaids identifies which questions usages to delete.
d1b7e03d 108 */
6b5f24d3 109 public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
d1b7e03d 110 $dm = new question_engine_data_mapper();
6b5f24d3 111 $dm->delete_questions_usage_by_activities($qubaids);
d1b7e03d
TH
112 }
113
114 /**
115 * Change the maxmark for the question_attempt with number in usage $slot
116 * for all the specified question_attempts.
117 * @param qubaid_condition $qubaids Selects which usages are updated.
f7970e3c 118 * @param int $slot the number is usage to affect.
d1b7e03d
TH
119 * @param number $newmaxmark the new max mark to set.
120 */
121 public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
122 $slot, $newmaxmark) {
123 $dm = new question_engine_data_mapper();
124 $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
125 }
126
127 /**
128 * @param array $questionids of question ids.
07f88584
TH
129 * @param qubaid_condition $qubaids ids of the usages to consider.
130 * @return boolean whether any of these questions are being used by any of
131 * those usages.
d1b7e03d 132 */
07f88584
TH
133 public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
134 if (is_null($qubaids)) {
135 return false;
136 }
d1b7e03d 137 $dm = new question_engine_data_mapper();
07f88584 138 return $dm->questions_in_use($questionids, $qubaids);
d1b7e03d
TH
139 }
140
141 /**
142 * Create an archetypal behaviour for a particular question attempt.
143 * Used by {@link question_definition::make_behaviour()}.
144 *
145 * @param string $preferredbehaviour the type of model required.
146 * @param question_attempt $qa the question attempt the model will process.
147 * @return question_behaviour an instance of appropriate behaviour class.
148 */
149 public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
9c197f44 150 self::load_behaviour_class($preferredbehaviour);
d1b7e03d
TH
151 $class = 'qbehaviour_' . $preferredbehaviour;
152 if (!constant($class . '::IS_ARCHETYPAL')) {
9c197f44
TH
153 throw new coding_exception('The requested behaviour is not actually ' .
154 'an archetypal one.');
d1b7e03d
TH
155 }
156 return new $class($qa, $preferredbehaviour);
157 }
158
159 /**
160 * @param string $behaviour the name of a behaviour.
161 * @return array of {@link question_display_options} field names, that are
162 * not relevant to this behaviour before a 'finish' action.
163 */
164 public static function get_behaviour_unused_display_options($behaviour) {
165 self::load_behaviour_class($behaviour);
166 $class = 'qbehaviour_' . $behaviour;
167 if (!method_exists($class, 'get_unused_display_options')) {
168 return question_behaviour::get_unused_display_options();
169 }
170 return call_user_func(array($class, 'get_unused_display_options'));
171 }
172
173 /**
174 * Create an behaviour for a particular type. If that type cannot be
175 * found, return an instance of qbehaviour_missing.
176 *
177 * Normally you should use {@link make_archetypal_behaviour()}, or
178 * call the constructor of a particular model class directly. This method
179 * is only intended for use by {@link question_attempt::load_from_records()}.
180 *
181 * @param string $behaviour the type of model to create.
182 * @param question_attempt $qa the question attempt the model will process.
183 * @param string $preferredbehaviour the preferred behaviour for the containing usage.
184 * @return question_behaviour an instance of appropriate behaviour class.
185 */
186 public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
187 try {
188 self::load_behaviour_class($behaviour);
189 } catch (Exception $e) {
9c197f44 190 self::load_behaviour_class('missing');
d1b7e03d
TH
191 return new qbehaviour_missing($qa, $preferredbehaviour);
192 }
193 $class = 'qbehaviour_' . $behaviour;
194 return new $class($qa, $preferredbehaviour);
195 }
196
197 /**
198 * Load the behaviour class(es) belonging to a particular model. That is,
199 * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
200 * of checking.
201 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
202 */
203 public static function load_behaviour_class($behaviour) {
204 global $CFG;
205 if (isset(self::$loadedbehaviours[$behaviour])) {
206 return;
207 }
208 $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
209 if (!is_readable($file)) {
88f0eb15 210 throw new coding_exception('Unknown question behaviour ' . $behaviour);
d1b7e03d
TH
211 }
212 include_once($file);
213 self::$loadedbehaviours[$behaviour] = 1;
214 }
215
216 /**
217 * Return an array where the keys are the internal names of the archetypal
218 * behaviours, and the values are a human-readable name. An
219 * archetypal behaviour is one that is suitable to pass the name of to
220 * {@link question_usage_by_activity::set_preferred_behaviour()}.
221 *
222 * @return array model name => lang string for this behaviour name.
223 */
224 public static function get_archetypal_behaviours() {
225 $archetypes = array();
fde4560d
TH
226 $behaviours = get_plugin_list('qbehaviour');
227 foreach ($behaviours as $behaviour => $notused) {
228 if (self::is_behaviour_archetypal($behaviour)) {
d1b7e03d
TH
229 $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
230 }
231 }
232 asort($archetypes, SORT_LOCALE_STRING);
233 return $archetypes;
234 }
235
fde4560d
TH
236 /**
237 * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
238 * @return bool whether this is an archetypal behaviour.
239 */
240 public static function is_behaviour_archetypal($behaviour) {
241 self::load_behaviour_class($behaviour);
242 $plugin = 'qbehaviour_' . $behaviour;
243 return constant($plugin . '::IS_ARCHETYPAL');
244 }
245
d1b7e03d
TH
246 /**
247 * Return an array where the keys are the internal names of the behaviours
248 * in preferred order and the values are a human-readable name.
249 *
250 * @param array $archetypes, array of behaviours
fde4560d
TH
251 * @param string $orderlist, a comma separated list of behaviour names
252 * @param string $disabledlist, a comma separated list of behaviour names
253 * @param string $current, current behaviour name
d1b7e03d
TH
254 * @return array model name => lang string for this behaviour name.
255 */
fde4560d 256 public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
d1b7e03d
TH
257
258 // Get disabled behaviours
fde4560d
TH
259 if ($disabledlist) {
260 $disabled = explode(',', $disabledlist);
d1b7e03d 261 } else {
fde4560d 262 $disabled = array();
d1b7e03d
TH
263 }
264
fde4560d
TH
265 if ($orderlist) {
266 $order = explode(',', $orderlist);
d1b7e03d 267 } else {
fde4560d 268 $order = array();
d1b7e03d
TH
269 }
270
fde4560d
TH
271 foreach ($disabled as $behaviour) {
272 if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
273 unset($archetypes[$behaviour]);
d1b7e03d
TH
274 }
275 }
276
277 // Get behaviours in preferred order
fde4560d
TH
278 $behaviourorder = array();
279 foreach ($order as $behaviour) {
280 if (array_key_exists($behaviour, $archetypes)) {
281 $behaviourorder[$behaviour] = $archetypes[$behaviour];
d1b7e03d
TH
282 }
283 }
284 // Get the rest of behaviours and sort them alphabetically
fde4560d 285 $leftover = array_diff_key($archetypes, $behaviourorder);
d1b7e03d
TH
286 asort($leftover, SORT_LOCALE_STRING);
287
288 // Set up the final order to be displayed
fde4560d 289 return $behaviourorder + $leftover;
d1b7e03d
TH
290 }
291
292 /**
293 * Return an array where the keys are the internal names of the behaviours
294 * in preferred order and the values are a human-readable name.
295 *
fde4560d 296 * @param string $currentbehaviour
d1b7e03d
TH
297 * @return array model name => lang string for this behaviour name.
298 */
fde4560d
TH
299 public static function get_behaviour_options($currentbehaviour) {
300 $config = question_bank::get_config();
d1b7e03d
TH
301 $archetypes = self::get_archetypal_behaviours();
302
303 // If no admin setting return all behavious
fde4560d 304 if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
d1b7e03d
TH
305 return $archetypes;
306 }
307
fde4560d
TH
308 if (empty($config->behavioursortorder)) {
309 $order = '';
310 } else {
311 $order = $config->behavioursortorder;
312 }
313 if (empty($config->disabledbehaviours)) {
314 $disabled = '';
315 } else {
316 $disabled = $config->disabledbehaviours;
317 }
318
319 return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
d1b7e03d
TH
320 }
321
322 /**
323 * Get the translated name of an behaviour, for display in the UI.
324 * @param string $behaviour the internal name of the model.
325 * @return string name from the current language pack.
326 */
327 public static function get_behaviour_name($behaviour) {
aa9bdbe3 328 return get_string('pluginname', 'qbehaviour_' . $behaviour);
d1b7e03d
TH
329 }
330
fde4560d
TH
331 /**
332 * Get the translated name of an behaviour, for display in the UI.
333 * @param string $behaviour the internal name of the model.
334 * @return string name from the current language pack.
335 */
336 public static function get_behaviour_required_behaviours($behaviour) {
337 $class = 'qbehaviour_' . $behaviour;
338 return $class::get_required_behaviours();
339 }
340
c749527b
TH
341 /**
342 * @return array all the file area names that may contain response files.
343 */
344 public static function get_all_response_file_areas() {
345 $variables = array();
346 foreach (question_bank::get_all_qtypes() as $qtype) {
347 $variables += $qtype->response_file_areas();
348 }
349
350 $areas = array();
351 foreach (array_unique($variables) as $variable) {
352 $areas[] = 'response_' . $variable;
353 }
354 return $areas;
355 }
356
d1b7e03d
TH
357 /**
358 * Returns the valid choices for the number of decimal places for showing
359 * question marks. For use in the user interface.
360 * @return array suitable for passing to {@link choose_from_menu()} or similar.
361 */
362 public static function get_dp_options() {
363 return question_display_options::get_dp_options();
364 }
365
6b5f24d3
TH
366 /**
367 * Initialise the JavaScript required on pages where questions will be displayed.
368 */
d1b7e03d
TH
369 public static function initialise_js() {
370 return question_flags::initialise_js();
371 }
372}
373
374
375/**
376 * This class contains all the options that controls how a question is displayed.
377 *
378 * Normally, what will happen is that the calling code will set up some display
379 * options to indicate what sort of question display it wants, and then before the
380 * question is rendered, the behaviour will be given a chance to modify the
381 * display options, so that, for example, A question that is finished will only
382 * be shown read-only, and a question that has not been submitted will not have
383 * any sort of feedback displayed.
384 *
017bc1d9
TH
385 * @copyright 2009 The Open University
386 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
387 */
388class question_display_options {
389 /**#@+ @var integer named constants for the values that most of the options take. */
390 const HIDDEN = 0;
391 const VISIBLE = 1;
392 const EDITABLE = 2;
393 /**#@-*/
394
395 /**#@+ @var integer named constants for the {@link $marks} option. */
396 const MAX_ONLY = 1;
397 const MARK_AND_MAX = 2;
398 /**#@-*/
399
400 /**
401 * @var integer maximum value for the {@link $markpd} option. This is
402 * effectively set by the database structure, which uses NUMBER(12,7) columns
403 * for question marks/fractions.
404 */
405 const MAX_DP = 7;
406
407 /**
408 * @var boolean whether the question should be displayed as a read-only review,
409 * or in an active state where you can change the answer.
410 */
411 public $readonly = false;
412
413 /**
414 * @var boolean whether the question type should output hidden form fields
415 * to reset any incorrect parts of the resonse to blank.
416 */
417 public $clearwrong = false;
418
419 /**
420 * Should the student have what they got right and wrong clearly indicated.
421 * This includes the green/red hilighting of the bits of their response,
422 * whether the one-line summary of the current state of the question says
423 * correct/incorrect or just answered.
424 * @var integer {@link question_display_options::HIDDEN} or
425 * {@link question_display_options::VISIBLE}
426 */
427 public $correctness = self::VISIBLE;
428
429 /**
430 * The the mark and/or the maximum available mark for this question be visible?
431 * @var integer {@link question_display_options::HIDDEN},
432 * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
433 */
434 public $marks = self::MARK_AND_MAX;
435
436 /** @var number of decimal places to use when formatting marks for output. */
437 public $markdp = 2;
438
439 /**
440 * Should the flag this question UI element be visible, and if so, should the
441 * flag state be changable?
442 * @var integer {@link question_display_options::HIDDEN},
443 * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
444 */
445 public $flags = self::VISIBLE;
446
447 /**
448 * Should the specific feedback be visible.
449 * @var integer {@link question_display_options::HIDDEN} or
450 * {@link question_display_options::VISIBLE}
451 */
452 public $feedback = self::VISIBLE;
453
454 /**
455 * For questions with a number of sub-parts (like matching, or
456 * multiple-choice, multiple-reponse) display the number of sub-parts that
457 * were correct.
458 * @var integer {@link question_display_options::HIDDEN} or
459 * {@link question_display_options::VISIBLE}
460 */
461 public $numpartscorrect = self::VISIBLE;
462
463 /**
464 * Should the general feedback be visible?
465 * @var integer {@link question_display_options::HIDDEN} or
466 * {@link question_display_options::VISIBLE}
467 */
468 public $generalfeedback = self::VISIBLE;
469
470 /**
471 * Should the automatically generated display of what the correct answer is
472 * be visible?
473 * @var integer {@link question_display_options::HIDDEN} or
474 * {@link question_display_options::VISIBLE}
475 */
476 public $rightanswer = self::VISIBLE;
477
478 /**
479 * Should the manually added marker's comment be visible. Should the link for
480 * adding/editing the comment be there.
481 * @var integer {@link question_display_options::HIDDEN},
482 * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
483 * Editable means that form fields are displayed inline.
484 */
485 public $manualcomment = self::VISIBLE;
486
487 /**
488 * Should we show a 'Make comment or override grade' link?
489 * @var string base URL for the edit comment script, which will be shown if
490 * $manualcomment = self::VISIBLE.
491 */
492 public $manualcommentlink = null;
493
494 /**
495 * Used in places like the question history table, to show a link to review
496 * this question in a certain state. If blank, a link is not shown.
497 * @var string base URL for a review question script.
498 */
499 public $questionreviewlink = null;
500
501 /**
502 * Should the history of previous question states table be visible?
503 * @var integer {@link question_display_options::HIDDEN} or
504 * {@link question_display_options::VISIBLE}
505 */
506 public $history = self::HIDDEN;
507
da729916
TH
508 /**
509 * If not empty, then a link to edit the question will be included in
510 * the info box for the question.
511 *
512 * If used, this array must contain an element courseid or cmid.
513 *
514 * It shoudl also contain a parameter returnurl => moodle_url giving a
515 * sensible URL to go back to when the editing form is submitted or cancelled.
516 *
517 * @var array url parameter for the edit link. id => questiosnid will be
518 * added automatically.
519 */
520 public $editquestionparams = array();
521
caeeff07
TH
522 /**
523 * @var int the context the attempt being output belongs to.
524 */
8026d4aa 525 public $context;
caeeff07 526
d1b7e03d
TH
527 /**
528 * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
529 * {@link rightanswer} and {@link manualcomment} to
530 * {@link question_display_options::HIDDEN}.
531 */
532 public function hide_all_feedback() {
533 $this->feedback = self::HIDDEN;
534 $this->numpartscorrect = self::HIDDEN;
535 $this->generalfeedback = self::HIDDEN;
536 $this->rightanswer = self::HIDDEN;
537 $this->manualcomment = self::HIDDEN;
538 $this->correctness = self::HIDDEN;
539 }
540
541 /**
542 * Returns the valid choices for the number of decimal places for showing
543 * question marks. For use in the user interface.
544 *
545 * Calling code should probably use {@link question_engine::get_dp_options()}
546 * rather than calling this method directly.
547 *
548 * @return array suitable for passing to {@link choose_from_menu()} or similar.
549 */
550 public static function get_dp_options() {
551 $options = array();
552 for ($i = 0; $i <= self::MAX_DP; $i += 1) {
553 $options[$i] = $i;
554 }
555 return $options;
556 }
557}
558
559
560/**
561 * Contains the logic for handling question flags.
562 *
f7970e3c 563 * @copyright 2010 The Open University
017bc1d9 564 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
565 */
566abstract class question_flags {
567 /**
568 * Get the checksum that validates that a toggle request is valid.
f7970e3c
TH
569 * @param int $qubaid the question usage id.
570 * @param int $questionid the question id.
571 * @param int $sessionid the question_attempt id.
d1b7e03d
TH
572 * @param object $user the user. If null, defaults to $USER.
573 * @return string that needs to be sent to question/toggleflag.php for it to work.
574 */
9c197f44
TH
575 protected static function get_toggle_checksum($qubaid, $questionid,
576 $qaid, $slot, $user = null) {
d1b7e03d
TH
577 if (is_null($user)) {
578 global $USER;
579 $user = $USER;
580 }
06f8ed54 581 return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
d1b7e03d
TH
582 }
583
584 /**
585 * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
586 * You need to append &newstate=0/1 to this.
587 * @return the post data to send.
588 */
589 public static function get_postdata(question_attempt $qa) {
590 $qaid = $qa->get_database_id();
591 $qubaid = $qa->get_usage_id();
592 $qid = $qa->get_question()->id;
06f8ed54
TH
593 $slot = $qa->get_slot();
594 $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
404da238
TH
595 return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" .
596 sesskey() . '&newstate=';
d1b7e03d
TH
597 }
598
599 /**
600 * If the request seems valid, update the flag state of a question attempt.
601 * Throws exceptions if this is not a valid update request.
f7970e3c
TH
602 * @param int $qubaid the question usage id.
603 * @param int $questionid the question id.
604 * @param int $sessionid the question_attempt id.
d1b7e03d
TH
605 * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
606 * corresponding to the last three arguments.
f7970e3c 607 * @param bool $newstate the new state of the flag. true = flagged.
d1b7e03d 608 */
06f8ed54 609 public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
d1b7e03d 610 // Check the checksum - it is very hard to know who a question session belongs
f9b0500f 611 // to, so we require that checksum parameter is matches an md5 hash of the
d1b7e03d
TH
612 // three ids and the users username. Since we are only updating a flag, that
613 // probably makes it sufficiently difficult for malicious users to toggle
614 // other users flags.
9c197f44 615 if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
88f0eb15 616 throw new moodle_exception('errorsavingflags', 'question');
d1b7e03d
TH
617 }
618
619 $dm = new question_engine_data_mapper();
06f8ed54 620 $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
d1b7e03d
TH
621 }
622
623 public static function initialise_js() {
f29aeb5a
TH
624 global $CFG, $PAGE, $OUTPUT;
625 static $done = false;
626 if ($done) {
627 return;
628 }
629 $module = array(
630 'name' => 'core_question_flags',
631 'fullpath' => '/question/flags.js',
632 'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
633 );
634 $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
474ee938
WO
635 $fltext = array(0 => get_string('clickflag', 'question'),
636 1 => get_string('clickunflag', 'question'));
f29aeb5a
TH
637 $flagattributes = array(
638 0 => array(
639 'src' => $OUTPUT->pix_url('i/unflagged') . '',
640 'title' => get_string('clicktoflag', 'question'),
641 'alt' => get_string('notflagged', 'question'),
474ee938 642 // 'text' => get_string('clickflag', 'question'),
f29aeb5a
TH
643 ),
644 1 => array(
645 'src' => $OUTPUT->pix_url('i/flagged') . '',
646 'title' => get_string('clicktounflag', 'question'),
647 'alt' => get_string('flagged', 'question'),
474ee938 648 // 'text' => get_string('clickunflag', 'question'),
f29aeb5a 649 ),
d1b7e03d 650 );
f29aeb5a 651 $PAGE->requires->js_init_call('M.core_question_flags.init',
474ee938 652 array($actionurl, $flagattributes, $fltext), false, $module);
f29aeb5a 653 $done = true;
d1b7e03d
TH
654 }
655}
656
657
f7970e3c
TH
658/**
659 * Exception thrown when the system detects that a student has done something
660 * out-of-order to a question. This can happen, for example, if they click
5e8a85aa 661 * the browser's back button in a quiz, then try to submit a different response.
f7970e3c
TH
662 *
663 * @copyright 2010 The Open University
664 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
665 */
d1b7e03d 666class question_out_of_sequence_exception extends moodle_exception {
c7df5006 667 public function __construct($qubaid, $slot, $postdata) {
d1b7e03d
TH
668 if ($postdata == null) {
669 $postdata = data_submitted();
670 }
671 parent::__construct('submissionoutofsequence', 'question', '', null,
672 "QUBAid: $qubaid, slot: $slot, post data: " . print_r($postdata, true));
673 }
674}
675
676
d1b7e03d
TH
677/**
678 * Useful functions for writing question types and behaviours.
679 *
680 * @copyright 2010 The Open University
017bc1d9 681 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
682 */
683abstract class question_utils {
684 /**
685 * Tests to see whether two arrays have the same keys, with the same values
686 * (as compared by ===) for each key. However, the order of the arrays does
687 * not have to be the same.
688 * @param array $array1 the first array.
689 * @param array $array2 the second array.
f7970e3c 690 * @return bool whether the two arrays have the same keys with the same
d1b7e03d
TH
691 * corresponding values.
692 */
693 public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
694 if (count($array1) != count($array2)) {
695 return false;
696 }
697 foreach ($array1 as $key => $value1) {
698 if (!array_key_exists($key, $array2)) {
699 return false;
700 }
701 if (((string) $value1) !== ((string) $array2[$key])) {
702 return false;
703 }
704 }
705 return true;
706 }
707
708 /**
709 * Tests to see whether two arrays have the same value at a particular key.
710 * This method will return true if:
711 * 1. Neither array contains the key; or
712 * 2. Both arrays contain the key, and the corresponding values compare
713 * identical when cast to strings and compared with ===.
714 * @param array $array1 the first array.
715 * @param array $array2 the second array.
716 * @param string $key an array key.
f7970e3c 717 * @return bool whether the two arrays have the same value (or lack of
d1b7e03d
TH
718 * one) for a given key.
719 */
720 public static function arrays_same_at_key(array $array1, array $array2, $key) {
721 if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
722 return ((string) $array1[$key]) === ((string) $array2[$key]);
723 }
724 if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
725 return true;
726 }
727 return false;
728 }
729
730 /**
731 * Tests to see whether two arrays have the same value at a particular key.
732 * Missing values are replaced by '', and then the values are cast to
733 * strings and compared with ===.
734 * @param array $array1 the first array.
735 * @param array $array2 the second array.
736 * @param string $key an array key.
f7970e3c 737 * @return bool whether the two arrays have the same value (or lack of
d1b7e03d
TH
738 * one) for a given key.
739 */
740 public static function arrays_same_at_key_missing_is_blank(
741 array $array1, array $array2, $key) {
742 if (array_key_exists($key, $array1)) {
743 $value1 = $array1[$key];
744 } else {
745 $value1 = '';
746 }
747 if (array_key_exists($key, $array2)) {
748 $value2 = $array2[$key];
749 } else {
750 $value2 = '';
751 }
752 return ((string) $value1) === ((string) $value2);
753 }
754
755 /**
756 * Tests to see whether two arrays have the same value at a particular key.
757 * Missing values are replaced by 0, and then the values are cast to
758 * integers and compared with ===.
759 * @param array $array1 the first array.
760 * @param array $array2 the second array.
761 * @param string $key an array key.
f7970e3c 762 * @return bool whether the two arrays have the same value (or lack of
d1b7e03d
TH
763 * one) for a given key.
764 */
765 public static function arrays_same_at_key_integer(
766 array $array1, array $array2, $key) {
767 if (array_key_exists($key, $array1)) {
768 $value1 = $array1[$key];
769 } else {
770 $value1 = 0;
771 }
772 if (array_key_exists($key, $array2)) {
773 $value2 = $array2[$key];
774 } else {
775 $value2 = 0;
776 }
777 return ((integer) $value1) === ((integer) $value2);
778 }
779
9c197f44
TH
780 private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
781 private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
782 private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
d1b7e03d
TH
783 private static $thousands = array('', 'm', 'mm', 'mmm');
784
785 /**
786 * Convert an integer to roman numerals.
9c197f44
TH
787 * @param int $number an integer between 1 and 3999 inclusive. Anything else
788 * will throw an exception.
d1b7e03d
TH
789 * @return string the number converted to lower case roman numerals.
790 */
791 public static function int_to_roman($number) {
792 if (!is_integer($number) || $number < 1 || $number > 3999) {
793 throw new coding_exception('Only integers between 0 and 3999 can be ' .
794 'converted to roman numerals.', $number);
795 }
796
797 return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
798 self::$tens[$number / 10 % 10] . self::$units[$number % 10];
799 }
dcd03928 800}
1da821bb
TH
801
802
803/**
804 * The interface for strategies for controlling which variant of each question is used.
805 *
c014b989 806 * @copyright 2011 The Open University
1da821bb
TH
807 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
808 */
809interface question_variant_selection_strategy {
810 /**
811 * @param int $maxvariants the num
812 * @param string $seed data that can be used to controls how the variant is selected
813 * in a semi-random way.
814 * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
815 */
816 public function choose_variant($maxvariants, $seed);
817}
818
819
820/**
821 * A {@link question_variant_selection_strategy} that is completely random.
822 *
c014b989 823 * @copyright 2011 The Open University
1da821bb
TH
824 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
825 */
826class question_variant_random_strategy implements question_variant_selection_strategy {
827 public function choose_variant($maxvariants, $seed) {
828 return rand(1, $maxvariants);
829 }
830}
c014b989
TH
831
832
833/**
834 * A {@link question_variant_selection_strategy} that is effectively random
835 * for the first attempt, and then after that cycles through the available
836 * variants so that the students will not get a repeated variant until they have
837 * seen them all.
838 *
839 * @copyright 2011 The Open University
840 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
841 */
842class question_variant_pseudorandom_no_repeats_strategy
843 implements question_variant_selection_strategy {
844
845 /** @var int the number of attempts this users has had, including the curent one. */
846 protected $attemptno;
847
848 /** @var int the user id the attempt belongs to. */
849 protected $userid;
850
851 /**
852 * Constructor.
853 * @param int $attemptno The attempt number.
854 * @param int $userid the user the attempt is for (defaults to $USER->id).
855 */
856 public function __construct($attemptno, $userid = null) {
857 $this->attemptno = $attemptno;
858 if (is_null($userid)) {
859 global $USER;
860 $this->userid = $USER->id;
861 } else {
862 $this->userid = $userid;
863 }
864 }
865
866 public function choose_variant($maxvariants, $seed) {
867 if ($maxvariants == 1) {
868 return 1;
869 }
870
871 $hash = sha1($seed . '|user' . $this->userid);
872 $randint = hexdec(substr($hash, 17, 7));
873
874 return ($randint + $this->attemptno) % $maxvariants + 1;
875 }
876}