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