weekly release 3.1dev
[moodle.git] / question / engine / datalib.php
CommitLineData
d1b7e03d 1<?php
d1b7e03d
TH
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
d1b7e03d 17/**
5e8a85aa 18 * Code for loading and saving question attempts to and from the database.
d1b7e03d 19 *
16e246ac
TH
20 * Note that many of the methods of this class should be considered private to
21 * the question engine. They should be accessed through the
22 * {@link question_engine} class. For example, you should call
23 * {@link question_engine::save_questions_usage_by_activity()} rather than
24 * {@link question_engine_data_mapper::insert_questions_usage_by_activity()}.
25 * The exception to this is some of the reporting methods, like
26 * {@link question_engine_data_mapper::load_attempts_at_question()}.
27 *
28 * (TODO, probably we should split this class up, so that it has no public
29 * methods. They should all be moved to a new public class.)
30 *
4040e2dd 31 * A note for future reference. This code is pretty efficient but there are some
94815ccf
TH
32 * potential optimisations that could be contemplated, at the cost of making the
33 * code more complex:
34 *
4040e2dd
TH
35 * 1. (This is probably not worth doing.) In the unit-of-work save method, we
36 * could get all the ids for steps due to be deleted or modified,
94815ccf
TH
37 * and delete all the question_attempt_step_data for all of those steps in one
38 * query. That would save one DB query for each ->stepsupdated. However that number
39 * is 0 except when re-grading, and when regrading, there are many more inserts
40 * into question_attempt_step_data than deletes, so it is really hardly worth it.
41 *
4040e2dd 42 * @package core_question
017bc1d9
TH
43 * @copyright 2009 The Open University
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
45 */
46
47
a17b297d
TH
48defined('MOODLE_INTERNAL') || die();
49
50
d1b7e03d
TH
51/**
52 * This class controls the loading and saving of question engine data to and from
53 * the database.
54 *
017bc1d9
TH
55 * @copyright 2009 The Open University
56 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
57 */
58class question_engine_data_mapper {
06f8ed54
TH
59 /**
60 * @var moodle_database normally points to global $DB, but I prefer not to
61 * use globals if I can help it.
62 */
63 protected $db;
64
65 /**
66 * @param moodle_database $db a database connectoin. Defaults to global $DB.
67 */
0a606a2b 68 public function __construct(moodle_database $db = null) {
06f8ed54
TH
69 if (is_null($db)) {
70 global $DB;
06f8ed54
TH
71 $this->db = $DB;
72 } else {
73 $this->db = $db;
74 }
75 }
76
d1b7e03d
TH
77 /**
78 * Store an entire {@link question_usage_by_activity} in the database,
79 * including all the question_attempts that comprise it.
16e246ac
TH
80 *
81 * You should not call this method directly. You should use
82 * @link question_engine::save_questions_usage_by_activity()}.
83 *
d1b7e03d
TH
84 * @param question_usage_by_activity $quba the usage to store.
85 */
86 public function insert_questions_usage_by_activity(question_usage_by_activity $quba) {
0ff4bd08 87 $record = new stdClass();
d1b7e03d 88 $record->contextid = $quba->get_owning_context()->id;
56b0df7e
TH
89 $record->component = $quba->get_owning_component();
90 $record->preferredbehaviour = $quba->get_preferred_behaviour();
d1b7e03d 91
06f8ed54 92 $newid = $this->db->insert_record('question_usages', $record);
d1b7e03d
TH
93 $quba->set_id_from_database($newid);
94
4040e2dd
TH
95 // Initially an array of array of question_attempt_step_objects.
96 // Built as a nested array for efficiency, then flattened.
97 $stepdata = array();
98
d1b7e03d 99 foreach ($quba->get_attempt_iterator() as $qa) {
4040e2dd
TH
100 $stepdata[] = $this->insert_question_attempt($qa, $quba->get_owning_context());
101 }
102
103 $stepdata = call_user_func_array('array_merge', $stepdata);
104 if ($stepdata) {
105 $this->insert_all_step_data($stepdata);
d1b7e03d
TH
106 }
107 }
108
109 /**
110 * Store an entire {@link question_attempt} in the database,
111 * including all the question_attempt_steps that comprise it.
16e246ac
TH
112 *
113 * You should not call this method directly. You should use
114 * @link question_engine::save_questions_usage_by_activity()}.
115 *
d1b7e03d 116 * @param question_attempt $qa the question attempt to store.
94815ccf 117 * @param context $context the context of the owning question_usage_by_activity.
4040e2dd 118 * @return array of question_attempt_step_data rows, that still need to be inserted.
d1b7e03d 119 */
217f9a61 120 public function insert_question_attempt(question_attempt $qa, $context) {
0ff4bd08 121 $record = new stdClass();
d1b7e03d
TH
122 $record->questionusageid = $qa->get_usage_id();
123 $record->slot = $qa->get_slot();
56b0df7e 124 $record->behaviour = $qa->get_behaviour_name();
d1b7e03d 125 $record->questionid = $qa->get_question()->id;
1da821bb 126 $record->variant = $qa->get_variant();
d1b7e03d
TH
127 $record->maxmark = $qa->get_max_mark();
128 $record->minfraction = $qa->get_min_fraction();
4e3d8293 129 $record->maxfraction = $qa->get_max_fraction();
d1b7e03d 130 $record->flagged = $qa->is_flagged();
56b0df7e 131 $record->questionsummary = $qa->get_question_summary();
2f1e464a 132 if (core_text::strlen($record->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
c83ed025 133 // It seems some people write very long quesions! MDL-30760
2f1e464a 134 $record->questionsummary = core_text::substr($record->questionsummary,
c83ed025
TH
135 0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
136 }
56b0df7e
TH
137 $record->rightanswer = $qa->get_right_answer_summary();
138 $record->responsesummary = $qa->get_response_summary();
d1b7e03d 139 $record->timemodified = time();
06f8ed54 140 $record->id = $this->db->insert_record('question_attempts', $record);
1c80e79a 141 $qa->set_database_id($record->id);
d1b7e03d 142
4040e2dd
TH
143 // Initially an array of array of question_attempt_step_objects.
144 // Built as a nested array for efficiency, then flattened.
145 $stepdata = array();
146
d1b7e03d 147 foreach ($qa->get_step_iterator() as $seq => $step) {
4040e2dd 148 $stepdata[] = $this->insert_question_attempt_step($step, $record->id, $seq, $context);
d1b7e03d 149 }
4040e2dd
TH
150
151 return call_user_func_array('array_merge', $stepdata);
d1b7e03d
TH
152 }
153
154 /**
94815ccf
TH
155 * Helper method used by insert_question_attempt_step and update_question_attempt_step
156 * @param question_attempt_step $step the step to store.
217f9a61
TH
157 * @param int $questionattemptid the question attept id this step belongs to.
158 * @param int $seq the sequence number of this stop.
94815ccf 159 * @return stdClass data to insert into the database.
d1b7e03d 160 */
94815ccf 161 protected function make_step_record(question_attempt_step $step, $questionattemptid, $seq) {
0ff4bd08 162 $record = new stdClass();
d1b7e03d
TH
163 $record->questionattemptid = $questionattemptid;
164 $record->sequencenumber = $seq;
deef04a4 165 $record->state = (string) $step->get_state();
d1b7e03d
TH
166 $record->fraction = $step->get_fraction();
167 $record->timecreated = $step->get_timecreated();
168 $record->userid = $step->get_user_id();
94815ccf
TH
169 return $record;
170 }
d1b7e03d 171
94815ccf
TH
172 /**
173 * Helper method used by insert_question_attempt_step and update_question_attempt_step
174 * @param question_attempt_step $step the step to store.
175 * @param int $stepid the id of the step.
176 * @param context $context the context of the owning question_usage_by_activity.
4040e2dd 177 * @return array of question_attempt_step_data rows, that still need to be inserted.
94815ccf 178 */
4040e2dd
TH
179 protected function prepare_step_data(question_attempt_step $step, $stepid, $context) {
180 $rows = array();
d1b7e03d 181 foreach ($step->get_all_data() as $name => $value) {
48d9c17d 182 if ($value instanceof question_file_saver) {
94815ccf 183 $value->save_files($stepid, $context);
ea07b2d6
TH
184 }
185 if ($value instanceof question_response_files) {
7a26403f 186 $value = (string) $value;
48d9c17d
TH
187 }
188
0ff4bd08 189 $data = new stdClass();
94815ccf 190 $data->attemptstepid = $stepid;
56b0df7e
TH
191 $data->name = $name;
192 $data->value = $value;
4040e2dd
TH
193 $rows[] = $data;
194 }
195 return $rows;
196 }
197
198 /**
199 * Insert a lot of records into question_attempt_step_data in one go.
16e246ac
TH
200 *
201 * Private method, only for use by other parts of the question engine.
202 *
4040e2dd
TH
203 * @param array $rows the rows to insert.
204 */
205 public function insert_all_step_data(array $rows) {
206 if (!$rows) {
207 return;
d1b7e03d 208 }
4040e2dd 209 $this->db->insert_records('question_attempt_step_data', $rows);
d1b7e03d
TH
210 }
211
94815ccf
TH
212 /**
213 * Store a {@link question_attempt_step} in the database.
16e246ac
TH
214 *
215 * Private method, only for use by other parts of the question engine.
216 *
94815ccf
TH
217 * @param question_attempt_step $step the step to store.
218 * @param int $questionattemptid the question attept id this step belongs to.
219 * @param int $seq the sequence number of this stop.
220 * @param context $context the context of the owning question_usage_by_activity.
4040e2dd 221 * @return array of question_attempt_step_data rows, that still need to be inserted.
94815ccf
TH
222 */
223 public function insert_question_attempt_step(question_attempt_step $step,
224 $questionattemptid, $seq, $context) {
225
226 $record = $this->make_step_record($step, $questionattemptid, $seq);
227 $record->id = $this->db->insert_record('question_attempt_steps', $record);
228
4040e2dd 229 return $this->prepare_step_data($step, $record->id, $context);
94815ccf
TH
230 }
231
232 /**
233 * Update a {@link question_attempt_step} in the database.
16e246ac
TH
234 *
235 * Private method, only for use by other parts of the question engine.
236 *
94815ccf
TH
237 * @param question_attempt_step $qa the step to store.
238 * @param int $questionattemptid the question attept id this step belongs to.
239 * @param int $seq the sequence number of this stop.
240 * @param context $context the context of the owning question_usage_by_activity.
4040e2dd 241 * @return array of question_attempt_step_data rows, that still need to be inserted.
94815ccf
TH
242 */
243 public function update_question_attempt_step(question_attempt_step $step,
244 $questionattemptid, $seq, $context) {
245
246 $record = $this->make_step_record($step, $questionattemptid, $seq);
247 $record->id = $step->get_id();
248 $this->db->update_record('question_attempt_steps', $record);
249
250 $this->db->delete_records('question_attempt_step_data',
251 array('attemptstepid' => $record->id));
4040e2dd 252 return $this->prepare_step_data($step, $record->id, $context);
94815ccf
TH
253 }
254
f6579bea
TH
255 /**
256 * Store new metadata for an existing {@link question_attempt} in the database.
257 *
258 * Private method, only for use by other parts of the question engine.
259 *
260 * @param question_attempt $qa the question attempt to store meta data for.
261 * @param array $names the names of the metadata variables to store.
262 * @return array of question_attempt_step_data rows, that still need to be inserted.
263 */
264 public function insert_question_attempt_metadata(question_attempt $qa, array $names) {
265 $firststep = $qa->get_step(0);
266
267 $rows = array();
268 foreach ($names as $name) {
269 $data = new stdClass();
270 $data->attemptstepid = $firststep->get_id();
271 $data->name = ':_' . $name;
272 $data->value = $firststep->get_metadata_var($name);
273 $rows[] = $data;
274 }
275
276 return $rows;
277 }
278
279 /**
280 * Updates existing metadata for an existing {@link question_attempt} in the database.
281 *
282 * Private method, only for use by other parts of the question engine.
283 *
284 * @param question_attempt $qa the question attempt to store meta data for.
285 * @param array $names the names of the metadata variables to store.
286 * @return array of question_attempt_step_data rows, that still need to be inserted.
287 */
288 public function update_question_attempt_metadata(question_attempt $qa, array $names) {
289 global $DB;
290 list($condition, $params) = $DB->get_in_or_equal($names);
291 $params[] = $qa->get_step(0)->get_id();
292 $DB->delete_records_select('question_attempt_step_data',
293 'name ' . $condition . ' AND attemptstepid = ?', $params);
294 return $this->insert_question_attempt_metadata($qa, $names);
295 }
296
d1b7e03d
TH
297 /**
298 * Load a {@link question_attempt_step} from the database.
16e246ac
TH
299 *
300 * Private method, only for use by other parts of the question engine.
301 *
f7970e3c 302 * @param int $stepid the id of the step to load.
d1b7e03d
TH
303 * @param question_attempt_step the step that was loaded.
304 */
305 public function load_question_attempt_step($stepid) {
35d5f1c2 306 $records = $this->db->get_recordset_sql("
d1b7e03d 307SELECT
dc1ee5cb 308 quba.contextid,
e5acdcb9 309 COALESCE(q.qtype, 'missingtype') AS qtype,
d1b7e03d
TH
310 qas.id AS attemptstepid,
311 qas.questionattemptid,
312 qas.sequencenumber,
313 qas.state,
314 qas.fraction,
315 qas.timecreated,
316 qas.userid,
317 qasd.name,
318 qasd.value
319
dc1ee5cb
TH
320FROM {question_attempt_steps} qas
321JOIN {question_attempts} qa ON qa.id = qas.questionattemptid
322JOIN {question_usages} quba ON quba.id = qa.questionusageid
323LEFT JOIN {question} q ON q.id = qa.questionid
06f8ed54 324LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
325
326WHERE
06f8ed54
TH
327 qas.id = :stepid
328 ", array('stepid' => $stepid));
d1b7e03d 329
35d5f1c2 330 if (!$records->valid()) {
88f0eb15 331 throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
d1b7e03d
TH
332 }
333
35d5f1c2
TH
334 $step = question_attempt_step::load_from_records($records, $stepid);
335 $records->close();
336
337 return $step;
d1b7e03d
TH
338 }
339
340 /**
341 * Load a {@link question_attempt} from the database, including all its
342 * steps.
16e246ac
TH
343 *
344 * Normally, you should use {@link question_engine::load_questions_usage_by_activity()}
345 * but there may be rare occasions where for performance reasons, you only
346 * wish to load one qa, in which case you may call this method.
347 *
f7970e3c 348 * @param int $questionattemptid the id of the question attempt to load.
d1b7e03d
TH
349 * @param question_attempt the question attempt that was loaded.
350 */
351 public function load_question_attempt($questionattemptid) {
35d5f1c2 352 $records = $this->db->get_recordset_sql("
d1b7e03d 353SELECT
7a719748 354 quba.contextid,
d1b7e03d
TH
355 quba.preferredbehaviour,
356 qa.id AS questionattemptid,
357 qa.questionusageid,
358 qa.slot,
359 qa.behaviour,
360 qa.questionid,
1da821bb 361 qa.variant,
d1b7e03d
TH
362 qa.maxmark,
363 qa.minfraction,
4e3d8293 364 qa.maxfraction,
d1b7e03d
TH
365 qa.flagged,
366 qa.questionsummary,
367 qa.rightanswer,
368 qa.responsesummary,
369 qa.timemodified,
370 qas.id AS attemptstepid,
371 qas.sequencenumber,
372 qas.state,
373 qas.fraction,
374 qas.timecreated,
375 qas.userid,
376 qasd.name,
377 qasd.value
378
d50e8a1b 379FROM {question_attempts} qa
06f8ed54
TH
380JOIN {question_usages} quba ON quba.id = qa.questionusageid
381LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
382LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
383
384WHERE
06f8ed54 385 qa.id = :questionattemptid
d1b7e03d
TH
386
387ORDER BY
388 qas.sequencenumber
06f8ed54 389 ", array('questionattemptid' => $questionattemptid));
d1b7e03d 390
35d5f1c2 391 if (!$records->valid()) {
88f0eb15 392 throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
d1b7e03d
TH
393 }
394
d50e8a1b 395 $record = $records->current();
35d5f1c2 396 $qa = question_attempt::load_from_records($records, $questionattemptid,
d1b7e03d 397 new question_usage_null_observer(), $record->preferredbehaviour);
35d5f1c2
TH
398 $records->close();
399
400 return $qa;
d1b7e03d
TH
401 }
402
403 /**
404 * Load a {@link question_usage_by_activity} from the database, including
405 * all its {@link question_attempt}s and all their steps.
16e246ac
TH
406 *
407 * You should call {@link question_engine::load_questions_usage_by_activity()}
408 * rather than calling this method directly.
409 *
f7970e3c 410 * @param int $qubaid the id of the usage to load.
d1b7e03d
TH
411 * @param question_usage_by_activity the usage that was loaded.
412 */
413 public function load_questions_usage_by_activity($qubaid) {
35d5f1c2 414 $records = $this->db->get_recordset_sql("
d1b7e03d 415SELECT
d1b7e03d
TH
416 quba.id AS qubaid,
417 quba.contextid,
418 quba.component,
419 quba.preferredbehaviour,
420 qa.id AS questionattemptid,
421 qa.questionusageid,
422 qa.slot,
423 qa.behaviour,
424 qa.questionid,
1da821bb 425 qa.variant,
d1b7e03d
TH
426 qa.maxmark,
427 qa.minfraction,
4e3d8293 428 qa.maxfraction,
d1b7e03d
TH
429 qa.flagged,
430 qa.questionsummary,
431 qa.rightanswer,
432 qa.responsesummary,
433 qa.timemodified,
434 qas.id AS attemptstepid,
435 qas.sequencenumber,
436 qas.state,
437 qas.fraction,
438 qas.timecreated,
439 qas.userid,
440 qasd.name,
441 qasd.value
442
06f8ed54
TH
443FROM {question_usages} quba
444LEFT JOIN {question_attempts} qa ON qa.questionusageid = quba.id
445LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
446LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
447
448WHERE
06f8ed54 449 quba.id = :qubaid
d1b7e03d
TH
450
451ORDER BY
452 qa.slot,
453 qas.sequencenumber
c76145d3 454 ", array('qubaid' => $qubaid));
d1b7e03d 455
35d5f1c2 456 if (!$records->valid()) {
88f0eb15 457 throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
d1b7e03d
TH
458 }
459
35d5f1c2
TH
460 $quba = question_usage_by_activity::load_from_records($records, $qubaid);
461 $records->close();
462
463 return $quba;
d1b7e03d
TH
464 }
465
99caa248
JP
466 /**
467 * Load all {@link question_usage_by_activity} from the database for one qubaid_condition
468 * Include all its {@link question_attempt}s and all their steps.
16e246ac
TH
469 *
470 * This method may be called publicly.
471 *
99caa248
JP
472 * @param qubaid_condition $qubaids the condition that tells us which usages to load.
473 * @return question_usage_by_activity[] the usages that were loaded.
474 */
475 public function load_questions_usages_by_activity($qubaids) {
476 $records = $this->db->get_recordset_sql("
477SELECT
478 quba.id AS qubaid,
479 quba.contextid,
480 quba.component,
481 quba.preferredbehaviour,
482 qa.id AS questionattemptid,
483 qa.questionusageid,
484 qa.slot,
485 qa.behaviour,
486 qa.questionid,
487 qa.variant,
488 qa.maxmark,
489 qa.minfraction,
490 qa.maxfraction,
491 qa.flagged,
492 qa.questionsummary,
493 qa.rightanswer,
494 qa.responsesummary,
495 qa.timemodified,
496 qas.id AS attemptstepid,
497 qas.sequencenumber,
498 qas.state,
499 qas.fraction,
500 qas.timecreated,
501 qas.userid,
502 qasd.name,
503 qasd.value
504
505FROM {question_usages} quba
506LEFT JOIN {question_attempts} qa ON qa.questionusageid = quba.id
507LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
508LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
509
510WHERE
511 quba.id {$qubaids->usage_id_in()}
512
513ORDER BY
514 quba.id,
515 qa.slot,
516 qas.sequencenumber
517 ", $qubaids->usage_id_in_params());
518
519 if (!$records->valid()) {
520 throw new coding_exception('Failed to load questions_usages_by_activity for qubaid_condition :' . $qubaids);
521 }
522
523 $qubas = array();
524 do {
525 $record = $records->current();
526 $qubas[$record->qubaid] = question_usage_by_activity::load_from_records($records, $record->qubaid);
527 } while ($records->valid());
528
529 $records->close();
530
531 return $qubas;
532 }
533
d1b7e03d
TH
534 /**
535 * Load information about the latest state of each question from the database.
536 *
16e246ac
TH
537 * This method may be called publicly.
538 *
d1b7e03d 539 * @param qubaid_condition $qubaids used to restrict which usages are included
e68e4ccf 540 * in the query. See {@link qubaid_condition}.
5b0a31bf 541 * @param array $slots A list of slots for the questions you want to know about.
e68e4ccf 542 * @param string|null $fields
d1b7e03d
TH
543 * @return array of records. See the SQL in this function to see the fields available.
544 */
e68e4ccf 545 public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots, $fields = null) {
a2ac2349 546 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
d1b7e03d 547
e68e4ccf 548 if ($fields === null) {
99caa248 549 $fields = "qas.id,
d1b7e03d
TH
550 qa.id AS questionattemptid,
551 qa.questionusageid,
552 qa.slot,
553 qa.behaviour,
554 qa.questionid,
1da821bb 555 qa.variant,
d1b7e03d
TH
556 qa.maxmark,
557 qa.minfraction,
4e3d8293 558 qa.maxfraction,
d1b7e03d
TH
559 qa.flagged,
560 qa.questionsummary,
561 qa.rightanswer,
562 qa.responsesummary,
563 qa.timemodified,
564 qas.id AS attemptstepid,
565 qas.sequencenumber,
566 qas.state,
567 qas.fraction,
568 qas.timecreated,
e68e4ccf
JP
569 qas.userid";
570
571 }
572
573 $records = $this->db->get_records_sql("
574SELECT
575 {$fields}
d1b7e03d
TH
576
577FROM {$qubaids->from_question_attempts('qa')}
84b37108
TH
578JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
579 AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
d1b7e03d
TH
580
581WHERE
582 {$qubaids->where()} AND
583 qa.slot $slottest
06f8ed54 584 ", $params + $qubaids->from_where_params());
d1b7e03d
TH
585
586 return $records;
587 }
588
589 /**
5e8a85aa
TH
590 * Load summary information about the state of each question in a group of
591 * attempts. This is used, for example, by the quiz manual grading report,
592 * to show how many attempts at each question need to be graded.
d1b7e03d 593 *
16e246ac
TH
594 * This method may be called publicly.
595 *
d1b7e03d
TH
596 * @param qubaid_condition $qubaids used to restrict which usages are included
597 * in the query. See {@link qubaid_condition}.
598 * @param array $slots A list of slots for the questions you want to konw about.
599 * @return array The array keys are slot,qestionid. The values are objects with
600 * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
601 * $manuallygraded and $all.
602 */
9c197f44
TH
603 public function load_questions_usages_question_state_summary(
604 qubaid_condition $qubaids, $slots) {
a2ac2349 605 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
d1b7e03d 606
06f8ed54 607 $rs = $this->db->get_recordset_sql("
d1b7e03d
TH
608SELECT
609 qa.slot,
610 qa.questionid,
611 q.name,
612 CASE qas.state
613 {$this->full_states_to_summary_state_sql()}
614 END AS summarystate,
615 COUNT(1) AS numattempts
616
617FROM {$qubaids->from_question_attempts('qa')}
84b37108
TH
618JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
619 AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
06f8ed54 620JOIN {question} q ON q.id = qa.questionid
d1b7e03d
TH
621
622WHERE
623 {$qubaids->where()} AND
624 qa.slot $slottest
625
626GROUP BY
627 qa.slot,
628 qa.questionid,
629 q.name,
630 q.id,
304f0d85
TH
631 CASE qas.state
632 {$this->full_states_to_summary_state_sql()}
633 END
d1b7e03d 634
f9b0500f 635ORDER BY
d1b7e03d
TH
636 qa.slot,
637 qa.questionid,
638 q.name,
639 q.id
06f8ed54 640 ", $params + $qubaids->from_where_params());
d1b7e03d 641
d1b7e03d 642 $results = array();
cf3b6568 643 foreach ($rs as $row) {
d1b7e03d
TH
644 $index = $row->slot . ',' . $row->questionid;
645
646 if (!array_key_exists($index, $results)) {
0ff4bd08 647 $res = new stdClass();
d1b7e03d
TH
648 $res->slot = $row->slot;
649 $res->questionid = $row->questionid;
650 $res->name = $row->name;
651 $res->inprogress = 0;
652 $res->needsgrading = 0;
653 $res->autograded = 0;
654 $res->manuallygraded = 0;
655 $res->all = 0;
656 $results[$index] = $res;
657 }
658
659 $results[$index]->{$row->summarystate} = $row->numattempts;
660 $results[$index]->all += $row->numattempts;
661 }
cf3b6568 662 $rs->close();
d1b7e03d
TH
663
664 return $results;
665 }
666
667 /**
668 * Get a list of usage ids where the question with slot $slot, and optionally
669 * also with question id $questionid, is in summary state $summarystate. Also
670 * return the total count of such states.
671 *
672 * Only a subset of the ids can be returned by using $orderby, $limitfrom and
673 * $limitnum. A special value 'random' can be passed as $orderby, in which case
674 * $limitfrom is ignored.
675 *
16e246ac
TH
676 * This method may be called publicly.
677 *
d1b7e03d
TH
678 * @param qubaid_condition $qubaids used to restrict which usages are included
679 * in the query. See {@link qubaid_condition}.
f7970e3c
TH
680 * @param int $slot The slot for the questions you want to konw about.
681 * @param int $questionid (optional) Only return attempts that were of this specific question.
d1b7e03d
TH
682 * @param string $summarystate the summary state of interest, or 'all'.
683 * @param string $orderby the column to order by.
cf3b6568 684 * @param array $params any params required by any of the SQL fragments.
f7970e3c 685 * @param int $limitfrom implements paging of the results.
d1b7e03d 686 * Ignored if $orderby = random or $limitnum is null.
f7970e3c 687 * @param int $limitnum implements paging of the results. null = all.
d1b7e03d
TH
688 * @return array with two elements, an array of usage ids, and a count of the total number.
689 */
690 public function load_questions_usages_where_question_in_state(
691 qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
5b0a31bf 692 $orderby = 'random', $params = array(), $limitfrom = 0, $limitnum = null) {
d1b7e03d
TH
693
694 $extrawhere = '';
695 if ($questionid) {
cf3b6568
TH
696 $extrawhere .= ' AND qa.questionid = :questionid';
697 $params['questionid'] = $questionid;
d1b7e03d
TH
698 }
699 if ($summarystate != 'all') {
cf3b6568 700 list($test, $sparams) = $this->in_summary_state_test($summarystate);
d1b7e03d 701 $extrawhere .= ' AND qas.state ' . $test;
cf3b6568 702 $params += $sparams;
d1b7e03d
TH
703 }
704
705 if ($orderby == 'random') {
706 $sqlorderby = '';
707 } else if ($orderby) {
708 $sqlorderby = 'ORDER BY ' . $orderby;
709 } else {
710 $sqlorderby = '';
711 }
712
5b0a31bf
TH
713 // We always want the total count, as well as the partcular list of ids
714 // based on the paging and sort order. Because the list of ids is never
715 // going to be too ridiculously long. My worst-case scenario is
716 // 10,000 students in the course, each doing 5 quiz attempts. That
d1b7e03d 717 // is a 50,000 element int => int array, which PHP seems to use 5MB
5b0a31bf
TH
718 // memory to store on a 64 bit server.
719 $qubaidswhere = $qubaids->where(); // Must call this before params.
cf3b6568
TH
720 $params += $qubaids->from_where_params();
721 $params['slot'] = $slot;
5b0a31bf 722
cf3b6568 723 $qubaids = $this->db->get_records_sql_menu("
d1b7e03d
TH
724SELECT
725 qa.questionusageid,
726 1
727
728FROM {$qubaids->from_question_attempts('qa')}
84b37108
TH
729JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
730 AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
cf3b6568 731JOIN {question} q ON q.id = qa.questionid
d1b7e03d
TH
732
733WHERE
5b0a31bf 734 {$qubaidswhere} AND
cf3b6568 735 qa.slot = :slot
d1b7e03d
TH
736 $extrawhere
737
738$sqlorderby
cf3b6568 739 ", $params);
d1b7e03d
TH
740
741 $qubaids = array_keys($qubaids);
742 $count = count($qubaids);
743
744 if ($orderby == 'random') {
745 shuffle($qubaids);
746 $limitfrom = 0;
747 }
748
749 if (!is_null($limitnum)) {
750 $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
751 }
752
753 return array($qubaids, $count);
754 }
755
756 /**
16e246ac
TH
757 * Load the average mark, and number of attempts, for each slot in a set of
758 * question usages..
759 *
760 * This method may be called publicly.
761 *
d1b7e03d
TH
762 * @param qubaid_condition $qubaids used to restrict which usages are included
763 * in the query. See {@link qubaid_condition}.
764 * @param array $slots if null, load info for all quesitions, otherwise only
765 * load the averages for the specified questions.
16e246ac 766 * @return array of objects with fields ->slot, ->averagefraction and ->numaveraged.
d1b7e03d
TH
767 */
768 public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
d1b7e03d 769 if (!empty($slots)) {
9c197f44
TH
770 list($slottest, $slotsparams) = $this->db->get_in_or_equal(
771 $slots, SQL_PARAMS_NAMED, 'slot');
f4fe3968 772 $slotwhere = " AND qa.slot {$slottest}";
d1b7e03d
TH
773 } else {
774 $slotwhere = '';
5b0a31bf 775 $slotsparams = array();
d1b7e03d
TH
776 }
777
e24ee794 778 list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
deef04a4
TH
779 (string) question_state::$gaveup,
780 (string) question_state::$gradedwrong,
781 (string) question_state::$gradedpartial,
782 (string) question_state::$gradedright,
783 (string) question_state::$mangaveup,
784 (string) question_state::$mangrwrong,
785 (string) question_state::$mangrpartial,
786 (string) question_state::$mangrright), SQL_PARAMS_NAMED, 'st');
d1b7e03d 787
e24ee794 788 return $this->db->get_records_sql("
d1b7e03d
TH
789SELECT
790 qa.slot,
791 AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
792 COUNT(1) AS numaveraged
793
794FROM {$qubaids->from_question_attempts('qa')}
84b37108
TH
795JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
796 AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
d1b7e03d
TH
797
798WHERE
799 {$qubaids->where()}
800 $slotwhere
801 AND qas.state $statetest
802
803GROUP BY qa.slot
804
805ORDER BY qa.slot
e24ee794 806 ", $slotsparams + $stateparams + $qubaids->from_where_params());
d1b7e03d
TH
807 }
808
809 /**
16e246ac 810 * Load all the attempts at a given queston from a set of question_usages.
d1b7e03d 811 * steps.
038014c4 812 *
16e246ac
TH
813 * This method may be called publicly.
814 *
f7970e3c 815 * @param int $questionid the question to load all the attempts fors.
d1b7e03d
TH
816 * @param qubaid_condition $qubaids used to restrict which usages are included
817 * in the query. See {@link qubaid_condition}.
038014c4 818 * @return question_attempt[] array of question_attempts that were loaded.
d1b7e03d
TH
819 */
820 public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
99caa248 821 $sql = "
d1b7e03d 822SELECT
04853f27 823 quba.contextid,
d1b7e03d
TH
824 quba.preferredbehaviour,
825 qa.id AS questionattemptid,
826 qa.questionusageid,
827 qa.slot,
828 qa.behaviour,
829 qa.questionid,
1da821bb 830 qa.variant,
d1b7e03d
TH
831 qa.maxmark,
832 qa.minfraction,
4e3d8293 833 qa.maxfraction,
d1b7e03d
TH
834 qa.flagged,
835 qa.questionsummary,
836 qa.rightanswer,
837 qa.responsesummary,
838 qa.timemodified,
839 qas.id AS attemptstepid,
840 qas.sequencenumber,
841 qas.state,
842 qas.fraction,
843 qas.timecreated,
844 qas.userid,
845 qasd.name,
846 qasd.value
847
848FROM {$qubaids->from_question_attempts('qa')}
04853f27
TH
849JOIN {question_usages} quba ON quba.id = qa.questionusageid
850LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
851LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
852
853WHERE
854 {$qubaids->where()} AND
04853f27 855 qa.questionid = :questionid
d1b7e03d
TH
856
857ORDER BY
858 quba.id,
859 qa.id,
99caa248
JP
860 qas.sequencenumber";
861
862 // For qubaid_list must call this after calling methods that generate sql.
863 $params = $qubaids->from_where_params();
864 $params['questionid'] = $questionid;
865
866 $records = $this->db->get_recordset_sql($sql, $params);
d1b7e03d 867
d1b7e03d 868 $questionattempts = array();
35d5f1c2
TH
869 while ($records->valid()) {
870 $record = $records->current();
d1b7e03d
TH
871 $questionattempts[$record->questionattemptid] =
872 question_attempt::load_from_records($records,
873 $record->questionattemptid, new question_usage_null_observer(),
874 $record->preferredbehaviour);
d1b7e03d 875 }
35d5f1c2
TH
876 $records->close();
877
d1b7e03d
TH
878 return $questionattempts;
879 }
880
881 /**
882 * Update a question_usages row to refect any changes in a usage (but not
883 * any of its question_attempts.
16e246ac
TH
884 *
885 * You should not call this method directly. You should use
886 * @link question_engine::save_questions_usage_by_activity()}.
887 *
d1b7e03d
TH
888 * @param question_usage_by_activity $quba the usage that has changed.
889 */
890 public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
0ff4bd08 891 $record = new stdClass();
d1b7e03d
TH
892 $record->id = $quba->get_id();
893 $record->contextid = $quba->get_owning_context()->id;
56b0df7e
TH
894 $record->component = $quba->get_owning_component();
895 $record->preferredbehaviour = $quba->get_preferred_behaviour();
d1b7e03d 896
88f0eb15 897 $this->db->update_record('question_usages', $record);
d1b7e03d
TH
898 }
899
900 /**
901 * Update a question_attempts row to refect any changes in a question_attempt
902 * (but not any of its steps).
16e246ac
TH
903 *
904 * You should not call this method directly. You should use
905 * @link question_engine::save_questions_usage_by_activity()}.
906 *
d1b7e03d
TH
907 * @param question_attempt $qa the question attempt that has changed.
908 */
909 public function update_question_attempt(question_attempt $qa) {
0ff4bd08 910 $record = new stdClass();
d1b7e03d 911 $record->id = $qa->get_database_id();
f6579bea 912 $record->slot = $qa->get_slot();
a8de8667 913 $record->variant = $qa->get_variant();
d1b7e03d
TH
914 $record->maxmark = $qa->get_max_mark();
915 $record->minfraction = $qa->get_min_fraction();
4e3d8293 916 $record->maxfraction = $qa->get_max_fraction();
d1b7e03d 917 $record->flagged = $qa->is_flagged();
56b0df7e
TH
918 $record->questionsummary = $qa->get_question_summary();
919 $record->rightanswer = $qa->get_right_answer_summary();
920 $record->responsesummary = $qa->get_response_summary();
d1b7e03d
TH
921 $record->timemodified = time();
922
88f0eb15 923 $this->db->update_record('question_attempts', $record);
d1b7e03d
TH
924 }
925
926 /**
927 * Delete a question_usage_by_activity and all its associated
16e246ac
TH
928 *
929 * You should not call this method directly. You should use
930 * @link question_engine::delete_questions_usage_by_activities()}.
931 *
d1b7e03d
TH
932 * {@link question_attempts} and {@link question_attempt_steps} from the
933 * database.
6b5f24d3 934 * @param qubaid_condition $qubaids identifies which question useages to delete.
d1b7e03d 935 */
6b5f24d3
TH
936 public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
937 $where = "qa.questionusageid {$qubaids->usage_id_in()}";
938 $params = $qubaids->usage_id_in_params();
939
940 $contextids = $this->db->get_records_sql_menu("
941 SELECT DISTINCT contextid, 1
942 FROM {question_usages}
a2ac2349 943 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
6b5f24d3
TH
944 foreach ($contextids as $contextid => $notused) {
945 $this->delete_response_files($contextid, "IN (
946 SELECT qas.id
947 FROM {question_attempts} qa
948 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
949 WHERE $where)", $params);
950 }
951
f0bfd964
TH
952 if ($this->db->get_dbfamily() == 'mysql') {
953 $this->delete_usage_records_for_mysql($qubaids);
954 return;
955 }
956
c76145d3 957 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
d1b7e03d 958 SELECT qas.id
c76145d3
TH
959 FROM {question_attempts} qa
960 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
c76145d3 961 WHERE $where)", $params);
6b5f24d3 962
c76145d3 963 $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
d1b7e03d 964 SELECT qa.id
c76145d3 965 FROM {question_attempts} qa
c76145d3 966 WHERE $where)", $params);
6b5f24d3
TH
967
968 $this->db->delete_records_select('question_attempts',
a2ac2349
TH
969 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
970 $qubaids->usage_id_in_params());
6b5f24d3
TH
971
972 $this->db->delete_records_select('question_usages',
a2ac2349 973 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
d1b7e03d
TH
974 }
975
f0bfd964
TH
976 /**
977 * This function is a work-around for poor MySQL performance with
978 * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
979 * syntax to get good performance. See MDL-29520.
980 * @param qubaid_condition $qubaids identifies which question useages to delete.
981 */
982 protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
2ec325c2
TH
983 $qubaidtest = $qubaids->usage_id_in();
984 if (strpos($qubaidtest, 'question_usages') !== false &&
985 strpos($qubaidtest, 'IN (SELECT') === 0) {
986 // This horrible hack is required by MDL-29847. It comes from
987 // http://www.xaprb.com/blog/2006/06/23/how-to-select-from-an-update-target-in-mysql/
988 $qubaidtest = 'IN (SELECT * FROM ' . substr($qubaidtest, 3) . ' AS hack_subquery_alias)';
989 }
990
f0bfd964
TH
991 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
992 $this->db->execute('
993 DELETE qu, qa, qas, qasd
994 FROM {question_usages} qu
995 JOIN {question_attempts} qa ON qa.questionusageid = qu.id
033d6566
EL
996 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
997 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
2ec325c2 998 WHERE qu.id ' . $qubaidtest,
f0bfd964
TH
999 $qubaids->usage_id_in_params());
1000 }
1001
d1b7e03d
TH
1002 /**
1003 * Delete all the steps for a question attempt.
16e246ac
TH
1004 *
1005 * Private method, only for use by other parts of the question engine.
1006 *
f7970e3c 1007 * @param int $qaids question_attempt id.
94815ccf 1008 * @param context $context the context that the $quba belongs to.
d1b7e03d 1009 */
94815ccf
TH
1010 public function delete_steps($stepids, $context) {
1011 if (empty($stepids)) {
d1b7e03d
TH
1012 return;
1013 }
94815ccf 1014 list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED);
6b5f24d3 1015
296e1e97 1016 $this->delete_response_files($context->id, $test, $params);
f0bfd964 1017
94815ccf 1018 $this->db->delete_records_select('question_attempt_step_data',
f4fe3968 1019 "attemptstepid {$test}", $params);
9c197f44 1020 $this->db->delete_records_select('question_attempt_steps',
f4fe3968 1021 "id {$test}", $params);
d1b7e03d
TH
1022 }
1023
6b5f24d3
TH
1024 /**
1025 * Delete all the files belonging to the response variables in the gives
1026 * question attempt steps.
1027 * @param int $contextid the context these attempts belong to.
1028 * @param string $itemidstest a bit of SQL that can be used in a
1029 * WHERE itemid $itemidstest clause. Must use named params.
1030 * @param array $params any query parameters used in $itemidstest.
1031 */
1032 protected function delete_response_files($contextid, $itemidstest, $params) {
1033 $fs = get_file_storage();
c749527b 1034 foreach (question_engine::get_all_response_file_areas() as $filearea) {
6b5f24d3
TH
1035 $fs->delete_area_files_select($contextid, 'question', $filearea,
1036 $itemidstest, $params);
1037 }
1038 }
1039
d1b7e03d
TH
1040 /**
1041 * Delete all the previews for a given question.
16e246ac
TH
1042 *
1043 * Private method, only for use by other parts of the question engine.
1044 *
f7970e3c 1045 * @param int $questionid question id.
d1b7e03d
TH
1046 */
1047 public function delete_previews($questionid) {
c76145d3 1048 $previews = $this->db->get_records_sql_menu("
d1b7e03d 1049 SELECT DISTINCT quba.id, 1
c76145d3
TH
1050 FROM {question_usages} quba
1051 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
d1b7e03d 1052 WHERE quba.component = 'core_question_preview' AND
c76145d3 1053 qa.questionid = ?", array($questionid));
d1b7e03d
TH
1054 if (empty($previews)) {
1055 return;
1056 }
6b5f24d3 1057 $this->delete_questions_usage_by_activities(new qubaid_list($previews));
d1b7e03d
TH
1058 }
1059
1060 /**
1061 * Update the flagged state of a question in the database.
16e246ac
TH
1062 *
1063 * You should call {@link question_engine::update_flag()()}
1064 * rather than calling this method directly.
1065 *
f7970e3c
TH
1066 * @param int $qubaid the question usage id.
1067 * @param int $questionid the question id.
1068 * @param int $sessionid the question_attempt id.
1069 * @param bool $newstate the new state of the flag. true = flagged.
d1b7e03d 1070 */
06f8ed54 1071 public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
f9b0500f 1072 if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
06f8ed54 1073 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
88f0eb15 1074 throw new moodle_exception('errorsavingflags', 'question');
d1b7e03d
TH
1075 }
1076
06f8ed54 1077 $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
d1b7e03d
TH
1078 }
1079
1080 /**
1081 * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
1082 * column to a summary state. Use this like
1083 * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
1084 * @param string SQL fragment.
1085 */
1086 protected function full_states_to_summary_state_sql() {
1087 $sql = '';
1088 foreach (question_state::get_all() as $state) {
f4fe3968 1089 $sql .= "WHEN '{$state}' THEN '{$state->get_summary_state()}'\n";
d1b7e03d
TH
1090 }
1091 return $sql;
1092 }
1093
1094 /**
1095 * Get the SQL needed to test that question_attempt_steps.state is in a
1096 * state corresponding to $summarystate.
16e246ac
TH
1097 *
1098 * This method may be called publicly.
1099 *
d1b7e03d
TH
1100 * @param string $summarystate one of
1101 * inprogress, needsgrading, manuallygraded or autograded
f7970e3c 1102 * @param bool $equal if false, do a NOT IN test. Default true.
d1b7e03d
TH
1103 * @return string SQL fragment.
1104 */
cf3b6568 1105 public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
d1b7e03d 1106 $states = question_state::get_all_for_summary_state($summarystate);
deef04a4
TH
1107 return $this->db->get_in_or_equal(array_map('strval', $states),
1108 SQL_PARAMS_NAMED, $prefix, $equal);
d1b7e03d
TH
1109 }
1110
1111 /**
1112 * Change the maxmark for the question_attempt with number in usage $slot
1113 * for all the specified question_attempts.
16e246ac
TH
1114 *
1115 * You should call {@link question_engine::set_max_mark_in_attempts()}
1116 * rather than calling this method directly.
1117 *
d1b7e03d 1118 * @param qubaid_condition $qubaids Selects which usages are updated.
f7970e3c 1119 * @param int $slot the number is usage to affect.
d1b7e03d
TH
1120 * @param number $newmaxmark the new max mark to set.
1121 */
1122 public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
cf256c05
TH
1123 if ($this->db->get_dbfamily() == 'mysql') {
1124 // MySQL's query optimiser completely fails to cope with the
1125 // set_field_select call below, so we have to give it a clue. See MDL-32616.
1126 // TODO MDL-29589 encapsulate this MySQL-specific code with a $DB method.
1127 $this->db->execute("
1128 UPDATE " . $qubaids->from_question_attempts('qa') . "
1129 SET qa.maxmark = :newmaxmark
1130 WHERE " . $qubaids->where() . "
1131 AND slot = :slot
1132 ", $qubaids->from_where_params() + array('newmaxmark' => $newmaxmark, 'slot' => $slot));
1133 return;
1134 }
1135
1136 // Normal databases.
c76145d3
TH
1137 $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
1138 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
1139 $qubaids->usage_id_in_params() + array('slot' => $slot));
d1b7e03d
TH
1140 }
1141
1142 /**
1143 * Return a subquery that computes the sum of the marks for all the questions
1144 * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
1145 * parameter.
1146 *
1147 * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
1148 * this method.
1149 *
16e246ac
TH
1150 * This method may be called publicly.
1151 *
d1b7e03d 1152 * @param string $qubaid SQL fragment that controls which usage is summed.
2daffca5
TH
1153 * This will normally be the name of a column in the outer query. Not that this
1154 * SQL fragment must not contain any placeholders.
d1b7e03d
TH
1155 * @return string SQL code for the subquery.
1156 */
1157 public function sum_usage_marks_subquery($qubaid) {
ad73a2cb
TH
1158 // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives
1159 // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that.
1160 // We always want to return a number, so the COALESCE is there to turn the
1161 // NULL total into a 0.
1162 return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0)
c76145d3 1163 FROM {question_attempts} qa
84b37108
TH
1164 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
1165 AND qas.sequencenumber = (
1166 SELECT MAX(summarks_qas.sequencenumber)
1167 FROM {question_attempt_steps} summarks_qas
1168 WHERE summarks_qas.questionattemptid = qa.id
d2c69d93 1169 )
d1b7e03d 1170 WHERE qa.questionusageid = $qubaid
9c197f44
TH
1171 HAVING COUNT(CASE
1172 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1
1173 ELSE NULL
1174 END) = 0";
d1b7e03d
TH
1175 }
1176
94c0ec21
TH
1177 /**
1178 * Get a subquery that returns the latest step of every qa in some qubas.
1179 * Currently, this is only used by the quiz reports. See
ac4d9157 1180 * {@link quiz_attempts_report_table::add_latest_state_join()}.
16e246ac
TH
1181 *
1182 * This method may be called publicly.
1183 *
94c0ec21
TH
1184 * @param string $alias alias to use for this inline-view.
1185 * @param qubaid_condition $qubaids restriction on which question_usages we
1186 * are interested in. This is important for performance.
1187 * @return array with two elements, the SQL fragment and any params requried.
1188 */
1189 public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) {
1190 return array("(
1191 SELECT {$alias}qa.id AS questionattemptid,
1192 {$alias}qa.questionusageid,
1193 {$alias}qa.slot,
1194 {$alias}qa.behaviour,
1195 {$alias}qa.questionid,
1196 {$alias}qa.variant,
1197 {$alias}qa.maxmark,
1198 {$alias}qa.minfraction,
4e3d8293 1199 {$alias}qa.maxfraction,
94c0ec21
TH
1200 {$alias}qa.flagged,
1201 {$alias}qa.questionsummary,
1202 {$alias}qa.rightanswer,
1203 {$alias}qa.responsesummary,
1204 {$alias}qa.timemodified,
1205 {$alias}qas.id AS attemptstepid,
1206 {$alias}qas.sequencenumber,
1207 {$alias}qas.state,
1208 {$alias}qas.fraction,
1209 {$alias}qas.timecreated,
1210 {$alias}qas.userid
1211
1212 FROM {$qubaids->from_question_attempts($alias . 'qa')}
84b37108
TH
1213 JOIN {question_attempt_steps} {$alias}qas ON {$alias}qas.questionattemptid = {$alias}qa.id
1214 AND {$alias}qas.sequencenumber = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
94c0ec21 1215 WHERE {$qubaids->where()}
f4fe3968 1216 ) {$alias}", $qubaids->from_where_params());
d1b7e03d
TH
1217 }
1218
1219 protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
d1b7e03d 1220 return "(
84b37108 1221 SELECT MAX(sequencenumber)
c76145d3 1222 FROM {question_attempt_steps}
d1b7e03d
TH
1223 WHERE questionattemptid = $questionattemptid
1224 )";
1225 }
1226
1227 /**
16e246ac
TH
1228 * Are any of these questions are currently in use?
1229 *
1230 * You should call {@link question_engine::questions_in_use()}
1231 * rather than calling this method directly.
1232 *
d1b7e03d 1233 * @param array $questionids of question ids.
07f88584 1234 * @param qubaid_condition $qubaids ids of the usages to consider.
16e246ac 1235 * @return bool whether any of these questions are being used by any of
07f88584 1236 * those usages.
d1b7e03d 1237 */
07f88584 1238 public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
c76145d3
TH
1239 list($test, $params) = $this->db->get_in_or_equal($questionids);
1240 return $this->db->record_exists_select('question_attempts',
07f88584
TH
1241 'questionid ' . $test . ' AND questionusageid ' .
1242 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
d1b7e03d 1243 }
bb93fc24
TH
1244
1245 /**
1246 * Get the number of times each variant has been used for each question in a list
1247 * in a set of usages.
1248 * @param array $questionids of question ids.
1249 * @param qubaid_condition $qubaids ids of the usages to consider.
1250 * @return array questionid => variant number => num uses.
1251 */
1252 public function load_used_variants(array $questionids, qubaid_condition $qubaids) {
1253 list($test, $params) = $this->db->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'qid');
1254 $recordset = $this->db->get_recordset_sql("
1255 SELECT qa.questionid, qa.variant, COUNT(1) AS usescount
1256 FROM " . $qubaids->from_question_attempts('qa') . "
1257 WHERE qa.questionid $test
1258 AND " . $qubaids->where() . "
1259 GROUP BY qa.questionid, qa.variant
1260 ORDER BY COUNT(1) ASC
1261 ", $params + $qubaids->from_where_params());
1262
1263 $usedvariants = array_combine($questionids, array_fill(0, count($questionids), array()));
1264 foreach ($recordset as $row) {
1265 $usedvariants[$row->questionid][$row->variant] = $row->usescount;
1266 }
1267 $recordset->close();
1268 return $usedvariants;
1269 }
d1b7e03d
TH
1270}
1271
6b5f24d3 1272
d1b7e03d
TH
1273/**
1274 * Implementation of the unit of work pattern for the question engine.
1275 *
1276 * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
1277 * changes to a {@link question_usage_by_activity}, and its constituent parts,
1278 * so that the changes can be saved to the database when {@link save()} is called.
1279 *
017bc1d9
TH
1280 * @copyright 2009 The Open University
1281 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1282 */
1283class question_engine_unit_of_work implements question_usage_observer {
1284 /** @var question_usage_by_activity the usage being tracked. */
1285 protected $quba;
1286
1287 /** @var boolean whether any of the fields of the usage have been changed. */
1288 protected $modified = false;
1289
1290 /**
94815ccf 1291 * @var array list of slot => {@link question_attempt}s that
f6579bea 1292 * have been added to the usage.
1700bd4d 1293 */
f6579bea 1294 protected $attemptsadded = array();
1700bd4d 1295
d1b7e03d 1296 /**
94815ccf 1297 * @var array list of slot => {@link question_attempt}s that
f6579bea 1298 * were already in the usage, and which have been modified.
d1b7e03d 1299 */
f6579bea 1300 protected $attemptsmodified = array();
d1b7e03d
TH
1301
1302 /**
94815ccf
TH
1303 * @var array of array(question_attempt_step, question_attempt id, seq number)
1304 * of steps that have been added to question attempts in this usage.
d1b7e03d 1305 */
94815ccf 1306 protected $stepsadded = array();
d1b7e03d
TH
1307
1308 /**
94815ccf
TH
1309 * @var array of array(question_attempt_step, question_attempt id, seq number)
1310 * of steps that have been modified in their attempt.
d1b7e03d 1311 */
94815ccf
TH
1312 protected $stepsmodified = array();
1313
1314 /**
1315 * @var array list of question_attempt_step.id => question_attempt_step of steps
1316 * that were previously stored in the database, but which are no longer required.
1317 */
1318 protected $stepsdeleted = array();
d1b7e03d 1319
f6579bea
TH
1320 /**
1321 * @var array int slot => string name => question_attempt.
1322 */
1323 protected $metadataadded = array();
1324
1325 /**
1326 * @var array int slot => string name => question_attempt.
1327 */
1328 protected $metadatamodified = array();
1329
d1b7e03d
TH
1330 /**
1331 * Constructor.
1332 * @param question_usage_by_activity $quba the usage to track.
1333 */
1334 public function __construct(question_usage_by_activity $quba) {
1335 $this->quba = $quba;
1336 }
1337
1338 public function notify_modified() {
1339 $this->modified = true;
1340 }
1341
f6579bea
TH
1342 public function notify_attempt_added(question_attempt $qa) {
1343 $this->attemptsadded[$qa->get_slot()] = $qa;
1344 }
1345
d1b7e03d 1346 public function notify_attempt_modified(question_attempt $qa) {
94815ccf
TH
1347 $slot = $qa->get_slot();
1348 if (!array_key_exists($slot, $this->attemptsadded)) {
1349 $this->attemptsmodified[$slot] = $qa;
d1b7e03d
TH
1350 }
1351 }
1352
f6579bea
TH
1353 public function notify_attempt_moved(question_attempt $qa, $oldslot) {
1354 $newslot = $qa->get_slot();
1355
1356 if (array_key_exists($oldslot, $this->attemptsadded)) {
1357 unset($this->attemptsadded[$oldslot]);
1358 $this->attemptsadded[$newslot] = $qa;
1359 return;
1700bd4d 1360 }
1700bd4d 1361
f6579bea
TH
1362 if (array_key_exists($oldslot, $this->attemptsmodified)) {
1363 unset($this->attemptsmodified[$oldslot]);
1364 }
1365 $this->attemptsmodified[$newslot] = $qa;
1366
1367 if (array_key_exists($oldslot, $this->metadataadded)) {
1368 $this->metadataadded[$newslot] = $this->metadataadded[$oldslot];
1369 unset($this->metadataadded[$oldslot]);
1370 }
1371 if (array_key_exists($oldslot, $this->metadatamodified)) {
1372 $this->metadatamodified[$newslot] = $this->metadatamodified[$oldslot];
1373 unset($this->metadatamodified[$oldslot]);
1700bd4d 1374 }
d1b7e03d
TH
1375 }
1376
94815ccf 1377 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
d1b7e03d
TH
1378 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1379 return;
1380 }
1381
94815ccf
TH
1382 if (($key = $this->is_step_added($step)) !== false) {
1383 return;
1384 }
1385
1386 if (($key = $this->is_step_modified($step)) !== false) {
1387 throw new coding_exception('Cannot add a step that has already been modified.');
1388 }
1389
1390 if (($key = $this->is_step_deleted($step)) !== false) {
1391 unset($this->stepsdeleted[$step->get_id()]);
1392 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1393 return;
1394 }
1395
1396 $stepid = $step->get_id();
1397 if ($stepid) {
1398 if (array_key_exists($stepid, $this->stepsdeleted)) {
1399 unset($this->stepsdeleted[$stepid]);
d1b7e03d 1400 }
94815ccf
TH
1401 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1402
1403 } else {
1404 $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
1405 }
1406 }
1407
1408 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
1409 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1410 return;
d1b7e03d
TH
1411 }
1412
94815ccf
TH
1413 if (($key = $this->is_step_added($step)) !== false) {
1414 return;
1415 }
1416
1417 if (($key = $this->is_step_deleted($step)) !== false) {
1418 throw new coding_exception('Cannot modify a step after it has been deleted.');
1419 }
1420
1421 $stepid = $step->get_id();
1422 if (empty($stepid)) {
1423 throw new coding_exception('Cannot modify a step that has never been stored in the database.');
1424 }
1425
1426 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
d1b7e03d
TH
1427 }
1428
94815ccf 1429 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
d1b7e03d
TH
1430 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1431 return;
1432 }
94815ccf
TH
1433
1434 if (($key = $this->is_step_added($step)) !== false) {
1435 unset($this->stepsadded[$key]);
1436 return;
1437 }
1438
1439 if (($key = $this->is_step_modified($step)) !== false) {
1440 unset($this->stepsmodified[$key]);
1441 }
1442
1443 $stepid = $step->get_id();
1444 if (empty($stepid)) {
1445 return; // Was never in the database.
1446 }
1447
1448 $this->stepsdeleted[$stepid] = $step;
1449 }
1450
f6579bea
TH
1451 public function notify_metadata_added(question_attempt $qa, $name) {
1452 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1453 return;
1454 }
1455
1456 if ($this->is_step_added($qa->get_step(0)) !== false) {
1457 return;
1458 }
1459
1460 if (isset($this->metadataadded[$qa->get_slot()][$name])) {
1461 return;
1462 }
1463
1464 $this->metadataadded[$qa->get_slot()][$name] = $qa;
1465 }
1466
1467 public function notify_metadata_modified(question_attempt $qa, $name) {
1468 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1469 return;
1470 }
1471
1472 if ($this->is_step_added($qa->get_step(0)) !== false) {
1473 return;
1474 }
1475
1476 if (isset($this->metadataadded[$qa->get_slot()][$name])) {
1477 return;
1478 }
1479
1480 if (isset($this->metadatamodified[$qa->get_slot()][$name])) {
1481 return;
1482 }
1483
1484 $this->metadatamodified[$qa->get_slot()][$name] = $qa;
1485 }
1486
94815ccf
TH
1487 /**
1488 * @param question_attempt_step $step a step
1489 * @return int|false if the step is in the list of steps to be added, return
1490 * the key, otherwise return false.
1491 */
1492 protected function is_step_added(question_attempt_step $step) {
1493 foreach ($this->stepsadded as $key => $data) {
1494 list($addedstep, $qaid, $seq) = $data;
1495 if ($addedstep === $step) {
1496 return $key;
1497 }
1498 }
1499 return false;
1500 }
1501
1502 /**
1503 * @param question_attempt_step $step a step
1504 * @return int|false if the step is in the list of steps to be modified, return
1505 * the key, otherwise return false.
1506 */
1507 protected function is_step_modified(question_attempt_step $step) {
1508 foreach ($this->stepsmodified as $key => $data) {
1509 list($modifiedstep, $qaid, $seq) = $data;
1510 if ($modifiedstep === $step) {
1511 return $key;
1512 }
1513 }
1514 return false;
1515 }
1516
1517 /**
1518 * @param question_attempt_step $step a step
1519 * @return bool whether the step is in the list of steps to be deleted.
1520 */
1521 protected function is_step_deleted(question_attempt_step $step) {
1522 foreach ($this->stepsdeleted as $deletedstep) {
1523 if ($deletedstep === $step) {
1524 return true;
1525 }
1526 }
1527 return false;
d1b7e03d
TH
1528 }
1529
1530 /**
1531 * Write all the changes we have recorded to the database.
1532 * @param question_engine_data_mapper $dm the mapper to use to update the database.
1533 */
1534 public function save(question_engine_data_mapper $dm) {
94815ccf
TH
1535 $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context());
1536
4040e2dd
TH
1537 // Initially an array of array of question_attempt_step_objects.
1538 // Built as a nested array for efficiency, then flattened.
1539 $stepdata = array();
1540
94815ccf
TH
1541 foreach ($this->stepsmodified as $stepinfo) {
1542 list($step, $questionattemptid, $seq) = $stepinfo;
4040e2dd
TH
1543 $stepdata[] = $dm->update_question_attempt_step(
1544 $step, $questionattemptid, $seq, $this->quba->get_owning_context());
94815ccf 1545 }
6b5f24d3 1546
d1b7e03d
TH
1547 foreach ($this->stepsadded as $stepinfo) {
1548 list($step, $questionattemptid, $seq) = $stepinfo;
4040e2dd
TH
1549 $stepdata[] = $dm->insert_question_attempt_step(
1550 $step, $questionattemptid, $seq, $this->quba->get_owning_context());
d1b7e03d 1551 }
6b5f24d3 1552
f6579bea
TH
1553 foreach ($this->attemptsmodified as $qa) {
1554 $dm->update_question_attempt($qa);
1700bd4d
K
1555 }
1556
d1b7e03d 1557 foreach ($this->attemptsadded as $qa) {
4040e2dd
TH
1558 $stepdata[] = $dm->insert_question_attempt(
1559 $qa, $this->quba->get_owning_context());
d1b7e03d 1560 }
6b5f24d3 1561
f6579bea
TH
1562 foreach ($this->metadataadded as $info) {
1563 $qa = reset($info);
1564 $stepdata[] = $dm->insert_question_attempt_metadata($qa, array_keys($info));
1565 }
1566
1567 foreach ($this->metadatamodified as $info) {
1568 $qa = reset($info);
1569 $stepdata[] = $dm->update_question_attempt_metadata($qa, array_keys($info));
d1b7e03d 1570 }
6b5f24d3 1571
d1b7e03d
TH
1572 if ($this->modified) {
1573 $dm->update_questions_usage_by_activity($this->quba);
1574 }
4040e2dd 1575
f6579bea
TH
1576 if ($stepdata) {
1577 $dm->insert_all_step_data(call_user_func_array('array_merge', $stepdata));
4040e2dd 1578 }
f6579bea
TH
1579
1580 $this->stepsdeleted = array();
1581 $this->stepsmodified = array();
1582 $this->stepsadded = array();
1583 $this->attemptsdeleted = array();
1584 $this->attemptsadded = array();
1585 $this->attemptsmodified = array();
1586 $this->modified = false;
d1b7e03d
TH
1587 }
1588}
1589
1590
8a1e7b77
TH
1591/**
1592 * The interface implemented by {@link question_file_saver} and {@link question_file_loader}.
1593 *
1594 * @copyright 2012 The Open University
1595 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1596 */
1597interface question_response_files {
1598 /**
1599 * Get the files that were submitted.
1600 * @return array of stored_files objects.
1601 */
1602 public function get_files();
1603}
1604
1605
217f9a61
TH
1606/**
1607 * This class represents the promise to save some files from a particular draft
1608 * file area into a particular file area. It is used beause the necessary
1609 * information about what to save is to hand in the
1610 * {@link question_attempt::process_response_files()} method, but we don't know
1611 * if this question attempt will actually be saved in the database until later,
1612 * when the {@link question_engine_unit_of_work} is saved, if it is.
1613 *
1614 * @copyright 2011 The Open University
1615 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1616 */
8a1e7b77 1617class question_file_saver implements question_response_files {
217f9a61
TH
1618 /** @var int the id of the draft file area to save files from. */
1619 protected $draftitemid;
1620 /** @var string the owning component name. */
1621 protected $component;
1622 /** @var string the file area name. */
1623 protected $filearea;
1624
cd3557e6
TH
1625 /**
1626 * @var string the value to store in the question_attempt_step_data to
1627 * represent these files.
1628 */
1629 protected $value = null;
1630
217f9a61
TH
1631 /**
1632 * Constuctor.
1633 * @param int $draftitemid the draft area to save the files from.
1634 * @param string $component the component for the file area to save into.
1635 * @param string $filearea the name of the file area to save into.
1636 */
48d9c17d 1637 public function __construct($draftitemid, $component, $filearea, $text = null) {
217f9a61
TH
1638 $this->draftitemid = $draftitemid;
1639 $this->component = $component;
1640 $this->filearea = $filearea;
48d9c17d 1641 $this->value = $this->compute_value($draftitemid, $text);
217f9a61
TH
1642 }
1643
48d9c17d
TH
1644 /**
1645 * Compute the value that should be stored in the question_attempt_step_data
1646 * table. Contains a hash that (almost) uniquely encodes all the files.
1647 * @param int $draftitemid the draft file area itemid.
1648 * @param string $text optional content containing file links.
1649 */
1650 protected function compute_value($draftitemid, $text) {
cd3557e6
TH
1651 global $USER;
1652
cd3557e6 1653 $fs = get_file_storage();
21c08c63 1654 $usercontext = context_user::instance($USER->id);
cd3557e6
TH
1655
1656 $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
48d9c17d 1657 $draftitemid, 'sortorder, filepath, filename', false);
cd3557e6
TH
1658
1659 $string = '';
1660 foreach ($files as $file) {
1661 $string .= $file->get_filepath() . $file->get_filename() . '|' .
1662 $file->get_contenthash() . '|';
1663 }
d629327a 1664 $hash = md5($string);
cd3557e6 1665
48d9c17d 1666 if (is_null($text)) {
d629327a
TH
1667 if ($string) {
1668 return $hash;
1669 } else {
1670 return '';
1671 }
48d9c17d
TH
1672 }
1673
1674 // We add the file hash so a simple string comparison will say if the
1675 // files have been changed. First strip off any existing file hash.
d629327a
TH
1676 if ($text !== '') {
1677 $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
1678 $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
3fc9410f
TH
1679 if ($string) {
1680 $text .= '<!-- File hash: ' . $hash . ' -->';
1681 }
48d9c17d
TH
1682 }
1683 return $text;
cd3557e6
TH
1684 }
1685
217f9a61 1686 public function __toString() {
48d9c17d 1687 return $this->value;
217f9a61
TH
1688 }
1689
1690 /**
1691 * Actually save the files.
1692 * @param integer $itemid the item id for the file area to save into.
1693 */
1694 public function save_files($itemid, $context) {
1695 file_save_draft_area_files($this->draftitemid, $context->id,
1696 $this->component, $this->filearea, $itemid);
1697 }
8a1e7b77
TH
1698
1699 /**
1700 * Get the files that were submitted.
1701 * @return array of stored_files objects.
1702 */
1703 public function get_files() {
1704 global $USER;
1705
1706 $fs = get_file_storage();
1707 $usercontext = context_user::instance($USER->id);
1708
1709 return $fs->get_area_files($usercontext->id, 'user', 'draft',
1710 $this->draftitemid, 'sortorder, filepath, filename', false);
1711 }
1712}
1713
1714
1715/**
1716 * This class is the mirror image of {@link question_file_saver}. It allows
1717 * files to be accessed again later (e.g. when re-grading) using that same
1718 * API as when doing the original grading.
1719 *
1720 * @copyright 2012 The Open University
1721 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1722 */
1723class question_file_loader implements question_response_files {
1724 /** @var question_attempt_step the step that these files belong to. */
1725 protected $step;
1726
1727 /** @var string the field name for these files - which is used to construct the file area name. */
1728 protected $name;
1729
1730 /**
1700bd4d 1731 * @var string the value to stored in the question_attempt_step_data to
8a1e7b77 1732 * represent these files.
1700bd4d 1733 */
8a1e7b77
TH
1734 protected $value;
1735
1736 /** @var int the context id that the files belong to. */
1737 protected $contextid;
1738
1739 /**
1740 * Constuctor.
1741 * @param question_attempt_step $step the step that these files belong to.
1742 * @param string $name string the field name for these files - which is used to construct the file area name.
1743 * @param string $value the value to stored in the question_attempt_step_data to
1744 * represent these files.
1745 * @param int $contextid the context id that the files belong to.
1746 */
1747 public function __construct(question_attempt_step $step, $name, $value, $contextid) {
d0782585
MPC
1748 $this->step = $step;
1749 $this->name = $name;
1750 $this->value = $value;
1751 $this->contextid = $contextid;
8a1e7b77
TH
1752 }
1753
1754 public function __toString() {
1755 return $this->value;
1756 }
1757
1758 /**
1759 * Get the files that were submitted.
1760 * @return array of stored_files objects.
1761 */
1762 public function get_files() {
1763 return $this->step->get_qt_files($this->name, $this->contextid);
1764 }
afb1b3d0
TH
1765
1766 /**
1767 * Copy these files into a draft area, and return the corresponding
1768 * {@link question_file_saver} that can save them again.
1769 *
1770 * This is used by {@link question_attempt::start_based_on()}, which is used
1771 * (for example) by the quizzes 'Each attempt builds on last' feature.
1772 *
1773 * @return question_file_saver that can re-save these files again.
1774 */
1775 public function get_question_file_saver() {
1776
d629327a
TH
1777 // There are three possibilities here for what $value will look like:
1778 // 1) some HTML content followed by an MD5 hash in a HTML comment;
1779 // 2) a plain MD5 hash;
1780 // 3) or some real content, without any hash.
1781 // The problem is that 3) is ambiguous in the case where a student writes
1782 // a response that looks exactly like an MD5 hash. For attempts made now,
1783 // we avoid case 3) by always going for case 1) or 2) (except when the
1784 // response is blank. However, there may be case 3) data in the database
1785 // so we need to handle it as best we can.
afb1b3d0
TH
1786 if (preg_match('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', $this->value)) {
1787 $value = preg_replace('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', '', $this->value);
1788
1789 } else if (preg_match('/^[0-9a-zA-Z]{32}$/', $this->value)) {
1790 $value = null;
1791
1792 } else {
d629327a 1793 $value = $this->value;
afb1b3d0
TH
1794 }
1795
1796 list($draftid, $text) = $this->step->prepare_response_files_draft_itemid_with_text(
1797 $this->name, $this->contextid, $value);
1798 return new question_file_saver($draftid, 'question', 'response_' . $this->name, $text);
1799 }
217f9a61
TH
1800}
1801
1802
d1b7e03d
TH
1803/**
1804 * This class represents a restriction on the set of question_usage ids to include
1805 * in a larger database query. Depending of the how you are going to restrict the
1806 * list of usages, construct an appropriate subclass.
1807 *
1808 * If $qubaids is an instance of this class, example usage might be
1809 *
1810 * SELECT qa.id, qa.maxmark
1811 * FROM $qubaids->from_question_attempts('qa')
1812 * WHERE $qubaids->where() AND qa.slot = 1
1813 *
f7970e3c 1814 * @copyright 2010 The Open University
017bc1d9 1815 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1816 */
1817abstract class qubaid_condition {
1818
1819 /**
1820 * @return string the SQL that needs to go in the FROM clause when trying
1821 * to select records from the 'question_attempts' table based on the
1822 * qubaid_condition.
1823 */
1824 public abstract function from_question_attempts($alias);
1825
1826 /** @return string the SQL that needs to go in the where clause. */
1827 public abstract function where();
1828
1829 /**
1830 * @return the params needed by a query that uses
1831 * {@link from_question_attempts()} and {@link where()}.
1832 */
1833 public abstract function from_where_params();
1834
1835 /**
1836 * @return string SQL that can use used in a WHERE qubaid IN (...) query.
1837 * This method returns the "IN (...)" part.
1838 */
1839 public abstract function usage_id_in();
1840
1841 /**
1842 * @return the params needed by a query that uses {@link usage_id_in()}.
1843 */
1844 public abstract function usage_id_in_params();
e68e4ccf
JP
1845
1846 /**
1847 * @return string 40-character hash code that uniquely identifies the combination of properties and class name of this qubaid
1848 * condition.
1849 */
1850 public function get_hash_code() {
1851 return sha1(serialize($this));
1852 }
d1b7e03d
TH
1853}
1854
1855
1856/**
1857 * This class represents a restriction on the set of question_usage ids to include
1858 * in a larger database query based on an explicit list of ids.
1859 *
f7970e3c 1860 * @copyright 2010 The Open University
017bc1d9 1861 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1862 */
1863class qubaid_list extends qubaid_condition {
1864 /** @var array of ids. */
1865 protected $qubaids;
1866 protected $columntotest = null;
1867 protected $params;
1868
1869 /**
1870 * Constructor.
1871 * @param array $qubaids of question usage ids.
1872 */
1873 public function __construct(array $qubaids) {
1874 $this->qubaids = $qubaids;
1875 }
1876
1877 public function from_question_attempts($alias) {
d1b7e03d 1878 $this->columntotest = $alias . '.questionusageid';
9b40c540 1879 return '{question_attempts} ' . $alias;
d1b7e03d
TH
1880 }
1881
1882 public function where() {
1883 global $DB;
1884
1885 if (is_null($this->columntotest)) {
88f0eb15 1886 throw new coding_exception('Must call from_question_attempts before where().');
d1b7e03d
TH
1887 }
1888 if (empty($this->qubaids)) {
9b40c540 1889 $this->params = array();
d1b7e03d
TH
1890 return '1 = 0';
1891 }
d1b7e03d 1892
9b40c540 1893 return $this->columntotest . ' ' . $this->usage_id_in();
d1b7e03d
TH
1894 }
1895
1896 public function from_where_params() {
1897 return $this->params;
1898 }
1899
1900 public function usage_id_in() {
1901 global $DB;
1902
1903 if (empty($this->qubaids)) {
5f79a9bc 1904 $this->params = array();
d1b7e03d
TH
1905 return '= 0';
1906 }
9c197f44
TH
1907 list($where, $this->params) = $DB->get_in_or_equal(
1908 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid');
d1b7e03d
TH
1909 return $where;
1910 }
1911
1912 public function usage_id_in_params() {
1913 return $this->params;
1914 }
1915}
1916
1917
1918/**
1919 * This class represents a restriction on the set of question_usage ids to include
1920 * in a larger database query based on JOINing to some other tables.
1921 *
1922 * The general form of the query is something like
1923 *
1924 * SELECT qa.id, qa.maxmark
1925 * FROM $from
2a3bdbf9 1926 * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
d1b7e03d
TH
1927 * WHERE $where AND qa.slot = 1
1928 *
1929 * where $from, $usageidcolumn and $where are the arguments to the constructor.
1930 *
f7970e3c 1931 * @copyright 2010 The Open University
017bc1d9 1932 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1933 */
1934class qubaid_join extends qubaid_condition {
1935 public $from;
1936 public $usageidcolumn;
1937 public $where;
1938 public $params;
1939
1940 /**
1941 * Constructor. The meaning of the arguments is explained in the class comment.
1942 * @param string $from SQL fragemnt to go in the FROM clause.
1943 * @param string $usageidcolumn the column in $from that should be
1944 * made equal to the usageid column in the JOIN clause.
1945 * @param string $where SQL fragment to go in the where clause.
e24ee794 1946 * @param array $params required by the SQL. You must use named parameters.
d1b7e03d
TH
1947 */
1948 public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
1949 $this->from = $from;
1950 $this->usageidcolumn = $usageidcolumn;
1951 $this->params = $params;
1952 if (empty($where)) {
1953 $where = '1 = 1';
1954 }
1955 $this->where = $where;
1956 }
1957
1958 public function from_question_attempts($alias) {
f4fe3968 1959 return "{$this->from}
0f33deaf 1960 JOIN {question_attempts} {$alias} ON " .
d1b7e03d
TH
1961 "{$alias}.questionusageid = $this->usageidcolumn";
1962 }
1963
1964 public function where() {
1965 return $this->where;
1966 }
1967
1968 public function from_where_params() {
1969 return $this->params;
1970 }
1971
1972 public function usage_id_in() {
f4fe3968 1973 return "IN (SELECT {$this->usageidcolumn} FROM {$this->from} WHERE {$this->where})";
d1b7e03d
TH
1974 }
1975
1976 public function usage_id_in_params() {
1977 return $this->params;
1978 }
1979}