PageRenderTime 61ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/question/engine/lib.php

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