MDL-32616 question engine: move unit test to better name
[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
TH
869 $record->id = $qa->get_database_id();
870 $record->maxmark = $qa->get_max_mark();
871 $record->minfraction = $qa->get_min_fraction();
4e3d8293 872 $record->maxfraction = $qa->get_max_fraction();
d1b7e03d 873 $record->flagged = $qa->is_flagged();
56b0df7e
TH
874 $record->questionsummary = $qa->get_question_summary();
875 $record->rightanswer = $qa->get_right_answer_summary();
876 $record->responsesummary = $qa->get_response_summary();
d1b7e03d
TH
877 $record->timemodified = time();
878
88f0eb15 879 $this->db->update_record('question_attempts', $record);
d1b7e03d
TH
880 }
881
882 /**
883 * Delete a question_usage_by_activity and all its associated
16e246ac
TH
884 *
885 * You should not call this method directly. You should use
886 * @link question_engine::delete_questions_usage_by_activities()}.
887 *
d1b7e03d
TH
888 * {@link question_attempts} and {@link question_attempt_steps} from the
889 * database.
6b5f24d3 890 * @param qubaid_condition $qubaids identifies which question useages to delete.
d1b7e03d 891 */
6b5f24d3
TH
892 public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
893 $where = "qa.questionusageid {$qubaids->usage_id_in()}";
894 $params = $qubaids->usage_id_in_params();
895
896 $contextids = $this->db->get_records_sql_menu("
897 SELECT DISTINCT contextid, 1
898 FROM {question_usages}
a2ac2349 899 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
6b5f24d3
TH
900 foreach ($contextids as $contextid => $notused) {
901 $this->delete_response_files($contextid, "IN (
902 SELECT qas.id
903 FROM {question_attempts} qa
904 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
905 WHERE $where)", $params);
906 }
907
f0bfd964
TH
908 if ($this->db->get_dbfamily() == 'mysql') {
909 $this->delete_usage_records_for_mysql($qubaids);
910 return;
911 }
912
c76145d3 913 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
d1b7e03d 914 SELECT qas.id
c76145d3
TH
915 FROM {question_attempts} qa
916 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
c76145d3 917 WHERE $where)", $params);
6b5f24d3 918
c76145d3 919 $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
d1b7e03d 920 SELECT qa.id
c76145d3 921 FROM {question_attempts} qa
c76145d3 922 WHERE $where)", $params);
6b5f24d3
TH
923
924 $this->db->delete_records_select('question_attempts',
a2ac2349
TH
925 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
926 $qubaids->usage_id_in_params());
6b5f24d3
TH
927
928 $this->db->delete_records_select('question_usages',
a2ac2349 929 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
d1b7e03d
TH
930 }
931
f0bfd964
TH
932 /**
933 * This function is a work-around for poor MySQL performance with
934 * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
935 * syntax to get good performance. See MDL-29520.
936 * @param qubaid_condition $qubaids identifies which question useages to delete.
937 */
938 protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
2ec325c2
TH
939 $qubaidtest = $qubaids->usage_id_in();
940 if (strpos($qubaidtest, 'question_usages') !== false &&
941 strpos($qubaidtest, 'IN (SELECT') === 0) {
942 // This horrible hack is required by MDL-29847. It comes from
943 // http://www.xaprb.com/blog/2006/06/23/how-to-select-from-an-update-target-in-mysql/
944 $qubaidtest = 'IN (SELECT * FROM ' . substr($qubaidtest, 3) . ' AS hack_subquery_alias)';
945 }
946
f0bfd964
TH
947 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
948 $this->db->execute('
949 DELETE qu, qa, qas, qasd
950 FROM {question_usages} qu
951 JOIN {question_attempts} qa ON qa.questionusageid = qu.id
033d6566
EL
952 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
953 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
2ec325c2 954 WHERE qu.id ' . $qubaidtest,
f0bfd964
TH
955 $qubaids->usage_id_in_params());
956 }
957
d1b7e03d
TH
958 /**
959 * Delete all the steps for a question attempt.
16e246ac
TH
960 *
961 * Private method, only for use by other parts of the question engine.
962 *
f7970e3c 963 * @param int $qaids question_attempt id.
94815ccf 964 * @param context $context the context that the $quba belongs to.
d1b7e03d 965 */
94815ccf
TH
966 public function delete_steps($stepids, $context) {
967 if (empty($stepids)) {
d1b7e03d
TH
968 return;
969 }
94815ccf 970 list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED);
6b5f24d3 971
296e1e97 972 $this->delete_response_files($context->id, $test, $params);
f0bfd964 973
94815ccf 974 $this->db->delete_records_select('question_attempt_step_data',
f4fe3968 975 "attemptstepid {$test}", $params);
9c197f44 976 $this->db->delete_records_select('question_attempt_steps',
f4fe3968 977 "id {$test}", $params);
d1b7e03d
TH
978 }
979
6b5f24d3
TH
980 /**
981 * Delete all the files belonging to the response variables in the gives
982 * question attempt steps.
983 * @param int $contextid the context these attempts belong to.
984 * @param string $itemidstest a bit of SQL that can be used in a
985 * WHERE itemid $itemidstest clause. Must use named params.
986 * @param array $params any query parameters used in $itemidstest.
987 */
988 protected function delete_response_files($contextid, $itemidstest, $params) {
989 $fs = get_file_storage();
c749527b 990 foreach (question_engine::get_all_response_file_areas() as $filearea) {
6b5f24d3
TH
991 $fs->delete_area_files_select($contextid, 'question', $filearea,
992 $itemidstest, $params);
993 }
994 }
995
d1b7e03d
TH
996 /**
997 * Delete all the previews for a given question.
16e246ac
TH
998 *
999 * Private method, only for use by other parts of the question engine.
1000 *
f7970e3c 1001 * @param int $questionid question id.
d1b7e03d
TH
1002 */
1003 public function delete_previews($questionid) {
c76145d3 1004 $previews = $this->db->get_records_sql_menu("
d1b7e03d 1005 SELECT DISTINCT quba.id, 1
c76145d3
TH
1006 FROM {question_usages} quba
1007 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
d1b7e03d 1008 WHERE quba.component = 'core_question_preview' AND
c76145d3 1009 qa.questionid = ?", array($questionid));
d1b7e03d
TH
1010 if (empty($previews)) {
1011 return;
1012 }
6b5f24d3 1013 $this->delete_questions_usage_by_activities(new qubaid_list($previews));
d1b7e03d
TH
1014 }
1015
1016 /**
1017 * Update the flagged state of a question in the database.
16e246ac
TH
1018 *
1019 * You should call {@link question_engine::update_flag()()}
1020 * rather than calling this method directly.
1021 *
f7970e3c
TH
1022 * @param int $qubaid the question usage id.
1023 * @param int $questionid the question id.
1024 * @param int $sessionid the question_attempt id.
1025 * @param bool $newstate the new state of the flag. true = flagged.
d1b7e03d 1026 */
06f8ed54 1027 public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
f9b0500f 1028 if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
06f8ed54 1029 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
88f0eb15 1030 throw new moodle_exception('errorsavingflags', 'question');
d1b7e03d
TH
1031 }
1032
06f8ed54 1033 $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
d1b7e03d
TH
1034 }
1035
1036 /**
1037 * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
1038 * column to a summary state. Use this like
1039 * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
1040 * @param string SQL fragment.
1041 */
1042 protected function full_states_to_summary_state_sql() {
1043 $sql = '';
1044 foreach (question_state::get_all() as $state) {
f4fe3968 1045 $sql .= "WHEN '{$state}' THEN '{$state->get_summary_state()}'\n";
d1b7e03d
TH
1046 }
1047 return $sql;
1048 }
1049
1050 /**
1051 * Get the SQL needed to test that question_attempt_steps.state is in a
1052 * state corresponding to $summarystate.
16e246ac
TH
1053 *
1054 * This method may be called publicly.
1055 *
d1b7e03d
TH
1056 * @param string $summarystate one of
1057 * inprogress, needsgrading, manuallygraded or autograded
f7970e3c 1058 * @param bool $equal if false, do a NOT IN test. Default true.
d1b7e03d
TH
1059 * @return string SQL fragment.
1060 */
cf3b6568 1061 public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
d1b7e03d 1062 $states = question_state::get_all_for_summary_state($summarystate);
deef04a4
TH
1063 return $this->db->get_in_or_equal(array_map('strval', $states),
1064 SQL_PARAMS_NAMED, $prefix, $equal);
d1b7e03d
TH
1065 }
1066
1067 /**
1068 * Change the maxmark for the question_attempt with number in usage $slot
1069 * for all the specified question_attempts.
16e246ac
TH
1070 *
1071 * You should call {@link question_engine::set_max_mark_in_attempts()}
1072 * rather than calling this method directly.
1073 *
d1b7e03d 1074 * @param qubaid_condition $qubaids Selects which usages are updated.
f7970e3c 1075 * @param int $slot the number is usage to affect.
d1b7e03d
TH
1076 * @param number $newmaxmark the new max mark to set.
1077 */
1078 public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
c76145d3
TH
1079 $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
1080 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
1081 $qubaids->usage_id_in_params() + array('slot' => $slot));
d1b7e03d
TH
1082 }
1083
1084 /**
1085 * Return a subquery that computes the sum of the marks for all the questions
1086 * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
1087 * parameter.
1088 *
1089 * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
1090 * this method.
1091 *
16e246ac
TH
1092 * This method may be called publicly.
1093 *
d1b7e03d 1094 * @param string $qubaid SQL fragment that controls which usage is summed.
2daffca5
TH
1095 * This will normally be the name of a column in the outer query. Not that this
1096 * SQL fragment must not contain any placeholders.
d1b7e03d
TH
1097 * @return string SQL code for the subquery.
1098 */
1099 public function sum_usage_marks_subquery($qubaid) {
ad73a2cb
TH
1100 // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives
1101 // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that.
1102 // We always want to return a number, so the COALESCE is there to turn the
1103 // NULL total into a 0.
1104 return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0)
c76145d3 1105 FROM {question_attempts} qa
84b37108
TH
1106 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
1107 AND qas.sequencenumber = (
1108 SELECT MAX(summarks_qas.sequencenumber)
1109 FROM {question_attempt_steps} summarks_qas
1110 WHERE summarks_qas.questionattemptid = qa.id
d2c69d93 1111 )
d1b7e03d 1112 WHERE qa.questionusageid = $qubaid
9c197f44
TH
1113 HAVING COUNT(CASE
1114 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1
1115 ELSE NULL
1116 END) = 0";
d1b7e03d
TH
1117 }
1118
94c0ec21
TH
1119 /**
1120 * Get a subquery that returns the latest step of every qa in some qubas.
1121 * Currently, this is only used by the quiz reports. See
ac4d9157 1122 * {@link quiz_attempts_report_table::add_latest_state_join()}.
16e246ac
TH
1123 *
1124 * This method may be called publicly.
1125 *
94c0ec21
TH
1126 * @param string $alias alias to use for this inline-view.
1127 * @param qubaid_condition $qubaids restriction on which question_usages we
1128 * are interested in. This is important for performance.
1129 * @return array with two elements, the SQL fragment and any params requried.
1130 */
1131 public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) {
1132 return array("(
1133 SELECT {$alias}qa.id AS questionattemptid,
1134 {$alias}qa.questionusageid,
1135 {$alias}qa.slot,
1136 {$alias}qa.behaviour,
1137 {$alias}qa.questionid,
1138 {$alias}qa.variant,
1139 {$alias}qa.maxmark,
1140 {$alias}qa.minfraction,
4e3d8293 1141 {$alias}qa.maxfraction,
94c0ec21
TH
1142 {$alias}qa.flagged,
1143 {$alias}qa.questionsummary,
1144 {$alias}qa.rightanswer,
1145 {$alias}qa.responsesummary,
1146 {$alias}qa.timemodified,
1147 {$alias}qas.id AS attemptstepid,
1148 {$alias}qas.sequencenumber,
1149 {$alias}qas.state,
1150 {$alias}qas.fraction,
1151 {$alias}qas.timecreated,
1152 {$alias}qas.userid
1153
1154 FROM {$qubaids->from_question_attempts($alias . 'qa')}
84b37108
TH
1155 JOIN {question_attempt_steps} {$alias}qas ON {$alias}qas.questionattemptid = {$alias}qa.id
1156 AND {$alias}qas.sequencenumber = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
94c0ec21 1157 WHERE {$qubaids->where()}
f4fe3968 1158 ) {$alias}", $qubaids->from_where_params());
d1b7e03d
TH
1159 }
1160
1161 protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
d1b7e03d 1162 return "(
84b37108 1163 SELECT MAX(sequencenumber)
c76145d3 1164 FROM {question_attempt_steps}
d1b7e03d
TH
1165 WHERE questionattemptid = $questionattemptid
1166 )";
1167 }
1168
1169 /**
16e246ac
TH
1170 * Are any of these questions are currently in use?
1171 *
1172 * You should call {@link question_engine::questions_in_use()}
1173 * rather than calling this method directly.
1174 *
d1b7e03d 1175 * @param array $questionids of question ids.
07f88584 1176 * @param qubaid_condition $qubaids ids of the usages to consider.
16e246ac 1177 * @return bool whether any of these questions are being used by any of
07f88584 1178 * those usages.
d1b7e03d 1179 */
07f88584 1180 public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
c76145d3
TH
1181 list($test, $params) = $this->db->get_in_or_equal($questionids);
1182 return $this->db->record_exists_select('question_attempts',
07f88584
TH
1183 'questionid ' . $test . ' AND questionusageid ' .
1184 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
d1b7e03d
TH
1185 }
1186}
1187
6b5f24d3 1188
d1b7e03d
TH
1189/**
1190 * Implementation of the unit of work pattern for the question engine.
1191 *
1192 * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
1193 * changes to a {@link question_usage_by_activity}, and its constituent parts,
1194 * so that the changes can be saved to the database when {@link save()} is called.
1195 *
017bc1d9
TH
1196 * @copyright 2009 The Open University
1197 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1198 */
1199class question_engine_unit_of_work implements question_usage_observer {
1200 /** @var question_usage_by_activity the usage being tracked. */
1201 protected $quba;
1202
1203 /** @var boolean whether any of the fields of the usage have been changed. */
1204 protected $modified = false;
1205
1206 /**
94815ccf 1207 * @var array list of slot => {@link question_attempt}s that
d1b7e03d
TH
1208 * were already in the usage, and which have been modified.
1209 */
1210 protected $attemptsmodified = array();
1211
1212 /**
94815ccf 1213 * @var array list of slot => {@link question_attempt}s that
d1b7e03d
TH
1214 * have been added to the usage.
1215 */
1216 protected $attemptsadded = array();
1217
1218 /**
94815ccf
TH
1219 * @var array of array(question_attempt_step, question_attempt id, seq number)
1220 * of steps that have been added to question attempts in this usage.
d1b7e03d 1221 */
94815ccf 1222 protected $stepsadded = array();
d1b7e03d
TH
1223
1224 /**
94815ccf
TH
1225 * @var array of array(question_attempt_step, question_attempt id, seq number)
1226 * of steps that have been modified in their attempt.
d1b7e03d 1227 */
94815ccf
TH
1228 protected $stepsmodified = array();
1229
1230 /**
1231 * @var array list of question_attempt_step.id => question_attempt_step of steps
1232 * that were previously stored in the database, but which are no longer required.
1233 */
1234 protected $stepsdeleted = array();
d1b7e03d
TH
1235
1236 /**
1237 * Constructor.
1238 * @param question_usage_by_activity $quba the usage to track.
1239 */
1240 public function __construct(question_usage_by_activity $quba) {
1241 $this->quba = $quba;
1242 }
1243
1244 public function notify_modified() {
1245 $this->modified = true;
1246 }
1247
1248 public function notify_attempt_modified(question_attempt $qa) {
94815ccf
TH
1249 $slot = $qa->get_slot();
1250 if (!array_key_exists($slot, $this->attemptsadded)) {
1251 $this->attemptsmodified[$slot] = $qa;
d1b7e03d
TH
1252 }
1253 }
1254
1255 public function notify_attempt_added(question_attempt $qa) {
1256 $this->attemptsadded[$qa->get_slot()] = $qa;
1257 }
1258
94815ccf 1259 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
d1b7e03d
TH
1260 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1261 return;
1262 }
1263
94815ccf
TH
1264 if (($key = $this->is_step_added($step)) !== false) {
1265 return;
1266 }
1267
1268 if (($key = $this->is_step_modified($step)) !== false) {
1269 throw new coding_exception('Cannot add a step that has already been modified.');
1270 }
1271
1272 if (($key = $this->is_step_deleted($step)) !== false) {
1273 unset($this->stepsdeleted[$step->get_id()]);
1274 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1275 return;
1276 }
1277
1278 $stepid = $step->get_id();
1279 if ($stepid) {
1280 if (array_key_exists($stepid, $this->stepsdeleted)) {
1281 unset($this->stepsdeleted[$stepid]);
d1b7e03d 1282 }
94815ccf
TH
1283 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1284
1285 } else {
1286 $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
1287 }
1288 }
1289
1290 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
1291 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1292 return;
d1b7e03d
TH
1293 }
1294
94815ccf
TH
1295 if (($key = $this->is_step_added($step)) !== false) {
1296 return;
1297 }
1298
1299 if (($key = $this->is_step_deleted($step)) !== false) {
1300 throw new coding_exception('Cannot modify a step after it has been deleted.');
1301 }
1302
1303 $stepid = $step->get_id();
1304 if (empty($stepid)) {
1305 throw new coding_exception('Cannot modify a step that has never been stored in the database.');
1306 }
1307
1308 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
d1b7e03d
TH
1309 }
1310
94815ccf 1311 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
d1b7e03d
TH
1312 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1313 return;
1314 }
94815ccf
TH
1315
1316 if (($key = $this->is_step_added($step)) !== false) {
1317 unset($this->stepsadded[$key]);
1318 return;
1319 }
1320
1321 if (($key = $this->is_step_modified($step)) !== false) {
1322 unset($this->stepsmodified[$key]);
1323 }
1324
1325 $stepid = $step->get_id();
1326 if (empty($stepid)) {
1327 return; // Was never in the database.
1328 }
1329
1330 $this->stepsdeleted[$stepid] = $step;
1331 }
1332
1333 /**
1334 * @param question_attempt_step $step a step
1335 * @return int|false if the step is in the list of steps to be added, return
1336 * the key, otherwise return false.
1337 */
1338 protected function is_step_added(question_attempt_step $step) {
1339 foreach ($this->stepsadded as $key => $data) {
1340 list($addedstep, $qaid, $seq) = $data;
1341 if ($addedstep === $step) {
1342 return $key;
1343 }
1344 }
1345 return false;
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 modified, return
1351 * the key, otherwise return false.
1352 */
1353 protected function is_step_modified(question_attempt_step $step) {
1354 foreach ($this->stepsmodified as $key => $data) {
1355 list($modifiedstep, $qaid, $seq) = $data;
1356 if ($modifiedstep === $step) {
1357 return $key;
1358 }
1359 }
1360 return false;
1361 }
1362
1363 /**
1364 * @param question_attempt_step $step a step
1365 * @return bool whether the step is in the list of steps to be deleted.
1366 */
1367 protected function is_step_deleted(question_attempt_step $step) {
1368 foreach ($this->stepsdeleted as $deletedstep) {
1369 if ($deletedstep === $step) {
1370 return true;
1371 }
1372 }
1373 return false;
d1b7e03d
TH
1374 }
1375
1376 /**
1377 * Write all the changes we have recorded to the database.
1378 * @param question_engine_data_mapper $dm the mapper to use to update the database.
1379 */
1380 public function save(question_engine_data_mapper $dm) {
94815ccf
TH
1381 $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context());
1382
4040e2dd
TH
1383 // Initially an array of array of question_attempt_step_objects.
1384 // Built as a nested array for efficiency, then flattened.
1385 $stepdata = array();
1386
94815ccf
TH
1387 foreach ($this->stepsmodified as $stepinfo) {
1388 list($step, $questionattemptid, $seq) = $stepinfo;
4040e2dd
TH
1389 $stepdata[] = $dm->update_question_attempt_step(
1390 $step, $questionattemptid, $seq, $this->quba->get_owning_context());
94815ccf 1391 }
6b5f24d3 1392
d1b7e03d
TH
1393 foreach ($this->stepsadded as $stepinfo) {
1394 list($step, $questionattemptid, $seq) = $stepinfo;
4040e2dd
TH
1395 $stepdata[] = $dm->insert_question_attempt_step(
1396 $step, $questionattemptid, $seq, $this->quba->get_owning_context());
d1b7e03d 1397 }
6b5f24d3 1398
d1b7e03d 1399 foreach ($this->attemptsadded as $qa) {
4040e2dd
TH
1400 $stepdata[] = $dm->insert_question_attempt(
1401 $qa, $this->quba->get_owning_context());
d1b7e03d 1402 }
6b5f24d3 1403
d1b7e03d
TH
1404 foreach ($this->attemptsmodified as $qa) {
1405 $dm->update_question_attempt($qa);
1406 }
6b5f24d3 1407
d1b7e03d
TH
1408 if ($this->modified) {
1409 $dm->update_questions_usage_by_activity($this->quba);
1410 }
4040e2dd
TH
1411
1412 if (!$stepdata) {
1413 return;
1414 }
1415 $dm->insert_all_step_data(call_user_func_array('array_merge', $stepdata));
d1b7e03d
TH
1416 }
1417}
1418
1419
8a1e7b77
TH
1420/**
1421 * The interface implemented by {@link question_file_saver} and {@link question_file_loader}.
1422 *
1423 * @copyright 2012 The Open University
1424 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1425 */
1426interface question_response_files {
1427 /**
1428 * Get the files that were submitted.
1429 * @return array of stored_files objects.
1430 */
1431 public function get_files();
1432}
1433
1434
217f9a61
TH
1435/**
1436 * This class represents the promise to save some files from a particular draft
1437 * file area into a particular file area. It is used beause the necessary
1438 * information about what to save is to hand in the
1439 * {@link question_attempt::process_response_files()} method, but we don't know
1440 * if this question attempt will actually be saved in the database until later,
1441 * when the {@link question_engine_unit_of_work} is saved, if it is.
1442 *
1443 * @copyright 2011 The Open University
1444 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1445 */
8a1e7b77 1446class question_file_saver implements question_response_files {
217f9a61
TH
1447 /** @var int the id of the draft file area to save files from. */
1448 protected $draftitemid;
1449 /** @var string the owning component name. */
1450 protected $component;
1451 /** @var string the file area name. */
1452 protected $filearea;
1453
cd3557e6
TH
1454 /**
1455 * @var string the value to store in the question_attempt_step_data to
1456 * represent these files.
1457 */
1458 protected $value = null;
1459
217f9a61
TH
1460 /**
1461 * Constuctor.
1462 * @param int $draftitemid the draft area to save the files from.
1463 * @param string $component the component for the file area to save into.
1464 * @param string $filearea the name of the file area to save into.
1465 */
48d9c17d 1466 public function __construct($draftitemid, $component, $filearea, $text = null) {
217f9a61
TH
1467 $this->draftitemid = $draftitemid;
1468 $this->component = $component;
1469 $this->filearea = $filearea;
48d9c17d 1470 $this->value = $this->compute_value($draftitemid, $text);
217f9a61
TH
1471 }
1472
48d9c17d
TH
1473 /**
1474 * Compute the value that should be stored in the question_attempt_step_data
1475 * table. Contains a hash that (almost) uniquely encodes all the files.
1476 * @param int $draftitemid the draft file area itemid.
1477 * @param string $text optional content containing file links.
1478 */
1479 protected function compute_value($draftitemid, $text) {
cd3557e6
TH
1480 global $USER;
1481
cd3557e6 1482 $fs = get_file_storage();
21c08c63 1483 $usercontext = context_user::instance($USER->id);
cd3557e6
TH
1484
1485 $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
48d9c17d 1486 $draftitemid, 'sortorder, filepath, filename', false);
cd3557e6
TH
1487
1488 $string = '';
1489 foreach ($files as $file) {
1490 $string .= $file->get_filepath() . $file->get_filename() . '|' .
1491 $file->get_contenthash() . '|';
1492 }
d629327a 1493 $hash = md5($string);
cd3557e6 1494
48d9c17d 1495 if (is_null($text)) {
d629327a
TH
1496 if ($string) {
1497 return $hash;
1498 } else {
1499 return '';
1500 }
48d9c17d
TH
1501 }
1502
1503 // We add the file hash so a simple string comparison will say if the
1504 // files have been changed. First strip off any existing file hash.
d629327a
TH
1505 if ($text !== '') {
1506 $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
1507 $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
3fc9410f
TH
1508 if ($string) {
1509 $text .= '<!-- File hash: ' . $hash . ' -->';
1510 }
48d9c17d
TH
1511 }
1512 return $text;
cd3557e6
TH
1513 }
1514
217f9a61 1515 public function __toString() {
48d9c17d 1516 return $this->value;
217f9a61
TH
1517 }
1518
1519 /**
1520 * Actually save the files.
1521 * @param integer $itemid the item id for the file area to save into.
1522 */
1523 public function save_files($itemid, $context) {
1524 file_save_draft_area_files($this->draftitemid, $context->id,
1525 $this->component, $this->filearea, $itemid);
1526 }
8a1e7b77
TH
1527
1528 /**
1529 * Get the files that were submitted.
1530 * @return array of stored_files objects.
1531 */
1532 public function get_files() {
1533 global $USER;
1534
1535 $fs = get_file_storage();
1536 $usercontext = context_user::instance($USER->id);
1537
1538 return $fs->get_area_files($usercontext->id, 'user', 'draft',
1539 $this->draftitemid, 'sortorder, filepath, filename', false);
1540 }
1541}
1542
1543
1544/**
1545 * This class is the mirror image of {@link question_file_saver}. It allows
1546 * files to be accessed again later (e.g. when re-grading) using that same
1547 * API as when doing the original grading.
1548 *
1549 * @copyright 2012 The Open University
1550 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1551 */
1552class question_file_loader implements question_response_files {
1553 /** @var question_attempt_step the step that these files belong to. */
1554 protected $step;
1555
1556 /** @var string the field name for these files - which is used to construct the file area name. */
1557 protected $name;
1558
1559 /**
1560 * @var string the value to stored in the question_attempt_step_data to
1561 * represent these files.
1562 */
1563 protected $value;
1564
1565 /** @var int the context id that the files belong to. */
1566 protected $contextid;
1567
1568 /**
1569 * Constuctor.
1570 * @param question_attempt_step $step the step that these files belong to.
1571 * @param string $name string the field name for these files - which is used to construct the file area name.
1572 * @param string $value the value to stored in the question_attempt_step_data to
1573 * represent these files.
1574 * @param int $contextid the context id that the files belong to.
1575 */
1576 public function __construct(question_attempt_step $step, $name, $value, $contextid) {
d0782585
MPC
1577 $this->step = $step;
1578 $this->name = $name;
1579 $this->value = $value;
1580 $this->contextid = $contextid;
8a1e7b77
TH
1581 }
1582
1583 public function __toString() {
1584 return $this->value;
1585 }
1586
1587 /**
1588 * Get the files that were submitted.
1589 * @return array of stored_files objects.
1590 */
1591 public function get_files() {
1592 return $this->step->get_qt_files($this->name, $this->contextid);
1593 }
afb1b3d0
TH
1594
1595 /**
1596 * Copy these files into a draft area, and return the corresponding
1597 * {@link question_file_saver} that can save them again.
1598 *
1599 * This is used by {@link question_attempt::start_based_on()}, which is used
1600 * (for example) by the quizzes 'Each attempt builds on last' feature.
1601 *
1602 * @return question_file_saver that can re-save these files again.
1603 */
1604 public function get_question_file_saver() {
1605
d629327a
TH
1606 // There are three possibilities here for what $value will look like:
1607 // 1) some HTML content followed by an MD5 hash in a HTML comment;
1608 // 2) a plain MD5 hash;
1609 // 3) or some real content, without any hash.
1610 // The problem is that 3) is ambiguous in the case where a student writes
1611 // a response that looks exactly like an MD5 hash. For attempts made now,
1612 // we avoid case 3) by always going for case 1) or 2) (except when the
1613 // response is blank. However, there may be case 3) data in the database
1614 // so we need to handle it as best we can.
afb1b3d0
TH
1615 if (preg_match('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', $this->value)) {
1616 $value = preg_replace('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', '', $this->value);
1617
1618 } else if (preg_match('/^[0-9a-zA-Z]{32}$/', $this->value)) {
1619 $value = null;
1620
1621 } else {
d629327a 1622 $value = $this->value;
afb1b3d0
TH
1623 }
1624
1625 list($draftid, $text) = $this->step->prepare_response_files_draft_itemid_with_text(
1626 $this->name, $this->contextid, $value);
1627 return new question_file_saver($draftid, 'question', 'response_' . $this->name, $text);
1628 }
217f9a61
TH
1629}
1630
1631
d1b7e03d
TH
1632/**
1633 * This class represents a restriction on the set of question_usage ids to include
1634 * in a larger database query. Depending of the how you are going to restrict the
1635 * list of usages, construct an appropriate subclass.
1636 *
1637 * If $qubaids is an instance of this class, example usage might be
1638 *
1639 * SELECT qa.id, qa.maxmark
1640 * FROM $qubaids->from_question_attempts('qa')
1641 * WHERE $qubaids->where() AND qa.slot = 1
1642 *
f7970e3c 1643 * @copyright 2010 The Open University
017bc1d9 1644 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1645 */
1646abstract class qubaid_condition {
1647
1648 /**
1649 * @return string the SQL that needs to go in the FROM clause when trying
1650 * to select records from the 'question_attempts' table based on the
1651 * qubaid_condition.
1652 */
1653 public abstract function from_question_attempts($alias);
1654
1655 /** @return string the SQL that needs to go in the where clause. */
1656 public abstract function where();
1657
1658 /**
1659 * @return the params needed by a query that uses
1660 * {@link from_question_attempts()} and {@link where()}.
1661 */
1662 public abstract function from_where_params();
1663
1664 /**
1665 * @return string SQL that can use used in a WHERE qubaid IN (...) query.
1666 * This method returns the "IN (...)" part.
1667 */
1668 public abstract function usage_id_in();
1669
1670 /**
1671 * @return the params needed by a query that uses {@link usage_id_in()}.
1672 */
1673 public abstract function usage_id_in_params();
e68e4ccf
JP
1674
1675 /**
1676 * @return string 40-character hash code that uniquely identifies the combination of properties and class name of this qubaid
1677 * condition.
1678 */
1679 public function get_hash_code() {
1680 return sha1(serialize($this));
1681 }
d1b7e03d
TH
1682}
1683
1684
1685/**
1686 * This class represents a restriction on the set of question_usage ids to include
1687 * in a larger database query based on an explicit list of ids.
1688 *
f7970e3c 1689 * @copyright 2010 The Open University
017bc1d9 1690 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1691 */
1692class qubaid_list extends qubaid_condition {
1693 /** @var array of ids. */
1694 protected $qubaids;
1695 protected $columntotest = null;
1696 protected $params;
1697
1698 /**
1699 * Constructor.
1700 * @param array $qubaids of question usage ids.
1701 */
1702 public function __construct(array $qubaids) {
1703 $this->qubaids = $qubaids;
1704 }
1705
1706 public function from_question_attempts($alias) {
d1b7e03d 1707 $this->columntotest = $alias . '.questionusageid';
9b40c540 1708 return '{question_attempts} ' . $alias;
d1b7e03d
TH
1709 }
1710
1711 public function where() {
1712 global $DB;
1713
1714 if (is_null($this->columntotest)) {
88f0eb15 1715 throw new coding_exception('Must call from_question_attempts before where().');
d1b7e03d
TH
1716 }
1717 if (empty($this->qubaids)) {
9b40c540 1718 $this->params = array();
d1b7e03d
TH
1719 return '1 = 0';
1720 }
d1b7e03d 1721
9b40c540 1722 return $this->columntotest . ' ' . $this->usage_id_in();
d1b7e03d
TH
1723 }
1724
1725 public function from_where_params() {
1726 return $this->params;
1727 }
1728
1729 public function usage_id_in() {
1730 global $DB;
1731
1732 if (empty($this->qubaids)) {
5f79a9bc 1733 $this->params = array();
d1b7e03d
TH
1734 return '= 0';
1735 }
9c197f44
TH
1736 list($where, $this->params) = $DB->get_in_or_equal(
1737 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid');
d1b7e03d
TH
1738 return $where;
1739 }
1740
1741 public function usage_id_in_params() {
1742 return $this->params;
1743 }
1744}
1745
1746
1747/**
1748 * This class represents a restriction on the set of question_usage ids to include
1749 * in a larger database query based on JOINing to some other tables.
1750 *
1751 * The general form of the query is something like
1752 *
1753 * SELECT qa.id, qa.maxmark
1754 * FROM $from
2a3bdbf9 1755 * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
d1b7e03d
TH
1756 * WHERE $where AND qa.slot = 1
1757 *
1758 * where $from, $usageidcolumn and $where are the arguments to the constructor.
1759 *
f7970e3c 1760 * @copyright 2010 The Open University
017bc1d9 1761 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1762 */
1763class qubaid_join extends qubaid_condition {
1764 public $from;
1765 public $usageidcolumn;
1766 public $where;
1767 public $params;
1768
1769 /**
1770 * Constructor. The meaning of the arguments is explained in the class comment.
1771 * @param string $from SQL fragemnt to go in the FROM clause.
1772 * @param string $usageidcolumn the column in $from that should be
1773 * made equal to the usageid column in the JOIN clause.
1774 * @param string $where SQL fragment to go in the where clause.
e24ee794 1775 * @param array $params required by the SQL. You must use named parameters.
d1b7e03d
TH
1776 */
1777 public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
1778 $this->from = $from;
1779 $this->usageidcolumn = $usageidcolumn;
1780 $this->params = $params;
1781 if (empty($where)) {
1782 $where = '1 = 1';
1783 }
1784 $this->where = $where;
1785 }
1786
1787 public function from_question_attempts($alias) {
f4fe3968 1788 return "{$this->from}
0f33deaf 1789 JOIN {question_attempts} {$alias} ON " .
d1b7e03d
TH
1790 "{$alias}.questionusageid = $this->usageidcolumn";
1791 }
1792
1793 public function where() {
1794 return $this->where;
1795 }
1796
1797 public function from_where_params() {
1798 return $this->params;
1799 }
1800
1801 public function usage_id_in() {
f4fe3968 1802 return "IN (SELECT {$this->usageidcolumn} FROM {$this->from} WHERE {$this->where})";
d1b7e03d
TH
1803 }
1804
1805 public function usage_id_in_params() {
1806 return $this->params;
1807 }
1808}