Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 402 and 403]
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 27 defined('MOODLE_INTERNAL') || die(); 28 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'); 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 */ 56 abstract 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 */ 478 class question_display_options { 479 /**#@+ 480 * @var integer named constants for the values that most of the options take. 481 */ 482 const SHOW_ALL = -1; 483 const HIDDEN = 0; 484 const VISIBLE = 1; 485 const EDITABLE = 2; 486 /**#@-*/ 487 488 /**#@+ @var integer named constants for the {@link $marks} option. */ 489 const MAX_ONLY = 1; 490 const MARK_AND_MAX = 2; 491 /**#@-*/ 492 493 /** 494 * @var integer maximum value for the {@link $markpd} option. This is 495 * effectively set by the database structure, which uses NUMBER(12,7) columns 496 * for question marks/fractions. 497 */ 498 const MAX_DP = 7; 499 500 /** 501 * @var boolean whether the question should be displayed as a read-only review, 502 * or in an active state where you can change the answer. 503 */ 504 public $readonly = false; 505 506 /** 507 * @var boolean whether the question type should output hidden form fields 508 * to reset any incorrect parts of the resonse to blank. 509 */ 510 public $clearwrong = false; 511 512 /** 513 * Should the student have what they got right and wrong clearly indicated. 514 * This includes the green/red hilighting of the bits of their response, 515 * whether the one-line summary of the current state of the question says 516 * correct/incorrect or just answered. 517 * @var integer {@link question_display_options::HIDDEN} or 518 * {@link question_display_options::VISIBLE} 519 */ 520 public $correctness = self::VISIBLE; 521 522 /** 523 * The the mark and/or the maximum available mark for this question be visible? 524 * @var integer {@link question_display_options::HIDDEN}, 525 * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX} 526 */ 527 public $marks = self::MARK_AND_MAX; 528 529 /** @var number of decimal places to use when formatting marks for output. */ 530 public $markdp = 2; 531 532 /** 533 * Should the flag this question UI element be visible, and if so, should the 534 * flag state be changable? 535 * @var integer {@link question_display_options::HIDDEN}, 536 * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE} 537 */ 538 public $flags = self::VISIBLE; 539 540 /** 541 * Should the specific feedback be visible. 542 * @var integer {@link question_display_options::HIDDEN} or 543 * {@link question_display_options::VISIBLE} 544 */ 545 public $feedback = self::VISIBLE; 546 547 /** 548 * For questions with a number of sub-parts (like matching, or 549 * multiple-choice, multiple-reponse) display the number of sub-parts that 550 * were correct. 551 * @var integer {@link question_display_options::HIDDEN} or 552 * {@link question_display_options::VISIBLE} 553 */ 554 public $numpartscorrect = self::VISIBLE; 555 556 /** 557 * Should the general feedback be visible? 558 * @var integer {@link question_display_options::HIDDEN} or 559 * {@link question_display_options::VISIBLE} 560 */ 561 public $generalfeedback = self::VISIBLE; 562 563 /** 564 * Should the automatically generated display of what the correct answer is 565 * be visible? 566 * @var integer {@link question_display_options::HIDDEN} or 567 * {@link question_display_options::VISIBLE} 568 */ 569 public $rightanswer = self::VISIBLE; 570 571 /** 572 * Should the manually added marker's comment be visible. Should the link for 573 * adding/editing the comment be there. 574 * @var integer {@link question_display_options::HIDDEN}, 575 * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}. 576 * Editable means that form fields are displayed inline. 577 */ 578 public $manualcomment = self::VISIBLE; 579 580 /** 581 * Should we show a 'Make comment or override grade' link? 582 * @var string base URL for the edit comment script, which will be shown if 583 * $manualcomment = self::VISIBLE. 584 */ 585 public $manualcommentlink = null; 586 587 /** 588 * Used in places like the question history table, to show a link to review 589 * this question in a certain state. If blank, a link is not shown. 590 * @var moodle_url base URL for a review question script. 591 */ 592 public $questionreviewlink = null; 593 594 /** 595 * Should the history of previous question states table be visible? 596 * @var integer {@link question_display_options::HIDDEN} or 597 * {@link question_display_options::VISIBLE} 598 */ 599 public $history = self::HIDDEN; 600 601 /** 602 * @since 2.9 603 * @var string extra HTML to include at the end of the outcome (feedback) box 604 * of the question display. 605 * 606 * This field is now badly named. The place it included is was changed 607 * (for the better) but the name was left unchanged for backwards compatibility. 608 */ 609 public $extrainfocontent = ''; 610 611 /** 612 * @since 2.9 613 * @var string extra HTML to include in the history box of the question display, 614 * if it is shown. 615 */ 616 public $extrahistorycontent = ''; 617 618 /** 619 * If not empty, then a link to edit the question will be included in 620 * the info box for the question. 621 * 622 * If used, this array must contain an element courseid or cmid. 623 * 624 * It shoudl also contain a parameter returnurl => moodle_url giving a 625 * sensible URL to go back to when the editing form is submitted or cancelled. 626 * 627 * @var array url parameter for the edit link. id => questiosnid will be 628 * added automatically. 629 */ 630 public $editquestionparams = array(); 631 632 /** 633 * @var context the context the attempt being output belongs to. 634 */ 635 public $context; 636 637 /** 638 * @var int The option to show the action author in the response history. 639 */ 640 public $userinfoinhistory = self::HIDDEN; 641 642 /** 643 * This identifier should be added to the labels of all input fields in the question. 644 * 645 * This is so people using assistive technology can easily tell which input belong to 646 * which question. The helper {@see self::add_question_identifier_to_label() makes this easier. 647 * 648 * If not set before the question is rendered, then it defaults to 'Question N'. 649 * (lang string) 650 * 651 * @var string The identifier that the question being rendered is associated with. 652 * E.g. The question number when it is rendered on a quiz. 653 */ 654 public $questionidentifier = null; 655 656 /** 657 * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback}, 658 * {@link rightanswer} and {@link manualcomment} to 659 * {@link question_display_options::HIDDEN}. 660 */ 661 public function hide_all_feedback() { 662 $this->feedback = self::HIDDEN; 663 $this->numpartscorrect = self::HIDDEN; 664 $this->generalfeedback = self::HIDDEN; 665 $this->rightanswer = self::HIDDEN; 666 $this->manualcomment = self::HIDDEN; 667 $this->correctness = self::HIDDEN; 668 } 669 670 /** 671 * Returns the valid choices for the number of decimal places for showing 672 * question marks. For use in the user interface. 673 * 674 * Calling code should probably use {@link question_engine::get_dp_options()} 675 * rather than calling this method directly. 676 * 677 * @return array suitable for passing to {@link html_writer::select()} or similar. 678 */ 679 public static function get_dp_options() { 680 $options = array(); 681 for ($i = 0; $i <= self::MAX_DP; $i += 1) { 682 $options[$i] = $i; 683 } 684 return $options; 685 } 686 687 /** 688 * Helper to add the question identify (if there is one) to the label of an input field in a question. 689 * 690 * @param string $label The plain field label. E.g. 'Answer 1' 691 * @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a sr-only span. Default false. 692 * @param bool $addbefore If true, the question identifier will be added before the label. 693 * @return string The amended label. For example 'Answer 1, Question 1'. 694 */ 695 public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string { 696 if (!$this->has_question_identifier()) { 697 return $label; 698 } 699 $identifier = $this->questionidentifier; 700 if ($sridentifier) { 701 $identifier = html_writer::span($identifier, 'sr-only'); 702 } 703 $fieldlang = 'fieldinquestion'; 704 if ($addbefore) { 705 $fieldlang = 'fieldinquestionpre'; 706 } 707 return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]); 708 } 709 710 /** 711 * Whether a question number has been provided for the question that is being displayed. 712 * 713 * @return bool 714 */ 715 public function has_question_identifier(): bool { 716 return $this->questionidentifier !== null && trim($this->questionidentifier) !== ''; 717 } 718 } 719 720 721 /** 722 * Contains the logic for handling question flags. 723 * 724 * @copyright 2010 The Open University 725 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 726 */ 727 abstract class question_flags { 728 /** 729 * Get the checksum that validates that a toggle request is valid. 730 * @param int $qubaid the question usage id. 731 * @param int $questionid the question id. 732 * @param int $sessionid the question_attempt id. 733 * @param object $user the user. If null, defaults to $USER. 734 * @return string that needs to be sent to question/toggleflag.php for it to work. 735 */ 736 protected static function get_toggle_checksum($qubaid, $questionid, 737 $qaid, $slot, $user = null) { 738 if (is_null($user)) { 739 global $USER; 740 $user = $USER; 741 } 742 return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot); 743 } 744 745 /** 746 * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state. 747 * You need to append &newstate=0/1 to this. 748 * @return the post data to send. 749 */ 750 public static function get_postdata(question_attempt $qa) { 751 $qaid = $qa->get_database_id(); 752 $qubaid = $qa->get_usage_id(); 753 $qid = $qa->get_question_id(); 754 $slot = $qa->get_slot(); 755 $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot); 756 return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" . 757 sesskey() . '&newstate='; 758 } 759 760 /** 761 * If the request seems valid, update the flag state of a question attempt. 762 * Throws exceptions if this is not a valid update request. 763 * @param int $qubaid the question usage id. 764 * @param int $questionid the question id. 765 * @param int $sessionid the question_attempt id. 766 * @param string $checksum checksum, as computed by {@link get_toggle_checksum()} 767 * corresponding to the last three arguments. 768 * @param bool $newstate the new state of the flag. true = flagged. 769 */ 770 public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) { 771 // Check the checksum - it is very hard to know who a question session belongs 772 // to, so we require that checksum parameter is matches an md5 hash of the 773 // three ids and the users username. Since we are only updating a flag, that 774 // probably makes it sufficiently difficult for malicious users to toggle 775 // other users flags. 776 if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) { 777 throw new moodle_exception('errorsavingflags', 'question'); 778 } 779 780 $dm = new question_engine_data_mapper(); 781 $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate); 782 } 783 784 public static function initialise_js() { 785 global $CFG, $PAGE, $OUTPUT; 786 static $done = false; 787 if ($done) { 788 return; 789 } 790 $module = array( 791 'name' => 'core_question_flags', 792 'fullpath' => '/question/flags.js', 793 'requires' => array('base', 'dom', 'event-delegate', 'io-base'), 794 ); 795 $actionurl = $CFG->wwwroot . '/question/toggleflag.php'; 796 $flagattributes = array( 797 0 => array( 798 'src' => $OUTPUT->image_url('i/unflagged') . '', 799 'title' => get_string('clicktoflag', 'question'), 800 'alt' => get_string('flagged', 'question'), // Label on toggle should not change. 801 'text' => get_string('clickflag', 'question'), 802 ), 803 1 => array( 804 'src' => $OUTPUT->image_url('i/flagged') . '', 805 'title' => get_string('clicktounflag', 'question'), 806 'alt' => get_string('flagged', 'question'), 807 'text' => get_string('clickunflag', 'question'), 808 ), 809 ); 810 $PAGE->requires->js_init_call('M.core_question_flags.init', 811 array($actionurl, $flagattributes), false, $module); 812 $done = true; 813 } 814 } 815 816 817 /** 818 * Exception thrown when the system detects that a student has done something 819 * out-of-order to a question. This can happen, for example, if they click 820 * the browser's back button in a quiz, then try to submit a different response. 821 * 822 * @copyright 2010 The Open University 823 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 824 */ 825 class question_out_of_sequence_exception extends moodle_exception { 826 public function __construct($qubaid, $slot, $postdata) { 827 if ($postdata == null) { 828 $postdata = data_submitted(); 829 } 830 parent::__construct('submissionoutofsequence', 'question', '', null, 831 "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true)); 832 } 833 } 834 835 836 /** 837 * Useful functions for writing question types and behaviours. 838 * 839 * @copyright 2010 The Open University 840 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 841 */ 842 abstract class question_utils { 843 /** 844 * @var float tolerance to use when comparing question mark/fraction values. 845 * 846 * When comparing floating point numbers in a computer, the representation is not 847 * necessarily exact. Therefore, we need to allow a tolerance. 848 * Question marks are stored in the database as decimal numbers with 7 decimal places. 849 * Therefore, this is the appropriate tolerance to use. 850 */ 851 const MARK_TOLERANCE = 0.00000005; 852 853 /** 854 * Tests to see whether two arrays have the same keys, with the same values 855 * (as compared by ===) for each key. However, the order of the arrays does 856 * not have to be the same. 857 * @param array $array1 the first array. 858 * @param array $array2 the second array. 859 * @return bool whether the two arrays have the same keys with the same 860 * corresponding values. 861 */ 862 public static function arrays_have_same_keys_and_values(array $array1, array $array2) { 863 if (count($array1) != count($array2)) { 864 return false; 865 } 866 foreach ($array1 as $key => $value1) { 867 if (!array_key_exists($key, $array2)) { 868 return false; 869 } 870 if (((string) $value1) !== ((string) $array2[$key])) { 871 return false; 872 } 873 } 874 return true; 875 } 876 877 /** 878 * Tests to see whether two arrays have the same value at a particular key. 879 * This method will return true if: 880 * 1. Neither array contains the key; or 881 * 2. Both arrays contain the key, and the corresponding values compare 882 * identical when cast to strings and compared with ===. 883 * @param array $array1 the first array. 884 * @param array $array2 the second array. 885 * @param string $key an array key. 886 * @return bool whether the two arrays have the same value (or lack of 887 * one) for a given key. 888 */ 889 public static function arrays_same_at_key(array $array1, array $array2, $key) { 890 if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) { 891 return ((string) $array1[$key]) === ((string) $array2[$key]); 892 } 893 if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) { 894 return true; 895 } 896 return false; 897 } 898 899 /** 900 * Tests to see whether two arrays have the same value at a particular key. 901 * Missing values are replaced by '', and then the values are cast to 902 * strings and compared with ===. 903 * @param array $array1 the first array. 904 * @param array $array2 the second array. 905 * @param string $key an array key. 906 * @return bool whether the two arrays have the same value (or lack of 907 * one) for a given key. 908 */ 909 public static function arrays_same_at_key_missing_is_blank( 910 array $array1, array $array2, $key) { 911 if (array_key_exists($key, $array1)) { 912 $value1 = $array1[$key]; 913 } else { 914 $value1 = ''; 915 } 916 if (array_key_exists($key, $array2)) { 917 $value2 = $array2[$key]; 918 } else { 919 $value2 = ''; 920 } 921 return ((string) $value1) === ((string) $value2); 922 } 923 924 /** 925 * Tests to see whether two arrays have the same value at a particular key. 926 * Missing values are replaced by 0, and then the values are cast to 927 * integers and compared with ===. 928 * @param array $array1 the first array. 929 * @param array $array2 the second array. 930 * @param string $key an array key. 931 * @return bool whether the two arrays have the same value (or lack of 932 * one) for a given key. 933 */ 934 public static function arrays_same_at_key_integer( 935 array $array1, array $array2, $key) { 936 if (array_key_exists($key, $array1)) { 937 $value1 = (int) $array1[$key]; 938 } else { 939 $value1 = 0; 940 } 941 if (array_key_exists($key, $array2)) { 942 $value2 = (int) $array2[$key]; 943 } else { 944 $value2 = 0; 945 } 946 return $value1 === $value2; 947 } 948 949 private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix'); 950 private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc'); 951 private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm'); 952 private static $thousands = array('', 'm', 'mm', 'mmm'); 953 954 /** 955 * Convert an integer to roman numerals. 956 * @param int $number an integer between 1 and 3999 inclusive. Anything else 957 * will throw an exception. 958 * @return string the number converted to lower case roman numerals. 959 */ 960 public static function int_to_roman($number) { 961 if (!is_integer($number) || $number < 1 || $number > 3999) { 962 throw new coding_exception('Only integers between 0 and 3999 can be ' . 963 'converted to roman numerals.', $number); 964 } 965 966 return self::$thousands[floor($number / 1000) % 10] . self::$hundreds[floor($number / 100) % 10] . 967 self::$tens[floor($number / 10) % 10] . self::$units[$number % 10]; 968 } 969 970 /** 971 * Convert an integer to a letter of alphabet. 972 * @param int $number an integer between 1 and 26 inclusive. 973 * Anything else will throw an exception. 974 * @return string the number converted to upper case letter of alphabet. 975 */ 976 public static function int_to_letter($number) { 977 $alphabet = [ 978 '1' => 'A', 979 '2' => 'B', 980 '3' => 'C', 981 '4' => 'D', 982 '5' => 'E', 983 '6' => 'F', 984 '7' => 'G', 985 '8' => 'H', 986 '9' => 'I', 987 '10' => 'J', 988 '11' => 'K', 989 '12' => 'L', 990 '13' => 'M', 991 '14' => 'N', 992 '15' => 'O', 993 '16' => 'P', 994 '17' => 'Q', 995 '18' => 'R', 996 '19' => 'S', 997 '20' => 'T', 998 '21' => 'U', 999 '22' => 'V', 1000 '23' => 'W', 1001 '24' => 'X', 1002 '25' => 'Y', 1003 '26' => 'Z' 1004 ]; 1005 if (!is_integer($number) || $number < 1 || $number > count($alphabet)) { 1006 throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number); 1007 } 1008 return $alphabet[$number]; 1009 } 1010 1011 /** 1012 * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED). 1013 * This method copes with: 1014 * - keeping null or '' input unchanged - important to let teaches set a question back to requries grading. 1015 * - numbers that were typed as either 1.00 or 1,00 form. 1016 * - invalid things, which get turned into null. 1017 * 1018 * @param string|null $mark raw use input of a mark. 1019 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 1020 */ 1021 public static function clean_param_mark($mark) { 1022 if ($mark === '' || is_null($mark)) { 1023 return $mark; 1024 } 1025 1026 $mark = str_replace(',', '.', $mark); 1027 // This regexp should match the one in validate_param. 1028 if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) { 1029 return null; 1030 } 1031 1032 return clean_param($mark, PARAM_FLOAT); 1033 } 1034 1035 /** 1036 * Get a sumitted variable (from the GET or POST data) that is a mark. 1037 * @param string $parname the submitted variable name. 1038 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 1039 */ 1040 public static function optional_param_mark($parname) { 1041 return self::clean_param_mark( 1042 optional_param($parname, null, PARAM_RAW_TRIMMED)); 1043 } 1044 1045 /** 1046 * Convert part of some question content to plain text. 1047 * @param string $text the text. 1048 * @param int $format the text format. 1049 * @param array $options formatting options. Passed to {@link format_text}. 1050 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 1051 */ 1052 public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) { 1053 // The following call to html_to_text uses the option that strips out 1054 // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens. 1055 // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't 1056 // matter what. We use http://example.com/. 1057 $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text); 1058 return html_to_text(format_text($text, $format, $options), 0, false); 1059 } 1060 1061 /** 1062 * Get the options required to configure the filepicker for one of the editor 1063 * toolbar buttons. 1064 * 1065 * @param mixed $acceptedtypes array of types of '*'. 1066 * @param int $draftitemid the draft area item id. 1067 * @param context $context the context. 1068 * @return object the required options. 1069 */ 1070 protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) { 1071 $filepickeroptions = new stdClass(); 1072 $filepickeroptions->accepted_types = $acceptedtypes; 1073 $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL; 1074 $filepickeroptions->context = $context; 1075 $filepickeroptions->env = 'filepicker'; 1076 1077 $options = initialise_filepicker($filepickeroptions); 1078 $options->context = $context; 1079 $options->client_id = uniqid(); 1080 $options->env = 'editor'; 1081 $options->itemid = $draftitemid; 1082 1083 return $options; 1084 } 1085 1086 /** 1087 * Get filepicker options for question related text areas. 1088 * 1089 * @param context $context the context. 1090 * @param int $draftitemid the draft area item id. 1091 * @return array An array of options 1092 */ 1093 public static function get_filepicker_options($context, $draftitemid) { 1094 return [ 1095 'image' => self::specific_filepicker_options(['image'], $draftitemid, $context), 1096 'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context), 1097 'link' => self::specific_filepicker_options('*', $draftitemid, $context), 1098 ]; 1099 } 1100 1101 /** 1102 * Get editor options for question related text areas. 1103 * 1104 * @param context $context the context. 1105 * @return array An array of options 1106 */ 1107 public static function get_editor_options($context) { 1108 global $CFG; 1109 1110 $editoroptions = [ 1111 'subdirs' => 0, 1112 'context' => $context, 1113 'maxfiles' => EDITOR_UNLIMITED_FILES, 1114 'maxbytes' => $CFG->maxbytes, 1115 'noclean' => 0, 1116 'trusttext' => 0, 1117 'autosave' => false 1118 ]; 1119 1120 return $editoroptions; 1121 } 1122 } 1123 1124 1125 /** 1126 * The interface for strategies for controlling which variant of each question is used. 1127 * 1128 * @copyright 2011 The Open University 1129 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1130 */ 1131 interface question_variant_selection_strategy { 1132 /** 1133 * @param int $maxvariants the num 1134 * @param string $seed data that can be used to controls how the variant is selected 1135 * in a semi-random way. 1136 * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive. 1137 */ 1138 public function choose_variant($maxvariants, $seed); 1139 } 1140 1141 1142 /** 1143 * A {@link question_variant_selection_strategy} that is completely random. 1144 * 1145 * @copyright 2011 The Open University 1146 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1147 */ 1148 class question_variant_random_strategy implements question_variant_selection_strategy { 1149 public function choose_variant($maxvariants, $seed) { 1150 return rand(1, $maxvariants); 1151 } 1152 } 1153 1154 1155 /** 1156 * A {@link question_variant_selection_strategy} that is effectively random 1157 * for the first attempt, and then after that cycles through the available 1158 * variants so that the students will not get a repeated variant until they have 1159 * seen them all. 1160 * 1161 * @copyright 2011 The Open University 1162 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1163 */ 1164 class question_variant_pseudorandom_no_repeats_strategy 1165 implements question_variant_selection_strategy { 1166 1167 /** @var int the number of attempts this users has had, including the curent one. */ 1168 protected $attemptno; 1169 1170 /** @var int the user id the attempt belongs to. */ 1171 protected $userid; 1172 1173 /** @var string extra input fed into the pseudo-random code. */ 1174 protected $extrarandomness = ''; 1175 1176 /** 1177 * Constructor. 1178 * @param int $attemptno The attempt number. 1179 * @param int $userid the user the attempt is for (defaults to $USER->id). 1180 */ 1181 public function __construct($attemptno, $userid = null, $extrarandomness = '') { 1182 $this->attemptno = $attemptno; 1183 if (is_null($userid)) { 1184 global $USER; 1185 $this->userid = $USER->id; 1186 } else { 1187 $this->userid = $userid; 1188 } 1189 1190 if ($extrarandomness) { 1191 $this->extrarandomness = '|' . $extrarandomness; 1192 } 1193 } 1194 1195 public function choose_variant($maxvariants, $seed) { 1196 if ($maxvariants == 1) { 1197 return 1; 1198 } 1199 1200 $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness); 1201 $randint = hexdec(substr($hash, 17, 7)); 1202 1203 return ($randint + $this->attemptno) % $maxvariants + 1; 1204 } 1205 } 1206 1207 /** 1208 * A {@link question_variant_selection_strategy} designed ONLY for testing. 1209 * For selected questions it wil return a specific variants. For the other 1210 * slots it will use a fallback strategy. 1211 * 1212 * @copyright 2013 The Open University 1213 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1214 */ 1215 class question_variant_forced_choices_selection_strategy 1216 implements question_variant_selection_strategy { 1217 1218 /** @var array seed => variant to select. */ 1219 protected $forcedchoices; 1220 1221 /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */ 1222 protected $basestrategy; 1223 1224 /** 1225 * Constructor. 1226 * @param array $forcedchoices array seed => variant to select. 1227 * @param question_variant_selection_strategy $basestrategy strategy used 1228 * to make the non-forced choices. 1229 */ 1230 public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) { 1231 $this->forcedchoices = $forcedchoices; 1232 $this->basestrategy = $basestrategy; 1233 } 1234 1235 public function choose_variant($maxvariants, $seed) { 1236 if (array_key_exists($seed, $this->forcedchoices)) { 1237 if ($this->forcedchoices[$seed] > $maxvariants) { 1238 throw new coding_exception('Forced variant out of range.'); 1239 } 1240 return $this->forcedchoices[$seed]; 1241 } else { 1242 return $this->basestrategy->choose_variant($maxvariants, $seed); 1243 } 1244 } 1245 1246 /** 1247 * Helper method for preparing the $forcedchoices array. 1248 * @param array $variantsbyslot slot number => variant to select. 1249 * @param question_usage_by_activity $quba the question usage we need a strategy for. 1250 * @throws coding_exception when variant cannot be forced as doesn't work. 1251 * @return array that can be passed to the constructor as $forcedchoices. 1252 */ 1253 public static function prepare_forced_choices_array(array $variantsbyslot, 1254 question_usage_by_activity $quba) { 1255 1256 $forcedchoices = array(); 1257 1258 foreach ($variantsbyslot as $slot => $varianttochoose) { 1259 $question = $quba->get_question($slot); 1260 $seed = $question->get_variants_selection_seed(); 1261 if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) { 1262 throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot); 1263 } 1264 if ($varianttochoose > $question->get_num_variants()) { 1265 throw new coding_exception('Forced variant out of range at slot ' . $slot); 1266 } 1267 $forcedchoices[$seed] = $varianttochoose; 1268 } 1269 return $forcedchoices; 1270 } 1271 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body