questionusage.php 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  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 file defines the question usage class, and a few related classes.
  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. /**
  26. * This class keeps track of a group of questions that are being attempted,
  27. * and which state, and so on, each one is currently in.
  28. *
  29. * A quiz attempt or a lesson attempt could use an instance of this class to
  30. * keep track of all the questions in the attempt and process student submissions.
  31. * It is basically a collection of {@question_attempt} objects.
  32. *
  33. * The questions being attempted as part of this usage are identified by an integer
  34. * that is passed into many of the methods as $slot. ($question->id is not
  35. * used so that the same question can be used more than once in an attempt.)
  36. *
  37. * Normally, calling code should be able to do everything it needs to be calling
  38. * methods of this class. You should not normally need to get individual
  39. * {@question_attempt} objects and play around with their inner workind, in code
  40. * that it outside the quetsion engine.
  41. *
  42. * Instances of this class correspond to rows in the question_usages table.
  43. *
  44. * @copyright 2009 The Open University
  45. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46. */
  47. class question_usage_by_activity {
  48. /**
  49. * @var integer|string the id for this usage. If this usage was loaded from
  50. * the database, then this is the database id. Otherwise a unique random
  51. * string is used.
  52. */
  53. protected $id = null;
  54. /**
  55. * @var string name of an archetypal behaviour, that should be used
  56. * by questions in this usage if possible.
  57. */
  58. protected $preferredbehaviour = null;
  59. /** @var context the context this usage belongs to. */
  60. protected $context;
  61. /** @var string plugin name of the plugin this usage belongs to. */
  62. protected $owningcomponent;
  63. /** @var array {@link question_attempt}s that make up this usage. */
  64. protected $questionattempts = array();
  65. /** @var question_usage_observer that tracks changes to this usage. */
  66. protected $observer;
  67. /**
  68. * Create a new instance. Normally, calling code should use
  69. * {@link question_engine::make_questions_usage_by_activity()} or
  70. * {@link question_engine::load_questions_usage_by_activity()} rather than
  71. * calling this constructor directly.
  72. *
  73. * @param string $component the plugin creating this attempt. For example mod_quiz.
  74. * @param object $context the context this usage belongs to.
  75. */
  76. public function __construct($component, $context) {
  77. $this->owningcomponent = $component;
  78. $this->context = $context;
  79. $this->observer = new question_usage_null_observer();
  80. }
  81. /**
  82. * @param string $behaviour the name of an archetypal behaviour, that should
  83. * be used by questions in this usage if possible.
  84. */
  85. public function set_preferred_behaviour($behaviour) {
  86. $this->preferredbehaviour = $behaviour;
  87. $this->observer->notify_modified();
  88. }
  89. /** @return string the name of the preferred behaviour. */
  90. public function get_preferred_behaviour() {
  91. return $this->preferredbehaviour;
  92. }
  93. /** @return context the context this usage belongs to. */
  94. public function get_owning_context() {
  95. return $this->context;
  96. }
  97. /** @return string the name of the plugin that owns this attempt. */
  98. public function get_owning_component() {
  99. return $this->owningcomponent;
  100. }
  101. /** @return int|string If this usage came from the database, then the id
  102. * from the question_usages table is returned. Otherwise a random string is
  103. * returned. */
  104. public function get_id() {
  105. if (is_null($this->id)) {
  106. $this->id = random_string(10);
  107. }
  108. return $this->id;
  109. }
  110. /**
  111. * For internal use only. Used by {@link question_engine_data_mapper} to set
  112. * the id when a usage is saved to the database.
  113. * @param int $id the newly determined id for this usage.
  114. */
  115. public function set_id_from_database($id) {
  116. $this->id = $id;
  117. foreach ($this->questionattempts as $qa) {
  118. $qa->set_usage_id($id);
  119. }
  120. }
  121. /** @return question_usage_observer that is tracking changes made to this usage. */
  122. public function get_observer() {
  123. return $this->observer;
  124. }
  125. /**
  126. * You should almost certainly not call this method from your code. It is for
  127. * internal use only.
  128. * @param question_usage_observer that should be used to tracking changes made to this usage.
  129. */
  130. public function set_observer($observer) {
  131. $this->observer = $observer;
  132. foreach ($this->questionattempts as $qa) {
  133. $qa->set_observer($observer);
  134. }
  135. }
  136. /**
  137. * Add another question to this usage.
  138. *
  139. * The added question is not started until you call {@link start_question()}
  140. * on it.
  141. *
  142. * @param question_definition $question the question to add.
  143. * @param number $maxmark the maximum this question will be marked out of in
  144. * this attempt (optional). If not given, $question->defaultmark is used.
  145. * @return int the number used to identify this question within this usage.
  146. */
  147. public function add_question(question_definition $question, $maxmark = null) {
  148. $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark);
  149. $qa->set_slot($this->next_slot_number());
  150. $this->questionattempts[$this->next_slot_number()] = $qa;
  151. $this->observer->notify_attempt_added($qa);
  152. return $qa->get_slot();
  153. }
  154. /**
  155. * Add another question to this usage, in the place of an existing slot.
  156. * The question_attempt that was in that slot is moved to the end at a new
  157. * slot number, which is returned.
  158. *
  159. * The added question is not started until you call {@link start_question()}
  160. * on it.
  161. *
  162. * @param int $slot the slot-number of the question to replace.
  163. * @param question_definition $question the question to add.
  164. * @param number $maxmark the maximum this question will be marked out of in
  165. * this attempt (optional). If not given, the max mark from the $qa we
  166. * are replacing is used.
  167. * @return int the new slot number of the question that was displaced.
  168. */
  169. public function add_question_in_place_of_other($slot, question_definition $question, $maxmark = null) {
  170. $newslot = $this->next_slot_number();
  171. $oldqa = $this->get_question_attempt($slot);
  172. $oldqa->set_slot($newslot);
  173. $this->questionattempts[$newslot] = $oldqa;
  174. if ($maxmark === null) {
  175. $maxmark = $oldqa->get_max_mark();
  176. }
  177. $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark);
  178. $qa->set_slot($slot);
  179. $this->questionattempts[$slot] = $qa;
  180. $this->observer->notify_attempt_moved($oldqa, $slot);
  181. $this->observer->notify_attempt_added($qa);
  182. return $newslot;
  183. }
  184. /**
  185. * The slot number that will be allotted to the next question added.
  186. */
  187. public function next_slot_number() {
  188. return count($this->questionattempts) + 1;
  189. }
  190. /**
  191. * Get the question_definition for a question in this attempt.
  192. * @param int $slot the number used to identify this question within this usage.
  193. * @return question_definition the requested question object.
  194. */
  195. public function get_question($slot) {
  196. return $this->get_question_attempt($slot)->get_question();
  197. }
  198. /** @return array all the identifying numbers of all the questions in this usage. */
  199. public function get_slots() {
  200. return array_keys($this->questionattempts);
  201. }
  202. /** @return int the identifying number of the first question that was added to this usage. */
  203. public function get_first_question_number() {
  204. reset($this->questionattempts);
  205. return key($this->questionattempts);
  206. }
  207. /** @return int the number of questions that are currently in this usage. */
  208. public function question_count() {
  209. return count($this->questionattempts);
  210. }
  211. /**
  212. * Note the part of the {@link question_usage_by_activity} comment that explains
  213. * that {@link question_attempt} objects should be considered part of the inner
  214. * workings of the question engine, and should not, if possible, be accessed directly.
  215. *
  216. * @return question_attempt_iterator for iterating over all the questions being
  217. * attempted. as part of this usage.
  218. */
  219. public function get_attempt_iterator() {
  220. return new question_attempt_iterator($this);
  221. }
  222. /**
  223. * Check whether $number actually corresponds to a question attempt that is
  224. * part of this usage. Throws an exception if not.
  225. *
  226. * @param int $slot a number allegedly identifying a question within this usage.
  227. */
  228. protected function check_slot($slot) {
  229. if (!array_key_exists($slot, $this->questionattempts)) {
  230. throw new coding_exception('There is no question_attempt number ' . $slot .
  231. ' in this attempt.');
  232. }
  233. }
  234. /**
  235. * Note the part of the {@link question_usage_by_activity} comment that explains
  236. * that {@link question_attempt} objects should be considered part of the inner
  237. * workings of the question engine, and should not, if possible, be accessed directly.
  238. *
  239. * @param int $slot the number used to identify this question within this usage.
  240. * @return question_attempt the corresponding {@link question_attempt} object.
  241. */
  242. public function get_question_attempt($slot) {
  243. $this->check_slot($slot);
  244. return $this->questionattempts[$slot];
  245. }
  246. /**
  247. * Get the current state of the attempt at a question.
  248. * @param int $slot the number used to identify this question within this usage.
  249. * @return question_state.
  250. */
  251. public function get_question_state($slot) {
  252. return $this->get_question_attempt($slot)->get_state();
  253. }
  254. /**
  255. * @param int $slot the number used to identify this question within this usage.
  256. * @param bool $showcorrectness Whether right/partial/wrong states should
  257. * be distinguised.
  258. * @return string A brief textual description of the current state.
  259. */
  260. public function get_question_state_string($slot, $showcorrectness) {
  261. return $this->get_question_attempt($slot)->get_state_string($showcorrectness);
  262. }
  263. /**
  264. * @param int $slot the number used to identify this question within this usage.
  265. * @param bool $showcorrectness Whether right/partial/wrong states should
  266. * be distinguised.
  267. * @return string a CSS class name for the current state.
  268. */
  269. public function get_question_state_class($slot, $showcorrectness) {
  270. return $this->get_question_attempt($slot)->get_state_class($showcorrectness);
  271. }
  272. /**
  273. * Whether this attempt at a given question could be completed just by the
  274. * student interacting with the question, before {@link finish_question()} is called.
  275. *
  276. * @param int $slot the number used to identify this question within this usage.
  277. * @return boolean whether the attempt at the given question can finish naturally.
  278. */
  279. public function can_question_finish_during_attempt($slot) {
  280. return $this->get_question_attempt($slot)->can_finish_during_attempt();
  281. }
  282. /**
  283. * Get the time of the most recent action performed on a question.
  284. * @param int $slot the number used to identify this question within this usage.
  285. * @return int timestamp.
  286. */
  287. public function get_question_action_time($slot) {
  288. return $this->get_question_attempt($slot)->get_last_action_time();
  289. }
  290. /**
  291. * Get the current fraction awarded for the attempt at a question.
  292. * @param int $slot the number used to identify this question within this usage.
  293. * @return number|null The current fraction for this question, or null if one has
  294. * not been assigned yet.
  295. */
  296. public function get_question_fraction($slot) {
  297. return $this->get_question_attempt($slot)->get_fraction();
  298. }
  299. /**
  300. * Get the current mark awarded for the attempt at a question.
  301. * @param int $slot the number used to identify this question within this usage.
  302. * @return number|null The current mark for this question, or null if one has
  303. * not been assigned yet.
  304. */
  305. public function get_question_mark($slot) {
  306. return $this->get_question_attempt($slot)->get_mark();
  307. }
  308. /**
  309. * Get the maximum mark possible for the attempt at a question.
  310. * @param int $slot the number used to identify this question within this usage.
  311. * @return number the available marks for this question.
  312. */
  313. public function get_question_max_mark($slot) {
  314. return $this->get_question_attempt($slot)->get_max_mark();
  315. }
  316. /**
  317. * Get the total mark for all questions in this usage.
  318. * @return number The sum of marks of all the question_attempts in this usage.
  319. */
  320. public function get_total_mark() {
  321. $mark = 0;
  322. foreach ($this->questionattempts as $qa) {
  323. if ($qa->get_max_mark() > 0 && $qa->get_state() == question_state::$needsgrading) {
  324. return null;
  325. }
  326. $mark += $qa->get_mark();
  327. }
  328. return $mark;
  329. }
  330. /**
  331. * Get summary information about this usage.
  332. *
  333. * Some behaviours may be able to provide interesting summary information
  334. * about the attempt as a whole, and this method provides access to that data.
  335. * To see how this works, try setting a quiz to one of the CBM behaviours,
  336. * and then look at the extra information displayed at the top of the quiz
  337. * review page once you have sumitted an attempt.
  338. *
  339. * In the return value, the array keys are identifiers of the form
  340. * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary.
  341. * The values are arrays with two items, title and content. Each of these
  342. * will be either a string, or a renderable.
  343. *
  344. * @return array as described above.
  345. */
  346. public function get_summary_information(question_display_options $options) {
  347. return question_engine::get_behaviour_type($this->preferredbehaviour)
  348. ->summarise_usage($this, $options);
  349. }
  350. /**
  351. * @return string a simple textual summary of the question that was asked.
  352. */
  353. public function get_question_summary($slot) {
  354. return $this->get_question_attempt($slot)->get_question_summary();
  355. }
  356. /**
  357. * @return string a simple textual summary of response given.
  358. */
  359. public function get_response_summary($slot) {
  360. return $this->get_question_attempt($slot)->get_response_summary();
  361. }
  362. /**
  363. * @return string a simple textual summary of the correct resonse.
  364. */
  365. public function get_right_answer_summary($slot) {
  366. return $this->get_question_attempt($slot)->get_right_answer_summary();
  367. }
  368. /**
  369. * Return one of the bits of metadata for a particular question attempt in
  370. * this usage.
  371. * @param int $slot the slot number of the question of inereest.
  372. * @param string $name the name of the metadata variable to return.
  373. * @return string the value of that metadata variable.
  374. */
  375. public function get_question_attempt_metadata($slot, $name) {
  376. return $this->get_question_attempt($slot)->get_metadata($name);
  377. }
  378. /**
  379. * Set some metadata for a particular question attempt in this usage.
  380. * @param int $slot the slot number of the question of inerest.
  381. * @param string $name the name of the metadata variable to return.
  382. * @param string $value the value to set that metadata variable to.
  383. */
  384. public function set_question_attempt_metadata($slot, $name, $value) {
  385. $this->get_question_attempt($slot)->set_metadata($name, $value);
  386. }
  387. /**
  388. * Get the {@link core_question_renderer}, in collaboration with appropriate
  389. * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
  390. * HTML to display this question.
  391. * @param int $slot the number used to identify this question within this usage.
  392. * @param question_display_options $options controls how the question is rendered.
  393. * @param string|null $number The question number to display. 'i' is a special
  394. * value that gets displayed as Information. Null means no number is displayed.
  395. * @return string HTML fragment representing the question.
  396. */
  397. public function render_question($slot, $options, $number = null) {
  398. $options->context = $this->context;
  399. return $this->get_question_attempt($slot)->render($options, $number);
  400. }
  401. /**
  402. * Generate any bits of HTML that needs to go in the <head> tag when this question
  403. * is displayed in the body.
  404. * @param int $slot the number used to identify this question within this usage.
  405. * @return string HTML fragment.
  406. */
  407. public function render_question_head_html($slot) {
  408. //$options->context = $this->context;
  409. return $this->get_question_attempt($slot)->render_head_html();
  410. }
  411. /**
  412. * Like {@link render_question()} but displays the question at the past step
  413. * indicated by $seq, rather than showing the latest step.
  414. *
  415. * @param int $slot the number used to identify this question within this usage.
  416. * @param int $seq the seq number of the past state to display.
  417. * @param question_display_options $options controls how the question is rendered.
  418. * @param string|null $number The question number to display. 'i' is a special
  419. * value that gets displayed as Information. Null means no number is displayed.
  420. * @return string HTML fragment representing the question.
  421. */
  422. public function render_question_at_step($slot, $seq, $options, $number = null) {
  423. $options->context = $this->context;
  424. return $this->get_question_attempt($slot)->render_at_step(
  425. $seq, $options, $number, $this->preferredbehaviour);
  426. }
  427. /**
  428. * Checks whether the users is allow to be served a particular file.
  429. * @param int $slot the number used to identify this question within this usage.
  430. * @param question_display_options $options the options that control display of the question.
  431. * @param string $component the name of the component we are serving files for.
  432. * @param string $filearea the name of the file area.
  433. * @param array $args the remaining bits of the file path.
  434. * @param bool $forcedownload whether the user must be forced to download the file.
  435. * @return bool true if the user can access this file.
  436. */
  437. public function check_file_access($slot, $options, $component, $filearea,
  438. $args, $forcedownload) {
  439. return $this->get_question_attempt($slot)->check_file_access(
  440. $options, $component, $filearea, $args, $forcedownload);
  441. }
  442. /**
  443. * Replace a particular question_attempt with a different one.
  444. *
  445. * For internal use only. Used when reloading the state of a question from the
  446. * database.
  447. *
  448. * @param array $records Raw records loaded from the database.
  449. * @param int $questionattemptid The id of the question_attempt to extract.
  450. * @return question_attempt The newly constructed question_attempt_step.
  451. */
  452. public function replace_loaded_question_attempt_info($slot, $qa) {
  453. $this->check_slot($slot);
  454. $this->questionattempts[$slot] = $qa;
  455. }
  456. /**
  457. * You should probably not use this method in code outside the question engine.
  458. * The main reason for exposing it was for the benefit of unit tests.
  459. * @param int $slot the number used to identify this question within this usage.
  460. * @return string return the prefix that is pre-pended to field names in the HTML
  461. * that is output.
  462. */
  463. public function get_field_prefix($slot) {
  464. return $this->get_question_attempt($slot)->get_field_prefix();
  465. }
  466. /**
  467. * Get the number of variants available for the question in this slot.
  468. * @param int $slot the number used to identify this question within this usage.
  469. * @return int the number of variants available.
  470. */
  471. public function get_num_variants($slot) {
  472. return $this->get_question_attempt($slot)->get_question()->get_num_variants();
  473. }
  474. /**
  475. * Get the variant of the question being used in a given slot.
  476. * @param int $slot the number used to identify this question within this usage.
  477. * @return int the variant of this question that is being used.
  478. */
  479. public function get_variant($slot) {
  480. return $this->get_question_attempt($slot)->get_variant();
  481. }
  482. /**
  483. * Start the attempt at a question that has been added to this usage.
  484. * @param int $slot the number used to identify this question within this usage.
  485. * @param int $variant which variant of the question to use. Must be between
  486. * 1 and ->get_num_variants($slot) inclusive. If not give, a variant is
  487. * chosen at random.
  488. * @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
  489. */
  490. public function start_question($slot, $variant = null, $timenow = null) {
  491. if (is_null($variant)) {
  492. $variant = rand(1, $this->get_num_variants($slot));
  493. }
  494. $qa = $this->get_question_attempt($slot);
  495. $qa->start($this->preferredbehaviour, $variant, array(), $timenow);
  496. $this->observer->notify_attempt_modified($qa);
  497. }
  498. /**
  499. * Start the attempt at all questions that has been added to this usage.
  500. * @param question_variant_selection_strategy how to pick which variant of each question to use.
  501. * @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
  502. * @param int $userid optional, the user to attribute this action to. Defaults to the current user.
  503. */
  504. public function start_all_questions(question_variant_selection_strategy $variantstrategy = null,
  505. $timestamp = null, $userid = null) {
  506. if (is_null($variantstrategy)) {
  507. $variantstrategy = new question_variant_random_strategy();
  508. }
  509. foreach ($this->questionattempts as $qa) {
  510. $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy));
  511. $this->observer->notify_attempt_modified($qa);
  512. }
  513. }
  514. /**
  515. * Start the attempt at a question, starting from the point where the previous
  516. * question_attempt $oldqa had reached. This is used by the quiz 'Each attempt
  517. * builds on last' mode.
  518. * @param int $slot the number used to identify this question within this usage.
  519. * @param question_attempt $oldqa a previous attempt at this quetsion that
  520. * defines the starting point.
  521. */
  522. public function start_question_based_on($slot, question_attempt $oldqa) {
  523. $qa = $this->get_question_attempt($slot);
  524. $qa->start_based_on($oldqa);
  525. $this->observer->notify_attempt_modified($qa);
  526. }
  527. /**
  528. * Process all the question actions in the current request.
  529. *
  530. * If there is a parameter slots included in the post data, then only
  531. * those question numbers will be processed, otherwise all questions in this
  532. * useage will be.
  533. *
  534. * This function also does {@link update_question_flags()}.
  535. *
  536. * @param int $timestamp optional, use this timestamp as 'now'.
  537. * @param array $postdata optional, only intended for testing. Use this data
  538. * instead of the data from $_POST.
  539. */
  540. public function process_all_actions($timestamp = null, $postdata = null) {
  541. foreach ($this->get_slots_in_request($postdata) as $slot) {
  542. if (!$this->validate_sequence_number($slot, $postdata)) {
  543. continue;
  544. }
  545. $submitteddata = $this->extract_responses($slot, $postdata);
  546. $this->process_action($slot, $submitteddata, $timestamp);
  547. }
  548. $this->update_question_flags($postdata);
  549. }
  550. /**
  551. * Process all the question autosave data in the current request.
  552. *
  553. * If there is a parameter slots included in the post data, then only
  554. * those question numbers will be processed, otherwise all questions in this
  555. * useage will be.
  556. *
  557. * This function also does {@link update_question_flags()}.
  558. *
  559. * @param int $timestamp optional, use this timestamp as 'now'.
  560. * @param array $postdata optional, only intended for testing. Use this data
  561. * instead of the data from $_POST.
  562. */
  563. public function process_all_autosaves($timestamp = null, $postdata = null) {
  564. foreach ($this->get_slots_in_request($postdata) as $slot) {
  565. if (!$this->is_autosave_required($slot, $postdata)) {
  566. continue;
  567. }
  568. $submitteddata = $this->extract_responses($slot, $postdata);
  569. $this->process_autosave($slot, $submitteddata, $timestamp);
  570. }
  571. $this->update_question_flags($postdata);
  572. }
  573. /**
  574. * Get the list of slot numbers that should be processed as part of processing
  575. * the current request.
  576. * @param array $postdata optional, only intended for testing. Use this data
  577. * instead of the data from $_POST.
  578. * @return array of slot numbers.
  579. */
  580. protected function get_slots_in_request($postdata = null) {
  581. // Note: we must not use "question_attempt::get_submitted_var()" because there is no attempt instance!!!
  582. if (is_null($postdata)) {
  583. $slots = optional_param('slots', null, PARAM_SEQUENCE);
  584. } else if (array_key_exists('slots', $postdata)) {
  585. $slots = clean_param($postdata['slots'], PARAM_SEQUENCE);
  586. } else {
  587. $slots = null;
  588. }
  589. if (is_null($slots)) {
  590. $slots = $this->get_slots();
  591. } else if (!$slots) {
  592. $slots = array();
  593. } else {
  594. $slots = explode(',', $slots);
  595. }
  596. return $slots;
  597. }
  598. /**
  599. * Get the submitted data from the current request that belongs to this
  600. * particular question.
  601. *
  602. * @param int $slot the number used to identify this question within this usage.
  603. * @param $postdata optional, only intended for testing. Use this data
  604. * instead of the data from $_POST.
  605. * @return array submitted data specific to this question.
  606. */
  607. public function extract_responses($slot, $postdata = null) {
  608. return $this->get_question_attempt($slot)->get_submitted_data($postdata);
  609. }
  610. /**
  611. * Transform an array of response data for slots to an array of post data as you would get from quiz attempt form.
  612. *
  613. * @param $simulatedresponses array keys are slot nos => contains arrays representing student
  614. * responses which will be passed to question_definition::prepare_simulated_post_data method
  615. * and then have the appropriate prefix added.
  616. * @return array simulated post data
  617. */
  618. public function prepare_simulated_post_data($simulatedresponses) {
  619. $simulatedpostdata = array();
  620. $simulatedpostdata['slots'] = implode(',', array_keys($simulatedresponses));
  621. foreach ($simulatedresponses as $slot => $responsedata) {
  622. $slotresponse = array();
  623. // Behaviour vars should not be processed by question type, just add prefix.
  624. $behaviourvars = $this->get_question_attempt($slot)->get_behaviour()->get_expected_data();
  625. foreach (array_keys($responsedata) as $responsedatakey) {
  626. if ($responsedatakey{0} === '-') {
  627. $behaviourvarname = substr($responsedatakey, 1);
  628. if (isset($behaviourvars[$behaviourvarname])) {
  629. // Expected behaviour var found.
  630. if ($responsedata[$responsedatakey]) {
  631. // Only set the behaviour var if the column value from the cvs file is non zero.
  632. // The behaviours only look at whether the var is set or not they don't look at the value.
  633. $slotresponse[$responsedatakey] = $responsedata[$responsedatakey];
  634. }
  635. }
  636. // Remove both expected and unexpected vars from data passed to question type.
  637. unset($responsedata[$responsedatakey]);
  638. }
  639. }
  640. $slotresponse += $this->get_question($slot)->prepare_simulated_post_data($responsedata);
  641. $slotresponse[':sequencecheck'] = $this->get_question_attempt($slot)->get_sequence_check_count();
  642. // Add this slot's prefix to slot data.
  643. $prefix = $this->get_field_prefix($slot);
  644. foreach ($slotresponse as $key => $value) {
  645. $simulatedpostdata[$prefix.$key] = $value;
  646. }
  647. }
  648. return $simulatedpostdata;
  649. }
  650. /**
  651. * Process a specific action on a specific question.
  652. * @param int $slot the number used to identify this question within this usage.
  653. * @param $submitteddata the submitted data that constitutes the action.
  654. */
  655. public function process_action($slot, $submitteddata, $timestamp = null) {
  656. $qa = $this->get_question_attempt($slot);
  657. $qa->process_action($submitteddata, $timestamp);
  658. $this->observer->notify_attempt_modified($qa);
  659. }
  660. /**
  661. * Process an autosave action on a specific question.
  662. * @param int $slot the number used to identify this question within this usage.
  663. * @param $submitteddata the submitted data that constitutes the action.
  664. */
  665. public function process_autosave($slot, $submitteddata, $timestamp = null) {
  666. $qa = $this->get_question_attempt($slot);
  667. if ($qa->process_autosave($submitteddata, $timestamp)) {
  668. $this->observer->notify_attempt_modified($qa);
  669. }
  670. }
  671. /**
  672. * Check that the sequence number, that detects weird things like the student
  673. * clicking back, is OK. If the sequence check variable is not present, returns
  674. * false. If the check variable is present and correct, returns true. If the
  675. * variable is present and wrong, throws an exception.
  676. * @param int $slot the number used to identify this question within this usage.
  677. * @param array $submitteddata the submitted data that constitutes the action.
  678. * @return bool true if the check variable is present and correct. False if it
  679. * is missing. (Throws an exception if the check fails.)
  680. */
  681. public function validate_sequence_number($slot, $postdata = null) {
  682. $qa = $this->get_question_attempt($slot);
  683. $sequencecheck = $qa->get_submitted_var(
  684. $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata);
  685. if (is_null($sequencecheck)) {
  686. return false;
  687. } else if ($sequencecheck != $qa->get_sequence_check_count()) {
  688. throw new question_out_of_sequence_exception($this->id, $slot, $postdata);
  689. } else {
  690. return true;
  691. }
  692. }
  693. /**
  694. * Check, based on the sequence number, whether this auto-save is still required.
  695. * @param int $slot the number used to identify this question within this usage.
  696. * @param array $submitteddata the submitted data that constitutes the action.
  697. * @return bool true if the check variable is present and correct, otherwise false.
  698. */
  699. public function is_autosave_required($slot, $postdata = null) {
  700. $qa = $this->get_question_attempt($slot);
  701. $sequencecheck = $qa->get_submitted_var(
  702. $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata);
  703. if (is_null($sequencecheck)) {
  704. return false;
  705. } else if ($sequencecheck != $qa->get_sequence_check_count()) {
  706. return false;
  707. } else {
  708. return true;
  709. }
  710. }
  711. /**
  712. * Update the flagged state for all question_attempts in this usage, if their
  713. * flagged state was changed in the request.
  714. *
  715. * @param $postdata optional, only intended for testing. Use this data
  716. * instead of the data from $_POST.
  717. */
  718. public function update_question_flags($postdata = null) {
  719. foreach ($this->questionattempts as $qa) {
  720. $flagged = $qa->get_submitted_var(
  721. $qa->get_flag_field_name(), PARAM_BOOL, $postdata);
  722. if (!is_null($flagged) && $flagged != $qa->is_flagged()) {
  723. $qa->set_flagged($flagged);
  724. }
  725. }
  726. }
  727. /**
  728. * Get the correct response to a particular question. Passing the results of
  729. * this method to {@link process_action()} will probably result in full marks.
  730. * If it is not possible to compute a correct response, this method should return null.
  731. * @param int $slot the number used to identify this question within this usage.
  732. * @return array that constitutes a correct response to this question.
  733. */
  734. public function get_correct_response($slot) {
  735. return $this->get_question_attempt($slot)->get_correct_response();
  736. }
  737. /**
  738. * Finish the active phase of an attempt at a question.
  739. *
  740. * This is an external act of finishing the attempt. Think, for example, of
  741. * the 'Submit all and finish' button in the quiz. Some behaviours,
  742. * (for example, immediatefeedback) give a way of finishing the active phase
  743. * of a question attempt as part of a {@link process_action()} call.
  744. *
  745. * After the active phase is over, the only changes possible are things like
  746. * manual grading, or changing the flag state.
  747. *
  748. * @param int $slot the number used to identify this question within this usage.
  749. */
  750. public function finish_question($slot, $timestamp = null) {
  751. $qa = $this->get_question_attempt($slot);
  752. $qa->finish($timestamp);
  753. $this->observer->notify_attempt_modified($qa);
  754. }
  755. /**
  756. * Finish the active phase of an attempt at a question. See {@link finish_question()}
  757. * for a fuller description of what 'finish' means.
  758. */
  759. public function finish_all_questions($timestamp = null) {
  760. foreach ($this->questionattempts as $qa) {
  761. $qa->finish($timestamp);
  762. $this->observer->notify_attempt_modified($qa);
  763. }
  764. }
  765. /**
  766. * Perform a manual grading action on a question attempt.
  767. * @param int $slot the number used to identify this question within this usage.
  768. * @param string $comment the comment being added to the question attempt.
  769. * @param number $mark the mark that is being assigned. Can be null to just
  770. * add a comment.
  771. * @param int $commentformat one of the FORMAT_... constants. The format of $comment.
  772. */
  773. public function manual_grade($slot, $comment, $mark, $commentformat = null) {
  774. $qa = $this->get_question_attempt($slot);
  775. $qa->manual_grade($comment, $mark, $commentformat);
  776. $this->observer->notify_attempt_modified($qa);
  777. }
  778. /**
  779. * Regrade a question in this usage. This replays the sequence of submitted
  780. * actions to recompute the outcomes.
  781. * @param int $slot the number used to identify this question within this usage.
  782. * @param bool $finished whether the question attempt should be forced to be finished
  783. * after the regrade, or whether it may still be in progress (default false).
  784. * @param number $newmaxmark (optional) if given, will change the max mark while regrading.
  785. */
  786. public function regrade_question($slot, $finished = false, $newmaxmark = null) {
  787. $oldqa = $this->get_question_attempt($slot);
  788. if (is_null($newmaxmark)) {
  789. $newmaxmark = $oldqa->get_max_mark();
  790. }
  791. $newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(),
  792. $this->observer, $newmaxmark);
  793. $newqa->set_database_id($oldqa->get_database_id());
  794. $newqa->set_slot($oldqa->get_slot());
  795. $newqa->regrade($oldqa, $finished);
  796. $this->questionattempts[$slot] = $newqa;
  797. $this->observer->notify_attempt_modified($newqa);
  798. }
  799. /**
  800. * Regrade all the questions in this usage (without changing their max mark).
  801. * @param bool $finished whether each question should be forced to be finished
  802. * after the regrade, or whether it may still be in progress (default false).
  803. */
  804. public function regrade_all_questions($finished = false) {
  805. foreach ($this->questionattempts as $slot => $notused) {
  806. $this->regrade_question($slot, $finished);
  807. }
  808. }
  809. /**
  810. * Change the max mark for this question_attempt.
  811. * @param int $slot the slot number of the question of inerest.
  812. * @param float $maxmark the new max mark.
  813. */
  814. public function set_max_mark($slot, $maxmark) {
  815. $this->get_question_attempt($slot)->set_max_mark($maxmark);
  816. }
  817. /**
  818. * Create a question_usage_by_activity from records loaded from the database.
  819. *
  820. * For internal use only.
  821. *
  822. * @param Iterator $records Raw records loaded from the database.
  823. * @param int $questionattemptid The id of the question_attempt to extract.
  824. * @return question_usage_by_activity The newly constructed usage.
  825. */
  826. public static function load_from_records($records, $qubaid) {
  827. $record = $records->current();
  828. while ($record->qubaid != $qubaid) {
  829. $records->next();
  830. if (!$records->valid()) {
  831. throw new coding_exception("Question usage {$qubaid} not found in the database.");
  832. }
  833. $record = $records->current();
  834. }
  835. $quba = new question_usage_by_activity($record->component,
  836. context::instance_by_id($record->contextid, IGNORE_MISSING));
  837. $quba->set_id_from_database($record->qubaid);
  838. $quba->set_preferred_behaviour($record->preferredbehaviour);
  839. $quba->observer = new question_engine_unit_of_work($quba);
  840. // If slot is null then the current pointer in $records will not be
  841. // advanced in the while loop below, and we get stuck in an infinite loop,
  842. // since this method is supposed to always consume at least one record.
  843. // Therefore, in this case, advance the record here.
  844. if (is_null($record->slot)) {
  845. $records->next();
  846. }
  847. while ($record && $record->qubaid == $qubaid && !is_null($record->slot)) {
  848. $quba->questionattempts[$record->slot] =
  849. question_attempt::load_from_records($records,
  850. $record->questionattemptid, $quba->observer,
  851. $quba->get_preferred_behaviour());
  852. if ($records->valid()) {
  853. $record = $records->current();
  854. } else {
  855. $record = false;
  856. }
  857. }
  858. return $quba;
  859. }
  860. }
  861. /**
  862. * A class abstracting access to the
  863. * {@link question_usage_by_activity::$questionattempts} array.
  864. *
  865. * This class snapshots the list of {@link question_attempts} to iterate over
  866. * when it is created. If a question is added to the usage mid-iteration, it
  867. * will now show up.
  868. *
  869. * To create an instance of this class, use
  870. * {@link question_usage_by_activity::get_attempt_iterator()}
  871. *
  872. * @copyright 2009 The Open University
  873. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  874. */
  875. class question_attempt_iterator implements Iterator, ArrayAccess {
  876. /** @var question_usage_by_activity that we are iterating over. */
  877. protected $quba;
  878. /** @var array of question numbers. */
  879. protected $slots;
  880. /**
  881. * To create an instance of this class, use
  882. * {@link question_usage_by_activity::get_attempt_iterator()}.
  883. * @param $quba the usage to iterate over.
  884. */
  885. public function __construct(question_usage_by_activity $quba) {
  886. $this->quba = $quba;
  887. $this->slots = $quba->get_slots();
  888. $this->rewind();
  889. }
  890. /** @return question_attempt_step */
  891. public function current() {
  892. return $this->offsetGet(current($this->slots));
  893. }
  894. /** @return int */
  895. public function key() {
  896. return current($this->slots);
  897. }
  898. public function next() {
  899. next($this->slots);
  900. }
  901. public function rewind() {
  902. reset($this->slots);
  903. }
  904. /** @return bool */
  905. public function valid() {
  906. return current($this->slots) !== false;
  907. }
  908. /** @return bool */
  909. public function offsetExists($slot) {
  910. return in_array($slot, $this->slots);
  911. }
  912. /** @return question_attempt_step */
  913. public function offsetGet($slot) {
  914. return $this->quba->get_question_attempt($slot);
  915. }
  916. public function offsetSet($slot, $value) {
  917. throw new coding_exception('You are only allowed read-only access to ' .
  918. 'question_attempt::states through a question_attempt_step_iterator. Cannot set.');
  919. }
  920. public function offsetUnset($slot) {
  921. throw new coding_exception('You are only allowed read-only access to ' .
  922. 'question_attempt::states through a question_attempt_step_iterator. Cannot unset.');
  923. }
  924. }
  925. /**
  926. * Interface for things that want to be notified of signficant changes to a
  927. * {@link question_usage_by_activity}.
  928. *
  929. * A question behaviour controls the flow of actions a student can
  930. * take as they work through a question, and later, as a teacher manually grades it.
  931. *
  932. * @copyright 2009 The Open University
  933. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  934. */
  935. interface question_usage_observer {
  936. /** Called when a field of the question_usage_by_activity is changed. */
  937. public function notify_modified();
  938. /**
  939. * Called when a new question attempt is added to this usage.
  940. * @param question_attempt $qa the newly added question attempt.
  941. */
  942. public function notify_attempt_added(question_attempt $qa);
  943. /**
  944. * Called when the fields of a question attempt in this usage are modified.
  945. * @param question_attempt $qa the newly added question attempt.
  946. */
  947. public function notify_attempt_modified(question_attempt $qa);
  948. /**
  949. * Called when a question_attempt has been moved to a new slot.
  950. * @param question_attempt $qa The question attempt that was moved.
  951. * @param int $oldslot The previous slot number of that attempt.
  952. */
  953. public function notify_attempt_moved(question_attempt $qa, $oldslot);
  954. /**
  955. * Called when a new step is added to a question attempt in this usage.
  956. * @param question_attempt_step $step the new step.
  957. * @param question_attempt $qa the usage it is being added to.
  958. * @param int $seq the sequence number of the new step.
  959. */
  960. public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq);
  961. /**
  962. * Called when a new step is updated in a question attempt in this usage.
  963. * @param question_attempt_step $step the step that was updated.
  964. * @param question_attempt $qa the usage it is being added to.
  965. * @param int $seq the sequence number of the new step.
  966. */
  967. public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq);
  968. /**
  969. * Called when a new step is updated in a question attempt in this usage.
  970. * @param question_attempt_step $step the step to delete.
  971. * @param question_attempt $qa the usage it is being added to.
  972. */
  973. public function notify_step_deleted(question_attempt_step $step, question_attempt $qa);
  974. /**
  975. * Called when a new metadata variable is set on a question attempt in this usage.
  976. * @param question_attempt $qa the question attempt the metadata is being added to.
  977. * @param int $name the name of the metadata variable added.
  978. */
  979. public function notify_metadata_added(question_attempt $qa, $name);
  980. /**
  981. * Called when a metadata variable on a question attempt in this usage is updated.
  982. * @param question_attempt $qa the question attempt where the metadata is being modified.
  983. * @param int $name the name of the metadata variable modified.
  984. */
  985. public function notify_metadata_modified(question_attempt $qa, $name);
  986. }
  987. /**
  988. * Null implmentation of the {@link question_usage_watcher} interface.
  989. * Does nothing.
  990. *
  991. * @copyright 2009 The Open University
  992. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  993. */
  994. class question_usage_null_observer implements question_usage_observer {
  995. public function notify_modified() {
  996. }
  997. public function notify_attempt_added(question_attempt $qa) {
  998. }
  999. public function notify_attempt_modified(question_attempt $qa) {
  1000. }
  1001. public function notify_attempt_moved(question_attempt $qa, $oldslot) {
  1002. }
  1003. public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
  1004. }
  1005. public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
  1006. }
  1007. public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
  1008. }
  1009. public function notify_metadata_added(question_attempt $qa, $name) {
  1010. }
  1011. public function notify_metadata_modified(question_attempt $qa, $name) {
  1012. }
  1013. }