[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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(dirname(__FILE__) . '/questionusage.php'); 31 require_once(dirname(__FILE__) . '/questionattempt.php'); 32 require_once(dirname(__FILE__) . '/questionattemptstep.php'); 33 require_once(dirname(__FILE__) . '/states.php'); 34 require_once(dirname(__FILE__) . '/datalib.php'); 35 require_once(dirname(__FILE__) . '/renderer.php'); 36 require_once(dirname(__FILE__) . '/bank.php'); 37 require_once(dirname(__FILE__) . '/../type/questiontypebase.php'); 38 require_once(dirname(__FILE__) . '/../type/questionbase.php'); 39 require_once(dirname(__FILE__) . '/../type/rendererbase.php'); 40 require_once(dirname(__FILE__) . '/../behaviour/behaviourtypebase.php'); 41 require_once(dirname(__FILE__) . '/../behaviour/behaviourbase.php'); 42 require_once(dirname(__FILE__) . '/../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 object $context the context this usage belongs to. 70 * @return question_usage_by_activity the newly created object. 71 */ 72 public static function make_questions_usage_by_activity($component, $context) { 73 return new question_usage_by_activity($component, $context); 74 } 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 is_null($mark) || ($mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark); 147 } 148 149 /** 150 * @param array $questionids of question ids. 151 * @param qubaid_condition $qubaids ids of the usages to consider. 152 * @return boolean whether any of these questions are being used by any of 153 * those usages. 154 */ 155 public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) { 156 if (is_null($qubaids)) { 157 return false; 158 } 159 $dm = new question_engine_data_mapper(); 160 return $dm->questions_in_use($questionids, $qubaids); 161 } 162 163 /** 164 * Create an archetypal behaviour for a particular question attempt. 165 * Used by {@link question_definition::make_behaviour()}. 166 * 167 * @param string $preferredbehaviour the type of model required. 168 * @param question_attempt $qa the question attempt the model will process. 169 * @return question_behaviour an instance of appropriate behaviour class. 170 */ 171 public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) { 172 if (!self::is_behaviour_archetypal($preferredbehaviour)) { 173 throw new coding_exception('The requested behaviour is not actually ' . 174 'an archetypal one.'); 175 } 176 177 self::load_behaviour_class($preferredbehaviour); 178 $class = 'qbehaviour_' . $preferredbehaviour; 179 return new $class($qa, $preferredbehaviour); 180 } 181 182 /** 183 * @param string $behaviour the name of a behaviour. 184 * @return array of {@link question_display_options} field names, that are 185 * not relevant to this behaviour before a 'finish' action. 186 */ 187 public static function get_behaviour_unused_display_options($behaviour) { 188 return self::get_behaviour_type($behaviour)->get_unused_display_options(); 189 } 190 191 /** 192 * Create a behaviour for a particular type. If that type cannot be 193 * found, return an instance of qbehaviour_missing. 194 * 195 * Normally you should use {@link make_archetypal_behaviour()}, or 196 * call the constructor of a particular model class directly. This method 197 * is only intended for use by {@link question_attempt::load_from_records()}. 198 * 199 * @param string $behaviour the type of model to create. 200 * @param question_attempt $qa the question attempt the model will process. 201 * @param string $preferredbehaviour the preferred behaviour for the containing usage. 202 * @return question_behaviour an instance of appropriate behaviour class. 203 */ 204 public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) { 205 try { 206 self::load_behaviour_class($behaviour); 207 } catch (Exception $e) { 208 self::load_behaviour_class('missing'); 209 return new qbehaviour_missing($qa, $preferredbehaviour); 210 } 211 $class = 'qbehaviour_' . $behaviour; 212 return new $class($qa, $preferredbehaviour); 213 } 214 215 /** 216 * Load the behaviour class(es) belonging to a particular model. That is, 217 * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit 218 * of checking. 219 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'. 220 */ 221 public static function load_behaviour_class($behaviour) { 222 global $CFG; 223 if (isset(self::$loadedbehaviours[$behaviour])) { 224 return; 225 } 226 $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php'; 227 if (!is_readable($file)) { 228 throw new coding_exception('Unknown question behaviour ' . $behaviour); 229 } 230 include_once($file); 231 232 $class = 'qbehaviour_' . $behaviour; 233 if (!class_exists($class)) { 234 throw new coding_exception('Question behaviour ' . $behaviour . 235 ' does not define the required class ' . $class . '.'); 236 } 237 238 self::$loadedbehaviours[$behaviour] = 1; 239 } 240 241 /** 242 * Create a behaviour for a particular type. If that type cannot be 243 * found, return an instance of qbehaviour_missing. 244 * 245 * Normally you should use {@link make_archetypal_behaviour()}, or 246 * call the constructor of a particular model class directly. This method 247 * is only intended for use by {@link question_attempt::load_from_records()}. 248 * 249 * @param string $behaviour the type of model to create. 250 * @param question_attempt $qa the question attempt the model will process. 251 * @param string $preferredbehaviour the preferred behaviour for the containing usage. 252 * @return question_behaviour_type an instance of appropriate behaviour class. 253 */ 254 public static function get_behaviour_type($behaviour) { 255 256 if (array_key_exists($behaviour, self::$behaviourtypes)) { 257 return self::$behaviourtypes[$behaviour]; 258 } 259 260 self::load_behaviour_type_class($behaviour); 261 262 $class = 'qbehaviour_' . $behaviour . '_type'; 263 if (class_exists($class)) { 264 self::$behaviourtypes[$behaviour] = new $class(); 265 } else { 266 debugging('Question behaviour ' . $behaviour . 267 ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER); 268 self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour); 269 } 270 271 return self::$behaviourtypes[$behaviour]; 272 } 273 274 /** 275 * Load the behaviour type class for a particular behaviour. That is, 276 * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php'). 277 * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'. 278 */ 279 protected static function load_behaviour_type_class($behaviour) { 280 global $CFG; 281 if (isset(self::$behaviourtypes[$behaviour])) { 282 return; 283 } 284 $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php'; 285 if (!is_readable($file)) { 286 debugging('Question behaviour ' . $behaviour . 287 ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER); 288 } 289 include_once($file); 290 } 291 292 /** 293 * Return an array where the keys are the internal names of the archetypal 294 * behaviours, and the values are a human-readable name. An 295 * archetypal behaviour is one that is suitable to pass the name of to 296 * {@link question_usage_by_activity::set_preferred_behaviour()}. 297 * 298 * @return array model name => lang string for this behaviour name. 299 */ 300 public static function get_archetypal_behaviours() { 301 $archetypes = array(); 302 $behaviours = core_component::get_plugin_list('qbehaviour'); 303 foreach ($behaviours as $behaviour => $notused) { 304 if (self::is_behaviour_archetypal($behaviour)) { 305 $archetypes[$behaviour] = self::get_behaviour_name($behaviour); 306 } 307 } 308 asort($archetypes, SORT_LOCALE_STRING); 309 return $archetypes; 310 } 311 312 /** 313 * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'. 314 * @return bool whether this is an archetypal behaviour. 315 */ 316 public static function is_behaviour_archetypal($behaviour) { 317 return self::get_behaviour_type($behaviour)->is_archetypal(); 318 } 319 320 /** 321 * Return an array where the keys are the internal names of the behaviours 322 * in preferred order and the values are a human-readable name. 323 * 324 * @param array $archetypes, array of behaviours 325 * @param string $orderlist, a comma separated list of behaviour names 326 * @param string $disabledlist, a comma separated list of behaviour names 327 * @param string $current, current behaviour name 328 * @return array model name => lang string for this behaviour name. 329 */ 330 public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) { 331 332 // Get disabled behaviours 333 if ($disabledlist) { 334 $disabled = explode(',', $disabledlist); 335 } else { 336 $disabled = array(); 337 } 338 339 if ($orderlist) { 340 $order = explode(',', $orderlist); 341 } else { 342 $order = array(); 343 } 344 345 foreach ($disabled as $behaviour) { 346 if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) { 347 unset($archetypes[$behaviour]); 348 } 349 } 350 351 // Get behaviours in preferred order 352 $behaviourorder = array(); 353 foreach ($order as $behaviour) { 354 if (array_key_exists($behaviour, $archetypes)) { 355 $behaviourorder[$behaviour] = $archetypes[$behaviour]; 356 } 357 } 358 // Get the rest of behaviours and sort them alphabetically 359 $leftover = array_diff_key($archetypes, $behaviourorder); 360 asort($leftover, SORT_LOCALE_STRING); 361 362 // Set up the final order to be displayed 363 return $behaviourorder + $leftover; 364 } 365 366 /** 367 * Return an array where the keys are the internal names of the behaviours 368 * in preferred order and the values are a human-readable name. 369 * 370 * @param string $currentbehaviour 371 * @return array model name => lang string for this behaviour name. 372 */ 373 public static function get_behaviour_options($currentbehaviour) { 374 $config = question_bank::get_config(); 375 $archetypes = self::get_archetypal_behaviours(); 376 377 // If no admin setting return all behavious 378 if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) { 379 return $archetypes; 380 } 381 382 if (empty($config->behavioursortorder)) { 383 $order = ''; 384 } else { 385 $order = $config->behavioursortorder; 386 } 387 if (empty($config->disabledbehaviours)) { 388 $disabled = ''; 389 } else { 390 $disabled = $config->disabledbehaviours; 391 } 392 393 return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour); 394 } 395 396 /** 397 * Get the translated name of a behaviour, for display in the UI. 398 * @param string $behaviour the internal name of the model. 399 * @return string name from the current language pack. 400 */ 401 public static function get_behaviour_name($behaviour) { 402 return get_string('pluginname', 'qbehaviour_' . $behaviour); 403 } 404 405 /** 406 * @return array all the file area names that may contain response files. 407 */ 408 public static function get_all_response_file_areas() { 409 $variables = array(); 410 foreach (question_bank::get_all_qtypes() as $qtype) { 411 $variables += $qtype->response_file_areas(); 412 } 413 414 $areas = array(); 415 foreach (array_unique($variables) as $variable) { 416 $areas[] = 'response_' . $variable; 417 } 418 return $areas; 419 } 420 421 /** 422 * Returns the valid choices for the number of decimal places for showing 423 * question marks. For use in the user interface. 424 * @return array suitable for passing to {@link choose_from_menu()} or similar. 425 */ 426 public static function get_dp_options() { 427 return question_display_options::get_dp_options(); 428 } 429 430 /** 431 * Initialise the JavaScript required on pages where questions will be displayed. 432 */ 433 public static function initialise_js() { 434 return question_flags::initialise_js(); 435 } 436 } 437 438 439 /** 440 * This class contains all the options that controls how a question is displayed. 441 * 442 * Normally, what will happen is that the calling code will set up some display 443 * options to indicate what sort of question display it wants, and then before the 444 * question is rendered, the behaviour will be given a chance to modify the 445 * display options, so that, for example, A question that is finished will only 446 * be shown read-only, and a question that has not been submitted will not have 447 * any sort of feedback displayed. 448 * 449 * @copyright 2009 The Open University 450 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 451 */ 452 class question_display_options { 453 /**#@+ @var integer named constants for the values that most of the options take. */ 454 const HIDDEN = 0; 455 const VISIBLE = 1; 456 const EDITABLE = 2; 457 /**#@-*/ 458 459 /**#@+ @var integer named constants for the {@link $marks} option. */ 460 const MAX_ONLY = 1; 461 const MARK_AND_MAX = 2; 462 /**#@-*/ 463 464 /** 465 * @var integer maximum value for the {@link $markpd} option. This is 466 * effectively set by the database structure, which uses NUMBER(12,7) columns 467 * for question marks/fractions. 468 */ 469 const MAX_DP = 7; 470 471 /** 472 * @var boolean whether the question should be displayed as a read-only review, 473 * or in an active state where you can change the answer. 474 */ 475 public $readonly = false; 476 477 /** 478 * @var boolean whether the question type should output hidden form fields 479 * to reset any incorrect parts of the resonse to blank. 480 */ 481 public $clearwrong = false; 482 483 /** 484 * Should the student have what they got right and wrong clearly indicated. 485 * This includes the green/red hilighting of the bits of their response, 486 * whether the one-line summary of the current state of the question says 487 * correct/incorrect or just answered. 488 * @var integer {@link question_display_options::HIDDEN} or 489 * {@link question_display_options::VISIBLE} 490 */ 491 public $correctness = self::VISIBLE; 492 493 /** 494 * The the mark and/or the maximum available mark for this question be visible? 495 * @var integer {@link question_display_options::HIDDEN}, 496 * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX} 497 */ 498 public $marks = self::MARK_AND_MAX; 499 500 /** @var number of decimal places to use when formatting marks for output. */ 501 public $markdp = 2; 502 503 /** 504 * Should the flag this question UI element be visible, and if so, should the 505 * flag state be changable? 506 * @var integer {@link question_display_options::HIDDEN}, 507 * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE} 508 */ 509 public $flags = self::VISIBLE; 510 511 /** 512 * Should the specific feedback be visible. 513 * @var integer {@link question_display_options::HIDDEN} or 514 * {@link question_display_options::VISIBLE} 515 */ 516 public $feedback = self::VISIBLE; 517 518 /** 519 * For questions with a number of sub-parts (like matching, or 520 * multiple-choice, multiple-reponse) display the number of sub-parts that 521 * were correct. 522 * @var integer {@link question_display_options::HIDDEN} or 523 * {@link question_display_options::VISIBLE} 524 */ 525 public $numpartscorrect = self::VISIBLE; 526 527 /** 528 * Should the general feedback be visible? 529 * @var integer {@link question_display_options::HIDDEN} or 530 * {@link question_display_options::VISIBLE} 531 */ 532 public $generalfeedback = self::VISIBLE; 533 534 /** 535 * Should the automatically generated display of what the correct answer is 536 * be visible? 537 * @var integer {@link question_display_options::HIDDEN} or 538 * {@link question_display_options::VISIBLE} 539 */ 540 public $rightanswer = self::VISIBLE; 541 542 /** 543 * Should the manually added marker's comment be visible. Should the link for 544 * adding/editing the comment be there. 545 * @var integer {@link question_display_options::HIDDEN}, 546 * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}. 547 * Editable means that form fields are displayed inline. 548 */ 549 public $manualcomment = self::VISIBLE; 550 551 /** 552 * Should we show a 'Make comment or override grade' link? 553 * @var string base URL for the edit comment script, which will be shown if 554 * $manualcomment = self::VISIBLE. 555 */ 556 public $manualcommentlink = null; 557 558 /** 559 * Used in places like the question history table, to show a link to review 560 * this question in a certain state. If blank, a link is not shown. 561 * @var string base URL for a review question script. 562 */ 563 public $questionreviewlink = null; 564 565 /** 566 * Should the history of previous question states table be visible? 567 * @var integer {@link question_display_options::HIDDEN} or 568 * {@link question_display_options::VISIBLE} 569 */ 570 public $history = self::HIDDEN; 571 572 /** 573 * If not empty, then a link to edit the question will be included in 574 * the info box for the question. 575 * 576 * If used, this array must contain an element courseid or cmid. 577 * 578 * It shoudl also contain a parameter returnurl => moodle_url giving a 579 * sensible URL to go back to when the editing form is submitted or cancelled. 580 * 581 * @var array url parameter for the edit link. id => questiosnid will be 582 * added automatically. 583 */ 584 public $editquestionparams = array(); 585 586 /** 587 * @var int the context the attempt being output belongs to. 588 */ 589 public $context; 590 591 /** 592 * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback}, 593 * {@link rightanswer} and {@link manualcomment} to 594 * {@link question_display_options::HIDDEN}. 595 */ 596 public function hide_all_feedback() { 597 $this->feedback = self::HIDDEN; 598 $this->numpartscorrect = self::HIDDEN; 599 $this->generalfeedback = self::HIDDEN; 600 $this->rightanswer = self::HIDDEN; 601 $this->manualcomment = self::HIDDEN; 602 $this->correctness = self::HIDDEN; 603 } 604 605 /** 606 * Returns the valid choices for the number of decimal places for showing 607 * question marks. For use in the user interface. 608 * 609 * Calling code should probably use {@link question_engine::get_dp_options()} 610 * rather than calling this method directly. 611 * 612 * @return array suitable for passing to {@link choose_from_menu()} or similar. 613 */ 614 public static function get_dp_options() { 615 $options = array(); 616 for ($i = 0; $i <= self::MAX_DP; $i += 1) { 617 $options[$i] = $i; 618 } 619 return $options; 620 } 621 } 622 623 624 /** 625 * Contains the logic for handling question flags. 626 * 627 * @copyright 2010 The Open University 628 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 629 */ 630 abstract class question_flags { 631 /** 632 * Get the checksum that validates that a toggle request is valid. 633 * @param int $qubaid the question usage id. 634 * @param int $questionid the question id. 635 * @param int $sessionid the question_attempt id. 636 * @param object $user the user. If null, defaults to $USER. 637 * @return string that needs to be sent to question/toggleflag.php for it to work. 638 */ 639 protected static function get_toggle_checksum($qubaid, $questionid, 640 $qaid, $slot, $user = null) { 641 if (is_null($user)) { 642 global $USER; 643 $user = $USER; 644 } 645 return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot); 646 } 647 648 /** 649 * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state. 650 * You need to append &newstate=0/1 to this. 651 * @return the post data to send. 652 */ 653 public static function get_postdata(question_attempt $qa) { 654 $qaid = $qa->get_database_id(); 655 $qubaid = $qa->get_usage_id(); 656 $qid = $qa->get_question()->id; 657 $slot = $qa->get_slot(); 658 $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot); 659 return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" . 660 sesskey() . '&newstate='; 661 } 662 663 /** 664 * If the request seems valid, update the flag state of a question attempt. 665 * Throws exceptions if this is not a valid update request. 666 * @param int $qubaid the question usage id. 667 * @param int $questionid the question id. 668 * @param int $sessionid the question_attempt id. 669 * @param string $checksum checksum, as computed by {@link get_toggle_checksum()} 670 * corresponding to the last three arguments. 671 * @param bool $newstate the new state of the flag. true = flagged. 672 */ 673 public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) { 674 // Check the checksum - it is very hard to know who a question session belongs 675 // to, so we require that checksum parameter is matches an md5 hash of the 676 // three ids and the users username. Since we are only updating a flag, that 677 // probably makes it sufficiently difficult for malicious users to toggle 678 // other users flags. 679 if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) { 680 throw new moodle_exception('errorsavingflags', 'question'); 681 } 682 683 $dm = new question_engine_data_mapper(); 684 $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate); 685 } 686 687 public static function initialise_js() { 688 global $CFG, $PAGE, $OUTPUT; 689 static $done = false; 690 if ($done) { 691 return; 692 } 693 $module = array( 694 'name' => 'core_question_flags', 695 'fullpath' => '/question/flags.js', 696 'requires' => array('base', 'dom', 'event-delegate', 'io-base'), 697 ); 698 $actionurl = $CFG->wwwroot . '/question/toggleflag.php'; 699 $flagtext = array( 700 0 => get_string('clickflag', 'question'), 701 1 => get_string('clickunflag', 'question') 702 ); 703 $flagattributes = array( 704 0 => array( 705 'src' => $OUTPUT->pix_url('i/unflagged') . '', 706 'title' => get_string('clicktoflag', 'question'), 707 'alt' => get_string('notflagged', 'question'), 708 // 'text' => get_string('clickflag', 'question'), 709 ), 710 1 => array( 711 'src' => $OUTPUT->pix_url('i/flagged') . '', 712 'title' => get_string('clicktounflag', 'question'), 713 'alt' => get_string('flagged', 'question'), 714 // 'text' => get_string('clickunflag', 'question'), 715 ), 716 ); 717 $PAGE->requires->js_init_call('M.core_question_flags.init', 718 array($actionurl, $flagattributes, $flagtext), false, $module); 719 $done = true; 720 } 721 } 722 723 724 /** 725 * Exception thrown when the system detects that a student has done something 726 * out-of-order to a question. This can happen, for example, if they click 727 * the browser's back button in a quiz, then try to submit a different response. 728 * 729 * @copyright 2010 The Open University 730 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 731 */ 732 class question_out_of_sequence_exception extends moodle_exception { 733 public function __construct($qubaid, $slot, $postdata) { 734 if ($postdata == null) { 735 $postdata = data_submitted(); 736 } 737 parent::__construct('submissionoutofsequence', 'question', '', null, 738 "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true)); 739 } 740 } 741 742 743 /** 744 * Useful functions for writing question types and behaviours. 745 * 746 * @copyright 2010 The Open University 747 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 748 */ 749 abstract class question_utils { 750 /** 751 * Tests to see whether two arrays have the same keys, with the same values 752 * (as compared by ===) for each key. However, the order of the arrays does 753 * not have to be the same. 754 * @param array $array1 the first array. 755 * @param array $array2 the second array. 756 * @return bool whether the two arrays have the same keys with the same 757 * corresponding values. 758 */ 759 public static function arrays_have_same_keys_and_values(array $array1, array $array2) { 760 if (count($array1) != count($array2)) { 761 return false; 762 } 763 foreach ($array1 as $key => $value1) { 764 if (!array_key_exists($key, $array2)) { 765 return false; 766 } 767 if (((string) $value1) !== ((string) $array2[$key])) { 768 return false; 769 } 770 } 771 return true; 772 } 773 774 /** 775 * Tests to see whether two arrays have the same value at a particular key. 776 * This method will return true if: 777 * 1. Neither array contains the key; or 778 * 2. Both arrays contain the key, and the corresponding values compare 779 * identical when cast to strings and compared with ===. 780 * @param array $array1 the first array. 781 * @param array $array2 the second array. 782 * @param string $key an array key. 783 * @return bool whether the two arrays have the same value (or lack of 784 * one) for a given key. 785 */ 786 public static function arrays_same_at_key(array $array1, array $array2, $key) { 787 if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) { 788 return ((string) $array1[$key]) === ((string) $array2[$key]); 789 } 790 if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) { 791 return true; 792 } 793 return false; 794 } 795 796 /** 797 * Tests to see whether two arrays have the same value at a particular key. 798 * Missing values are replaced by '', and then the values are cast to 799 * strings and compared with ===. 800 * @param array $array1 the first array. 801 * @param array $array2 the second array. 802 * @param string $key an array key. 803 * @return bool whether the two arrays have the same value (or lack of 804 * one) for a given key. 805 */ 806 public static function arrays_same_at_key_missing_is_blank( 807 array $array1, array $array2, $key) { 808 if (array_key_exists($key, $array1)) { 809 $value1 = $array1[$key]; 810 } else { 811 $value1 = ''; 812 } 813 if (array_key_exists($key, $array2)) { 814 $value2 = $array2[$key]; 815 } else { 816 $value2 = ''; 817 } 818 return ((string) $value1) === ((string) $value2); 819 } 820 821 /** 822 * Tests to see whether two arrays have the same value at a particular key. 823 * Missing values are replaced by 0, and then the values are cast to 824 * integers and compared with ===. 825 * @param array $array1 the first array. 826 * @param array $array2 the second array. 827 * @param string $key an array key. 828 * @return bool whether the two arrays have the same value (or lack of 829 * one) for a given key. 830 */ 831 public static function arrays_same_at_key_integer( 832 array $array1, array $array2, $key) { 833 if (array_key_exists($key, $array1)) { 834 $value1 = (int) $array1[$key]; 835 } else { 836 $value1 = 0; 837 } 838 if (array_key_exists($key, $array2)) { 839 $value2 = (int) $array2[$key]; 840 } else { 841 $value2 = 0; 842 } 843 return $value1 === $value2; 844 } 845 846 private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix'); 847 private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc'); 848 private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm'); 849 private static $thousands = array('', 'm', 'mm', 'mmm'); 850 851 /** 852 * Convert an integer to roman numerals. 853 * @param int $number an integer between 1 and 3999 inclusive. Anything else 854 * will throw an exception. 855 * @return string the number converted to lower case roman numerals. 856 */ 857 public static function int_to_roman($number) { 858 if (!is_integer($number) || $number < 1 || $number > 3999) { 859 throw new coding_exception('Only integers between 0 and 3999 can be ' . 860 'converted to roman numerals.', $number); 861 } 862 863 return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] . 864 self::$tens[$number / 10 % 10] . self::$units[$number % 10]; 865 } 866 867 /** 868 * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED). 869 * This method copes with: 870 * - keeping null or '' input unchanged. 871 * - nubmers that were typed as either 1.00 or 1,00 form. 872 * 873 * @param string|null $mark raw use input of a mark. 874 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 875 */ 876 public static function clean_param_mark($mark) { 877 if ($mark === '' || is_null($mark)) { 878 return $mark; 879 } 880 881 return clean_param(str_replace(',', '.', $mark), PARAM_FLOAT); 882 } 883 884 /** 885 * Get a sumitted variable (from the GET or POST data) that is a mark. 886 * @param string $parname the submitted variable name. 887 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 888 */ 889 public static function optional_param_mark($parname) { 890 return self::clean_param_mark( 891 optional_param($parname, null, PARAM_RAW_TRIMMED)); 892 } 893 894 /** 895 * Convert part of some question content to plain text. 896 * @param string $text the text. 897 * @param int $format the text format. 898 * @param array $options formatting options. Passed to {@link format_text}. 899 * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null. 900 */ 901 public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) { 902 // The following call to html_to_text uses the option that strips out 903 // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens. 904 // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't 905 // matter what. We use http://example.com/. 906 $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text); 907 return html_to_text(format_text($text, $format, $options), 0, false); 908 } 909 } 910 911 912 /** 913 * The interface for strategies for controlling which variant of each question is used. 914 * 915 * @copyright 2011 The Open University 916 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 917 */ 918 interface question_variant_selection_strategy { 919 /** 920 * @param int $maxvariants the num 921 * @param string $seed data that can be used to controls how the variant is selected 922 * in a semi-random way. 923 * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive. 924 */ 925 public function choose_variant($maxvariants, $seed); 926 } 927 928 929 /** 930 * A {@link question_variant_selection_strategy} that is completely random. 931 * 932 * @copyright 2011 The Open University 933 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 934 */ 935 class question_variant_random_strategy implements question_variant_selection_strategy { 936 public function choose_variant($maxvariants, $seed) { 937 return rand(1, $maxvariants); 938 } 939 } 940 941 942 /** 943 * A {@link question_variant_selection_strategy} that is effectively random 944 * for the first attempt, and then after that cycles through the available 945 * variants so that the students will not get a repeated variant until they have 946 * seen them all. 947 * 948 * @copyright 2011 The Open University 949 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 950 */ 951 class question_variant_pseudorandom_no_repeats_strategy 952 implements question_variant_selection_strategy { 953 954 /** @var int the number of attempts this users has had, including the curent one. */ 955 protected $attemptno; 956 957 /** @var int the user id the attempt belongs to. */ 958 protected $userid; 959 960 /** @var string extra input fed into the pseudo-random code. */ 961 protected $extrarandomness = ''; 962 963 /** 964 * Constructor. 965 * @param int $attemptno The attempt number. 966 * @param int $userid the user the attempt is for (defaults to $USER->id). 967 */ 968 public function __construct($attemptno, $userid = null, $extrarandomness = '') { 969 $this->attemptno = $attemptno; 970 if (is_null($userid)) { 971 global $USER; 972 $this->userid = $USER->id; 973 } else { 974 $this->userid = $userid; 975 } 976 977 if ($extrarandomness) { 978 $this->extrarandomness = '|' . $extrarandomness; 979 } 980 } 981 982 public function choose_variant($maxvariants, $seed) { 983 if ($maxvariants == 1) { 984 return 1; 985 } 986 987 $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness); 988 $randint = hexdec(substr($hash, 17, 7)); 989 990 return ($randint + $this->attemptno) % $maxvariants + 1; 991 } 992 } 993 994 /** 995 * A {@link question_variant_selection_strategy} designed ONLY for testing. 996 * For selected questions it wil return a specific variants. For the other 997 * slots it will use a fallback strategy. 998 * 999 * @copyright 2013 The Open University 1000 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1001 */ 1002 class question_variant_forced_choices_selection_strategy 1003 implements question_variant_selection_strategy { 1004 1005 /** @var array seed => variant to select. */ 1006 protected $forcedchoices; 1007 1008 /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */ 1009 protected $basestrategy; 1010 1011 /** 1012 * Constructor. 1013 * @param array $forcedchoices array seed => variant to select. 1014 * @param question_variant_selection_strategy $basestrategy strategy used 1015 * to make the non-forced choices. 1016 */ 1017 public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) { 1018 $this->forcedchoices = $forcedchoices; 1019 $this->basestrategy = $basestrategy; 1020 } 1021 1022 public function choose_variant($maxvariants, $seed) { 1023 if (array_key_exists($seed, $this->forcedchoices)) { 1024 if ($this->forcedchoices[$seed] > $maxvariants) { 1025 throw new coding_exception('Forced variant out of range.'); 1026 } 1027 return $this->forcedchoices[$seed]; 1028 } else { 1029 return $this->basestrategy->choose_variant($maxvariants, $seed); 1030 } 1031 } 1032 1033 /** 1034 * Helper method for preparing the $forcedchoices array. 1035 * @param array $variantsbyslot slot number => variant to select. 1036 * @param question_usage_by_activity $quba the question usage we need a strategy for. 1037 * @throws coding_exception when variant cannot be forced as doesn't work. 1038 * @return array that can be passed to the constructor as $forcedchoices. 1039 */ 1040 public static function prepare_forced_choices_array(array $variantsbyslot, 1041 question_usage_by_activity $quba) { 1042 1043 $forcedchoices = array(); 1044 1045 foreach ($variantsbyslot as $slot => $varianttochoose) { 1046 $question = $quba->get_question($slot); 1047 $seed = $question->get_variants_selection_seed(); 1048 if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) { 1049 throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot); 1050 } 1051 if ($varianttochoose > $question->get_num_variants()) { 1052 throw new coding_exception('Forced variant out of range at slot ' . $slot); 1053 } 1054 $forcedchoices[$seed] = $varianttochoose; 1055 } 1056 return $forcedchoices; 1057 } 1058 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |