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