weekly release 2.3dev (blame stronk7 for 0202 mistake)
[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 *
017bc1d9 20 * @package moodlecore
d1b7e03d 21 * @subpackage questionengine
017bc1d9
TH
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
d1b7e03d
TH
30/**
31 * This class controls the loading and saving of question engine data to and from
32 * the database.
33 *
017bc1d9
TH
34 * @copyright 2009 The Open University
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
36 */
37class question_engine_data_mapper {
06f8ed54
TH
38 /**
39 * @var moodle_database normally points to global $DB, but I prefer not to
40 * use globals if I can help it.
41 */
42 protected $db;
43
44 /**
45 * @param moodle_database $db a database connectoin. Defaults to global $DB.
46 */
47 public function __construct($db = null) {
48 if (is_null($db)) {
49 global $DB;
06f8ed54
TH
50 $this->db = $DB;
51 } else {
52 $this->db = $db;
53 }
54 }
55
d1b7e03d
TH
56 /**
57 * Store an entire {@link question_usage_by_activity} in the database,
58 * including all the question_attempts that comprise it.
59 * @param question_usage_by_activity $quba the usage to store.
60 */
61 public function insert_questions_usage_by_activity(question_usage_by_activity $quba) {
0ff4bd08 62 $record = new stdClass();
d1b7e03d 63 $record->contextid = $quba->get_owning_context()->id;
56b0df7e
TH
64 $record->component = $quba->get_owning_component();
65 $record->preferredbehaviour = $quba->get_preferred_behaviour();
d1b7e03d 66
06f8ed54 67 $newid = $this->db->insert_record('question_usages', $record);
d1b7e03d
TH
68 $quba->set_id_from_database($newid);
69
70 foreach ($quba->get_attempt_iterator() as $qa) {
217f9a61 71 $this->insert_question_attempt($qa, $quba->get_owning_context());
d1b7e03d
TH
72 }
73 }
74
75 /**
76 * Store an entire {@link question_attempt} in the database,
77 * including all the question_attempt_steps that comprise it.
78 * @param question_attempt $qa the question attempt to store.
217f9a61 79 * @param object $context the context of the owning question_usage_by_activity.
d1b7e03d 80 */
217f9a61 81 public function insert_question_attempt(question_attempt $qa, $context) {
0ff4bd08 82 $record = new stdClass();
d1b7e03d
TH
83 $record->questionusageid = $qa->get_usage_id();
84 $record->slot = $qa->get_slot();
56b0df7e 85 $record->behaviour = $qa->get_behaviour_name();
d1b7e03d 86 $record->questionid = $qa->get_question()->id;
1da821bb 87 $record->variant = $qa->get_variant();
d1b7e03d
TH
88 $record->maxmark = $qa->get_max_mark();
89 $record->minfraction = $qa->get_min_fraction();
90 $record->flagged = $qa->is_flagged();
56b0df7e 91 $record->questionsummary = $qa->get_question_summary();
c83ed025
TH
92 if (textlib::strlen($record->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
93 // It seems some people write very long quesions! MDL-30760
94 $record->questionsummary = textlib::substr($record->questionsummary,
95 0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
96 }
56b0df7e
TH
97 $record->rightanswer = $qa->get_right_answer_summary();
98 $record->responsesummary = $qa->get_response_summary();
d1b7e03d 99 $record->timemodified = time();
06f8ed54 100 $record->id = $this->db->insert_record('question_attempts', $record);
d1b7e03d
TH
101
102 foreach ($qa->get_step_iterator() as $seq => $step) {
217f9a61 103 $this->insert_question_attempt_step($step, $record->id, $seq, $context);
d1b7e03d
TH
104 }
105 }
106
107 /**
108 * Store a {@link question_attempt_step} in the database.
109 * @param question_attempt_step $qa the step to store.
217f9a61
TH
110 * @param int $questionattemptid the question attept id this step belongs to.
111 * @param int $seq the sequence number of this stop.
112 * @param object $context the context of the owning question_usage_by_activity.
d1b7e03d
TH
113 */
114 public function insert_question_attempt_step(question_attempt_step $step,
217f9a61 115 $questionattemptid, $seq, $context) {
0ff4bd08 116 $record = new stdClass();
d1b7e03d
TH
117 $record->questionattemptid = $questionattemptid;
118 $record->sequencenumber = $seq;
deef04a4 119 $record->state = (string) $step->get_state();
d1b7e03d
TH
120 $record->fraction = $step->get_fraction();
121 $record->timecreated = $step->get_timecreated();
122 $record->userid = $step->get_user_id();
123
06f8ed54 124 $record->id = $this->db->insert_record('question_attempt_steps', $record);
d1b7e03d
TH
125
126 foreach ($step->get_all_data() as $name => $value) {
48d9c17d
TH
127 if ($value instanceof question_file_saver) {
128 $value->save_files($record->id, $context);
129 }
130
0ff4bd08 131 $data = new stdClass();
d1b7e03d 132 $data->attemptstepid = $record->id;
56b0df7e
TH
133 $data->name = $name;
134 $data->value = $value;
06f8ed54 135 $this->db->insert_record('question_attempt_step_data', $data, false);
d1b7e03d
TH
136 }
137 }
138
139 /**
140 * Load a {@link question_attempt_step} from the database.
f7970e3c 141 * @param int $stepid the id of the step to load.
d1b7e03d
TH
142 * @param question_attempt_step the step that was loaded.
143 */
144 public function load_question_attempt_step($stepid) {
35d5f1c2 145 $records = $this->db->get_recordset_sql("
d1b7e03d 146SELECT
d1b7e03d
TH
147 qas.id AS attemptstepid,
148 qas.questionattemptid,
149 qas.sequencenumber,
150 qas.state,
151 qas.fraction,
152 qas.timecreated,
153 qas.userid,
154 qasd.name,
155 qasd.value
156
06f8ed54
TH
157FROM {question_attempt_steps} qas
158LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
159
160WHERE
06f8ed54
TH
161 qas.id = :stepid
162 ", array('stepid' => $stepid));
d1b7e03d 163
35d5f1c2 164 if (!$records->valid()) {
88f0eb15 165 throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
d1b7e03d
TH
166 }
167
35d5f1c2
TH
168 $step = question_attempt_step::load_from_records($records, $stepid);
169 $records->close();
170
171 return $step;
d1b7e03d
TH
172 }
173
174 /**
175 * Load a {@link question_attempt} from the database, including all its
176 * steps.
f7970e3c 177 * @param int $questionattemptid the id of the question attempt to load.
d1b7e03d
TH
178 * @param question_attempt the question attempt that was loaded.
179 */
180 public function load_question_attempt($questionattemptid) {
35d5f1c2 181 $records = $this->db->get_recordset_sql("
d1b7e03d 182SELECT
7a719748 183 quba.contextid,
d1b7e03d
TH
184 quba.preferredbehaviour,
185 qa.id AS questionattemptid,
186 qa.questionusageid,
187 qa.slot,
188 qa.behaviour,
189 qa.questionid,
1da821bb 190 qa.variant,
d1b7e03d
TH
191 qa.maxmark,
192 qa.minfraction,
193 qa.flagged,
194 qa.questionsummary,
195 qa.rightanswer,
196 qa.responsesummary,
197 qa.timemodified,
198 qas.id AS attemptstepid,
199 qas.sequencenumber,
200 qas.state,
201 qas.fraction,
202 qas.timecreated,
203 qas.userid,
204 qasd.name,
205 qasd.value
206
06f8ed54
TH
207FROM {question_attempts qa
208JOIN {question_usages} quba ON quba.id = qa.questionusageid
209LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
210LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
211
212WHERE
06f8ed54 213 qa.id = :questionattemptid
d1b7e03d
TH
214
215ORDER BY
216 qas.sequencenumber
06f8ed54 217 ", array('questionattemptid' => $questionattemptid));
d1b7e03d 218
35d5f1c2 219 if (!$records->valid()) {
88f0eb15 220 throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
d1b7e03d
TH
221 }
222
223 $record = current($records);
35d5f1c2 224 $qa = question_attempt::load_from_records($records, $questionattemptid,
d1b7e03d 225 new question_usage_null_observer(), $record->preferredbehaviour);
35d5f1c2
TH
226 $records->close();
227
228 return $qa;
d1b7e03d
TH
229 }
230
231 /**
232 * Load a {@link question_usage_by_activity} from the database, including
233 * all its {@link question_attempt}s and all their steps.
f7970e3c 234 * @param int $qubaid the id of the usage to load.
d1b7e03d
TH
235 * @param question_usage_by_activity the usage that was loaded.
236 */
237 public function load_questions_usage_by_activity($qubaid) {
35d5f1c2 238 $records = $this->db->get_recordset_sql("
d1b7e03d 239SELECT
d1b7e03d
TH
240 quba.id AS qubaid,
241 quba.contextid,
242 quba.component,
243 quba.preferredbehaviour,
244 qa.id AS questionattemptid,
245 qa.questionusageid,
246 qa.slot,
247 qa.behaviour,
248 qa.questionid,
1da821bb 249 qa.variant,
d1b7e03d
TH
250 qa.maxmark,
251 qa.minfraction,
252 qa.flagged,
253 qa.questionsummary,
254 qa.rightanswer,
255 qa.responsesummary,
256 qa.timemodified,
257 qas.id AS attemptstepid,
258 qas.sequencenumber,
259 qas.state,
260 qas.fraction,
261 qas.timecreated,
262 qas.userid,
263 qasd.name,
264 qasd.value
265
06f8ed54
TH
266FROM {question_usages} quba
267LEFT JOIN {question_attempts} qa ON qa.questionusageid = quba.id
268LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
269LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
270
271WHERE
06f8ed54 272 quba.id = :qubaid
d1b7e03d
TH
273
274ORDER BY
275 qa.slot,
276 qas.sequencenumber
c76145d3 277 ", array('qubaid' => $qubaid));
d1b7e03d 278
35d5f1c2 279 if (!$records->valid()) {
88f0eb15 280 throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
d1b7e03d
TH
281 }
282
35d5f1c2
TH
283 $quba = question_usage_by_activity::load_from_records($records, $qubaid);
284 $records->close();
285
286 return $quba;
d1b7e03d
TH
287 }
288
289 /**
290 * Load information about the latest state of each question from the database.
291 *
292 * @param qubaid_condition $qubaids used to restrict which usages are included
293 * in the query. See {@link qubaid_condition}.
294 * @param array $slots A list of slots for the questions you want to konw about.
295 * @return array of records. See the SQL in this function to see the fields available.
296 */
297 public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
a2ac2349 298 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
d1b7e03d 299
9b40c540 300 $records = $this->db->get_records_sql("
d1b7e03d
TH
301SELECT
302 qas.id,
303 qa.id AS questionattemptid,
304 qa.questionusageid,
305 qa.slot,
306 qa.behaviour,
307 qa.questionid,
1da821bb 308 qa.variant,
d1b7e03d
TH
309 qa.maxmark,
310 qa.minfraction,
311 qa.flagged,
312 qa.questionsummary,
313 qa.rightanswer,
314 qa.responsesummary,
315 qa.timemodified,
316 qas.id AS attemptstepid,
317 qas.sequencenumber,
318 qas.state,
319 qas.fraction,
320 qas.timecreated,
321 qas.userid
322
323FROM {$qubaids->from_question_attempts('qa')}
06f8ed54 324JOIN {question_attempt_steps} qas ON
d1b7e03d
TH
325 qas.id = {$this->latest_step_for_qa_subquery()}
326
327WHERE
328 {$qubaids->where()} AND
329 qa.slot $slottest
06f8ed54 330 ", $params + $qubaids->from_where_params());
d1b7e03d
TH
331
332 return $records;
333 }
334
335 /**
5e8a85aa
TH
336 * Load summary information about the state of each question in a group of
337 * attempts. This is used, for example, by the quiz manual grading report,
338 * to show how many attempts at each question need to be graded.
d1b7e03d
TH
339 *
340 * @param qubaid_condition $qubaids used to restrict which usages are included
341 * in the query. See {@link qubaid_condition}.
342 * @param array $slots A list of slots for the questions you want to konw about.
343 * @return array The array keys are slot,qestionid. The values are objects with
344 * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
345 * $manuallygraded and $all.
346 */
9c197f44
TH
347 public function load_questions_usages_question_state_summary(
348 qubaid_condition $qubaids, $slots) {
a2ac2349 349 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
d1b7e03d 350
06f8ed54 351 $rs = $this->db->get_recordset_sql("
d1b7e03d
TH
352SELECT
353 qa.slot,
354 qa.questionid,
355 q.name,
356 CASE qas.state
357 {$this->full_states_to_summary_state_sql()}
358 END AS summarystate,
359 COUNT(1) AS numattempts
360
361FROM {$qubaids->from_question_attempts('qa')}
06f8ed54 362JOIN {question_attempt_steps} qas ON
d1b7e03d 363 qas.id = {$this->latest_step_for_qa_subquery()}
06f8ed54 364JOIN {question} q ON q.id = qa.questionid
d1b7e03d
TH
365
366WHERE
367 {$qubaids->where()} AND
368 qa.slot $slottest
369
370GROUP BY
371 qa.slot,
372 qa.questionid,
373 q.name,
374 q.id,
304f0d85
TH
375 CASE qas.state
376 {$this->full_states_to_summary_state_sql()}
377 END
d1b7e03d 378
f9b0500f 379ORDER BY
d1b7e03d
TH
380 qa.slot,
381 qa.questionid,
382 q.name,
383 q.id
06f8ed54 384 ", $params + $qubaids->from_where_params());
d1b7e03d 385
d1b7e03d 386 $results = array();
cf3b6568 387 foreach ($rs as $row) {
d1b7e03d
TH
388 $index = $row->slot . ',' . $row->questionid;
389
390 if (!array_key_exists($index, $results)) {
0ff4bd08 391 $res = new stdClass();
d1b7e03d
TH
392 $res->slot = $row->slot;
393 $res->questionid = $row->questionid;
394 $res->name = $row->name;
395 $res->inprogress = 0;
396 $res->needsgrading = 0;
397 $res->autograded = 0;
398 $res->manuallygraded = 0;
399 $res->all = 0;
400 $results[$index] = $res;
401 }
402
403 $results[$index]->{$row->summarystate} = $row->numattempts;
404 $results[$index]->all += $row->numattempts;
405 }
cf3b6568 406 $rs->close();
d1b7e03d
TH
407
408 return $results;
409 }
410
411 /**
412 * Get a list of usage ids where the question with slot $slot, and optionally
413 * also with question id $questionid, is in summary state $summarystate. Also
414 * return the total count of such states.
415 *
416 * Only a subset of the ids can be returned by using $orderby, $limitfrom and
417 * $limitnum. A special value 'random' can be passed as $orderby, in which case
418 * $limitfrom is ignored.
419 *
420 * @param qubaid_condition $qubaids used to restrict which usages are included
421 * in the query. See {@link qubaid_condition}.
f7970e3c
TH
422 * @param int $slot The slot for the questions you want to konw about.
423 * @param int $questionid (optional) Only return attempts that were of this specific question.
d1b7e03d
TH
424 * @param string $summarystate the summary state of interest, or 'all'.
425 * @param string $orderby the column to order by.
cf3b6568 426 * @param array $params any params required by any of the SQL fragments.
f7970e3c 427 * @param int $limitfrom implements paging of the results.
d1b7e03d 428 * Ignored if $orderby = random or $limitnum is null.
f7970e3c 429 * @param int $limitnum implements paging of the results. null = all.
d1b7e03d
TH
430 * @return array with two elements, an array of usage ids, and a count of the total number.
431 */
432 public function load_questions_usages_where_question_in_state(
433 qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
cf3b6568 434 $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) {
d1b7e03d
TH
435
436 $extrawhere = '';
437 if ($questionid) {
cf3b6568
TH
438 $extrawhere .= ' AND qa.questionid = :questionid';
439 $params['questionid'] = $questionid;
d1b7e03d
TH
440 }
441 if ($summarystate != 'all') {
cf3b6568 442 list($test, $sparams) = $this->in_summary_state_test($summarystate);
d1b7e03d 443 $extrawhere .= ' AND qas.state ' . $test;
cf3b6568 444 $params += $sparams;
d1b7e03d
TH
445 }
446
447 if ($orderby == 'random') {
448 $sqlorderby = '';
449 } else if ($orderby) {
450 $sqlorderby = 'ORDER BY ' . $orderby;
451 } else {
452 $sqlorderby = '';
453 }
454
455 // We always want the total count, as well as the partcular list of ids,
456 // based on the paging and sort order. Becuase the list of ids is never
457 // going to be too rediculously long. My worst-case scenario is
458 // 10,000 students in the coures, each doing 5 quiz attempts. That
459 // is a 50,000 element int => int array, which PHP seems to use 5MB
460 // memeory to store on a 64 bit server.
cf3b6568
TH
461 $params += $qubaids->from_where_params();
462 $params['slot'] = $slot;
463 $qubaids = $this->db->get_records_sql_menu("
d1b7e03d
TH
464SELECT
465 qa.questionusageid,
466 1
467
468FROM {$qubaids->from_question_attempts('qa')}
cf3b6568 469JOIN {question_attempt_steps} qas ON
d1b7e03d 470 qas.id = {$this->latest_step_for_qa_subquery()}
cf3b6568 471JOIN {question} q ON q.id = qa.questionid
d1b7e03d
TH
472
473WHERE
474 {$qubaids->where()} AND
cf3b6568 475 qa.slot = :slot
d1b7e03d
TH
476 $extrawhere
477
478$sqlorderby
cf3b6568 479 ", $params);
d1b7e03d
TH
480
481 $qubaids = array_keys($qubaids);
482 $count = count($qubaids);
483
484 if ($orderby == 'random') {
485 shuffle($qubaids);
486 $limitfrom = 0;
487 }
488
489 if (!is_null($limitnum)) {
490 $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
491 }
492
493 return array($qubaids, $count);
494 }
495
496 /**
497 * Load a {@link question_usage_by_activity} from the database, including
498 * all its {@link question_attempt}s and all their steps.
499 * @param qubaid_condition $qubaids used to restrict which usages are included
500 * in the query. See {@link qubaid_condition}.
501 * @param array $slots if null, load info for all quesitions, otherwise only
502 * load the averages for the specified questions.
503 */
504 public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
d1b7e03d 505 if (!empty($slots)) {
9c197f44
TH
506 list($slottest, $slotsparams) = $this->db->get_in_or_equal(
507 $slots, SQL_PARAMS_NAMED, 'slot');
d1b7e03d
TH
508 $slotwhere = " AND qa.slot $slottest";
509 } else {
510 $slotwhere = '';
e24ee794 511 $params = array();
d1b7e03d
TH
512 }
513
e24ee794 514 list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
deef04a4
TH
515 (string) question_state::$gaveup,
516 (string) question_state::$gradedwrong,
517 (string) question_state::$gradedpartial,
518 (string) question_state::$gradedright,
519 (string) question_state::$mangaveup,
520 (string) question_state::$mangrwrong,
521 (string) question_state::$mangrpartial,
522 (string) question_state::$mangrright), SQL_PARAMS_NAMED, 'st');
d1b7e03d 523
e24ee794 524 return $this->db->get_records_sql("
d1b7e03d
TH
525SELECT
526 qa.slot,
527 AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
528 COUNT(1) AS numaveraged
529
530FROM {$qubaids->from_question_attempts('qa')}
2a3bdbf9 531JOIN {question_attempt_steps} qas ON
d1b7e03d
TH
532 qas.id = {$this->latest_step_for_qa_subquery()}
533
534WHERE
535 {$qubaids->where()}
536 $slotwhere
537 AND qas.state $statetest
538
539GROUP BY qa.slot
540
541ORDER BY qa.slot
e24ee794 542 ", $slotsparams + $stateparams + $qubaids->from_where_params());
d1b7e03d
TH
543 }
544
545 /**
546 * Load a {@link question_attempt} from the database, including all its
547 * steps.
f7970e3c 548 * @param int $questionid the question to load all the attempts fors.
d1b7e03d
TH
549 * @param qubaid_condition $qubaids used to restrict which usages are included
550 * in the query. See {@link qubaid_condition}.
551 * @return array of question_attempts.
552 */
553 public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
9b40c540 554 global $DB;
04853f27
TH
555
556 $params = $qubaids->from_where_params();
557 $params['questionid'] = $questionid;
558
35d5f1c2 559 $records = $DB->get_recordset_sql("
d1b7e03d 560SELECT
04853f27 561 quba.contextid,
d1b7e03d
TH
562 quba.preferredbehaviour,
563 qa.id AS questionattemptid,
564 qa.questionusageid,
565 qa.slot,
566 qa.behaviour,
567 qa.questionid,
1da821bb 568 qa.variant,
d1b7e03d
TH
569 qa.maxmark,
570 qa.minfraction,
571 qa.flagged,
572 qa.questionsummary,
573 qa.rightanswer,
574 qa.responsesummary,
575 qa.timemodified,
576 qas.id AS attemptstepid,
577 qas.sequencenumber,
578 qas.state,
579 qas.fraction,
580 qas.timecreated,
581 qas.userid,
582 qasd.name,
583 qasd.value
584
585FROM {$qubaids->from_question_attempts('qa')}
04853f27
TH
586JOIN {question_usages} quba ON quba.id = qa.questionusageid
587LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
588LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
d1b7e03d
TH
589
590WHERE
591 {$qubaids->where()} AND
04853f27 592 qa.questionid = :questionid
d1b7e03d
TH
593
594ORDER BY
595 quba.id,
596 qa.id,
597 qas.sequencenumber
04853f27 598 ", $params);
d1b7e03d 599
d1b7e03d 600 $questionattempts = array();
35d5f1c2
TH
601 while ($records->valid()) {
602 $record = $records->current();
d1b7e03d
TH
603 $questionattempts[$record->questionattemptid] =
604 question_attempt::load_from_records($records,
605 $record->questionattemptid, new question_usage_null_observer(),
606 $record->preferredbehaviour);
d1b7e03d 607 }
35d5f1c2
TH
608 $records->close();
609
d1b7e03d
TH
610 return $questionattempts;
611 }
612
613 /**
614 * Update a question_usages row to refect any changes in a usage (but not
615 * any of its question_attempts.
616 * @param question_usage_by_activity $quba the usage that has changed.
617 */
618 public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
0ff4bd08 619 $record = new stdClass();
d1b7e03d
TH
620 $record->id = $quba->get_id();
621 $record->contextid = $quba->get_owning_context()->id;
56b0df7e
TH
622 $record->component = $quba->get_owning_component();
623 $record->preferredbehaviour = $quba->get_preferred_behaviour();
d1b7e03d 624
88f0eb15 625 $this->db->update_record('question_usages', $record);
d1b7e03d
TH
626 }
627
628 /**
629 * Update a question_attempts row to refect any changes in a question_attempt
630 * (but not any of its steps).
631 * @param question_attempt $qa the question attempt that has changed.
632 */
633 public function update_question_attempt(question_attempt $qa) {
0ff4bd08 634 $record = new stdClass();
d1b7e03d
TH
635 $record->id = $qa->get_database_id();
636 $record->maxmark = $qa->get_max_mark();
637 $record->minfraction = $qa->get_min_fraction();
638 $record->flagged = $qa->is_flagged();
56b0df7e
TH
639 $record->questionsummary = $qa->get_question_summary();
640 $record->rightanswer = $qa->get_right_answer_summary();
641 $record->responsesummary = $qa->get_response_summary();
d1b7e03d
TH
642 $record->timemodified = time();
643
88f0eb15 644 $this->db->update_record('question_attempts', $record);
d1b7e03d
TH
645 }
646
647 /**
648 * Delete a question_usage_by_activity and all its associated
649 * {@link question_attempts} and {@link question_attempt_steps} from the
650 * database.
6b5f24d3 651 * @param qubaid_condition $qubaids identifies which question useages to delete.
d1b7e03d 652 */
6b5f24d3
TH
653 public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
654 $where = "qa.questionusageid {$qubaids->usage_id_in()}";
655 $params = $qubaids->usage_id_in_params();
656
657 $contextids = $this->db->get_records_sql_menu("
658 SELECT DISTINCT contextid, 1
659 FROM {question_usages}
a2ac2349 660 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
6b5f24d3
TH
661 foreach ($contextids as $contextid => $notused) {
662 $this->delete_response_files($contextid, "IN (
663 SELECT qas.id
664 FROM {question_attempts} qa
665 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
666 WHERE $where)", $params);
667 }
668
f0bfd964
TH
669 if ($this->db->get_dbfamily() == 'mysql') {
670 $this->delete_usage_records_for_mysql($qubaids);
671 return;
672 }
673
c76145d3 674 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
d1b7e03d 675 SELECT qas.id
c76145d3
TH
676 FROM {question_attempts} qa
677 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
c76145d3 678 WHERE $where)", $params);
6b5f24d3 679
c76145d3 680 $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
d1b7e03d 681 SELECT qa.id
c76145d3 682 FROM {question_attempts} qa
c76145d3 683 WHERE $where)", $params);
6b5f24d3
TH
684
685 $this->db->delete_records_select('question_attempts',
a2ac2349
TH
686 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
687 $qubaids->usage_id_in_params());
6b5f24d3
TH
688
689 $this->db->delete_records_select('question_usages',
a2ac2349 690 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
d1b7e03d
TH
691 }
692
f0bfd964
TH
693 /**
694 * This function is a work-around for poor MySQL performance with
695 * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
696 * syntax to get good performance. See MDL-29520.
697 * @param qubaid_condition $qubaids identifies which question useages to delete.
698 */
699 protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
700 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
701 $this->db->execute('
702 DELETE qu, qa, qas, qasd
703 FROM {question_usages} qu
704 JOIN {question_attempts} qa ON qa.questionusageid = qu.id
033d6566
EL
705 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
706 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
f0bfd964
TH
707 WHERE qu.id ' . $qubaids->usage_id_in(),
708 $qubaids->usage_id_in_params());
709 }
710
711 /**
712 * This function is a work-around for poor MySQL performance with
713 * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
714 * syntax to get good performance. See MDL-29520.
715 * @param string $test sql fragment.
716 * @param array $params used by $test.
717 */
718 protected function delete_attempt_steps_for_mysql($test, $params) {
719 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
720 $this->db->execute('
721 DELETE qas, qasd
722 FROM {question_attempt_steps} qas
033d6566 723 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
f0bfd964
TH
724 WHERE qas.questionattemptid ' . $test, $params);
725 }
726
d1b7e03d
TH
727 /**
728 * Delete all the steps for a question attempt.
f7970e3c 729 * @param int $qaids question_attempt id.
d1b7e03d 730 */
6b5f24d3 731 public function delete_steps_for_question_attempts($qaids, $context) {
d1b7e03d
TH
732 if (empty($qaids)) {
733 return;
734 }
6b5f24d3
TH
735 list($test, $params) = $this->db->get_in_or_equal($qaids, SQL_PARAMS_NAMED);
736
737 $this->delete_response_files($context->id, "IN (
738 SELECT id
3b049b76 739 FROM {question_attempt_steps}
6b5f24d3
TH
740 WHERE questionattemptid $test)", $params);
741
f0bfd964
TH
742 if ($this->db->get_dbfamily() == 'mysql') {
743 $this->delete_attempt_steps_for_mysql($test, $params);
744 return;
745 }
746
c76145d3 747 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
d1b7e03d 748 SELECT qas.id
c76145d3
TH
749 FROM {question_attempt_steps} qas
750 WHERE questionattemptid $test)", $params);
9c197f44
TH
751 $this->db->delete_records_select('question_attempt_steps',
752 'questionattemptid ' . $test, $params);
d1b7e03d
TH
753 }
754
6b5f24d3
TH
755 /**
756 * Delete all the files belonging to the response variables in the gives
757 * question attempt steps.
758 * @param int $contextid the context these attempts belong to.
759 * @param string $itemidstest a bit of SQL that can be used in a
760 * WHERE itemid $itemidstest clause. Must use named params.
761 * @param array $params any query parameters used in $itemidstest.
762 */
763 protected function delete_response_files($contextid, $itemidstest, $params) {
764 $fs = get_file_storage();
c749527b 765 foreach (question_engine::get_all_response_file_areas() as $filearea) {
6b5f24d3
TH
766 $fs->delete_area_files_select($contextid, 'question', $filearea,
767 $itemidstest, $params);
768 }
769 }
770
d1b7e03d
TH
771 /**
772 * Delete all the previews for a given question.
f7970e3c 773 * @param int $questionid question id.
d1b7e03d
TH
774 */
775 public function delete_previews($questionid) {
c76145d3 776 $previews = $this->db->get_records_sql_menu("
d1b7e03d 777 SELECT DISTINCT quba.id, 1
c76145d3
TH
778 FROM {question_usages} quba
779 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
d1b7e03d 780 WHERE quba.component = 'core_question_preview' AND
c76145d3 781 qa.questionid = ?", array($questionid));
d1b7e03d
TH
782 if (empty($previews)) {
783 return;
784 }
6b5f24d3 785 $this->delete_questions_usage_by_activities(new qubaid_list($previews));
d1b7e03d
TH
786 }
787
788 /**
789 * Update the flagged state of a question in the database.
f7970e3c
TH
790 * @param int $qubaid the question usage id.
791 * @param int $questionid the question id.
792 * @param int $sessionid the question_attempt id.
793 * @param bool $newstate the new state of the flag. true = flagged.
d1b7e03d 794 */
06f8ed54 795 public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
f9b0500f 796 if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
06f8ed54 797 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
88f0eb15 798 throw new moodle_exception('errorsavingflags', 'question');
d1b7e03d
TH
799 }
800
06f8ed54 801 $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
d1b7e03d
TH
802 }
803
804 /**
805 * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
806 * column to a summary state. Use this like
807 * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
808 * @param string SQL fragment.
809 */
810 protected function full_states_to_summary_state_sql() {
811 $sql = '';
812 foreach (question_state::get_all() as $state) {
813 $sql .= "WHEN '$state' THEN '{$state->get_summary_state()}'\n";
814 }
815 return $sql;
816 }
817
818 /**
819 * Get the SQL needed to test that question_attempt_steps.state is in a
820 * state corresponding to $summarystate.
821 * @param string $summarystate one of
822 * inprogress, needsgrading, manuallygraded or autograded
f7970e3c 823 * @param bool $equal if false, do a NOT IN test. Default true.
d1b7e03d
TH
824 * @return string SQL fragment.
825 */
cf3b6568 826 public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
d1b7e03d 827 $states = question_state::get_all_for_summary_state($summarystate);
deef04a4
TH
828 return $this->db->get_in_or_equal(array_map('strval', $states),
829 SQL_PARAMS_NAMED, $prefix, $equal);
d1b7e03d
TH
830 }
831
832 /**
833 * Change the maxmark for the question_attempt with number in usage $slot
834 * for all the specified question_attempts.
835 * @param qubaid_condition $qubaids Selects which usages are updated.
f7970e3c 836 * @param int $slot the number is usage to affect.
d1b7e03d
TH
837 * @param number $newmaxmark the new max mark to set.
838 */
839 public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
c76145d3
TH
840 $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
841 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
842 $qubaids->usage_id_in_params() + array('slot' => $slot));
d1b7e03d
TH
843 }
844
845 /**
846 * Return a subquery that computes the sum of the marks for all the questions
847 * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
848 * parameter.
849 *
850 * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
851 * this method.
852 *
853 * @param string $qubaid SQL fragment that controls which usage is summed.
2daffca5
TH
854 * This will normally be the name of a column in the outer query. Not that this
855 * SQL fragment must not contain any placeholders.
d1b7e03d
TH
856 * @return string SQL code for the subquery.
857 */
858 public function sum_usage_marks_subquery($qubaid) {
ad73a2cb
TH
859 // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives
860 // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that.
861 // We always want to return a number, so the COALESCE is there to turn the
862 // NULL total into a 0.
863 return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0)
c76145d3 864 FROM {question_attempts} qa
d2c69d93
TH
865 JOIN {question_attempt_steps} qas ON qas.id = (
866 SELECT MAX(summarks_qas.id)
867 FROM {question_attempt_steps} summarks_qas
868 WHERE summarks_qas.questionattemptid = qa.id
869 )
d1b7e03d 870 WHERE qa.questionusageid = $qubaid
9c197f44
TH
871 HAVING COUNT(CASE
872 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1
873 ELSE NULL
874 END) = 0";
d1b7e03d
TH
875 }
876
94c0ec21
TH
877 /**
878 * Get a subquery that returns the latest step of every qa in some qubas.
879 * Currently, this is only used by the quiz reports. See
880 * {@link quiz_attempt_report_table::add_latest_state_join()}.
881 * @param string $alias alias to use for this inline-view.
882 * @param qubaid_condition $qubaids restriction on which question_usages we
883 * are interested in. This is important for performance.
884 * @return array with two elements, the SQL fragment and any params requried.
885 */
886 public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) {
887 return array("(
888 SELECT {$alias}qa.id AS questionattemptid,
889 {$alias}qa.questionusageid,
890 {$alias}qa.slot,
891 {$alias}qa.behaviour,
892 {$alias}qa.questionid,
893 {$alias}qa.variant,
894 {$alias}qa.maxmark,
895 {$alias}qa.minfraction,
896 {$alias}qa.flagged,
897 {$alias}qa.questionsummary,
898 {$alias}qa.rightanswer,
899 {$alias}qa.responsesummary,
900 {$alias}qa.timemodified,
901 {$alias}qas.id AS attemptstepid,
902 {$alias}qas.sequencenumber,
903 {$alias}qas.state,
904 {$alias}qas.fraction,
905 {$alias}qas.timecreated,
906 {$alias}qas.userid
907
908 FROM {$qubaids->from_question_attempts($alias . 'qa')}
909 JOIN {question_attempt_steps} {$alias}qas ON
910 {$alias}qas.id = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
911 WHERE {$qubaids->where()}
912 ) $alias", $qubaids->from_where_params());
d1b7e03d
TH
913 }
914
915 protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
d1b7e03d
TH
916 return "(
917 SELECT MAX(id)
c76145d3 918 FROM {question_attempt_steps}
d1b7e03d
TH
919 WHERE questionattemptid = $questionattemptid
920 )";
921 }
922
923 /**
924 * @param array $questionids of question ids.
07f88584
TH
925 * @param qubaid_condition $qubaids ids of the usages to consider.
926 * @return boolean whether any of these questions are being used by any of
927 * those usages.
d1b7e03d 928 */
07f88584 929 public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
c76145d3
TH
930 list($test, $params) = $this->db->get_in_or_equal($questionids);
931 return $this->db->record_exists_select('question_attempts',
07f88584
TH
932 'questionid ' . $test . ' AND questionusageid ' .
933 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
d1b7e03d
TH
934 }
935}
936
6b5f24d3 937
d1b7e03d
TH
938/**
939 * Implementation of the unit of work pattern for the question engine.
940 *
941 * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
942 * changes to a {@link question_usage_by_activity}, and its constituent parts,
943 * so that the changes can be saved to the database when {@link save()} is called.
944 *
017bc1d9
TH
945 * @copyright 2009 The Open University
946 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
947 */
948class question_engine_unit_of_work implements question_usage_observer {
949 /** @var question_usage_by_activity the usage being tracked. */
950 protected $quba;
951
952 /** @var boolean whether any of the fields of the usage have been changed. */
953 protected $modified = false;
954
955 /**
956 * @var array list of number in usage => {@link question_attempt}s that
957 * were already in the usage, and which have been modified.
958 */
959 protected $attemptsmodified = array();
960
961 /**
962 * @var array list of number in usage => {@link question_attempt}s that
963 * have been added to the usage.
964 */
965 protected $attemptsadded = array();
966
967 /**
968 * @var array list of question attempt ids to delete the steps for, before
969 * inserting new steps.
970 */
971 protected $attemptstodeletestepsfor = array();
972
973 /**
974 * @var array list of array(question_attempt_step, question_attempt id, seq number)
975 * of steps that have been added to question attempts in this usage.
976 */
977 protected $stepsadded = array();
978
979 /**
980 * Constructor.
981 * @param question_usage_by_activity $quba the usage to track.
982 */
983 public function __construct(question_usage_by_activity $quba) {
984 $this->quba = $quba;
985 }
986
987 public function notify_modified() {
988 $this->modified = true;
989 }
990
991 public function notify_attempt_modified(question_attempt $qa) {
992 $no = $qa->get_slot();
993 if (!array_key_exists($no, $this->attemptsadded)) {
994 $this->attemptsmodified[$no] = $qa;
995 }
996 }
997
998 public function notify_attempt_added(question_attempt $qa) {
999 $this->attemptsadded[$qa->get_slot()] = $qa;
1000 }
1001
1002 public function notify_delete_attempt_steps(question_attempt $qa) {
1003
1004 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1005 return;
1006 }
1007
1008 $qaid = $qa->get_database_id();
1009 foreach ($this->stepsadded as $key => $stepinfo) {
1010 if ($stepinfo[1] == $qaid) {
1011 unset($this->stepsadded[$key]);
1012 }
1013 }
1014
1015 $this->attemptstodeletestepsfor[$qaid] = 1;
1016 }
1017
1018 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
1019 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1020 return;
1021 }
1022 $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
1023 }
1024
1025 /**
1026 * Write all the changes we have recorded to the database.
1027 * @param question_engine_data_mapper $dm the mapper to use to update the database.
1028 */
1029 public function save(question_engine_data_mapper $dm) {
6b5f24d3
TH
1030 $dm->delete_steps_for_question_attempts(array_keys($this->attemptstodeletestepsfor),
1031 $this->quba->get_owning_context());
1032
d1b7e03d
TH
1033 foreach ($this->stepsadded as $stepinfo) {
1034 list($step, $questionattemptid, $seq) = $stepinfo;
217f9a61
TH
1035 $dm->insert_question_attempt_step($step, $questionattemptid, $seq,
1036 $this->quba->get_owning_context());
d1b7e03d 1037 }
6b5f24d3 1038
d1b7e03d 1039 foreach ($this->attemptsadded as $qa) {
217f9a61 1040 $dm->insert_question_attempt($qa, $this->quba->get_owning_context());
d1b7e03d 1041 }
6b5f24d3 1042
d1b7e03d
TH
1043 foreach ($this->attemptsmodified as $qa) {
1044 $dm->update_question_attempt($qa);
1045 }
6b5f24d3 1046
d1b7e03d
TH
1047 if ($this->modified) {
1048 $dm->update_questions_usage_by_activity($this->quba);
1049 }
1050 }
1051}
1052
1053
217f9a61
TH
1054/**
1055 * This class represents the promise to save some files from a particular draft
1056 * file area into a particular file area. It is used beause the necessary
1057 * information about what to save is to hand in the
1058 * {@link question_attempt::process_response_files()} method, but we don't know
1059 * if this question attempt will actually be saved in the database until later,
1060 * when the {@link question_engine_unit_of_work} is saved, if it is.
1061 *
1062 * @copyright 2011 The Open University
1063 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1064 */
1065class question_file_saver {
1066 /** @var int the id of the draft file area to save files from. */
1067 protected $draftitemid;
1068 /** @var string the owning component name. */
1069 protected $component;
1070 /** @var string the file area name. */
1071 protected $filearea;
1072
cd3557e6
TH
1073 /**
1074 * @var string the value to store in the question_attempt_step_data to
1075 * represent these files.
1076 */
1077 protected $value = null;
1078
217f9a61
TH
1079 /**
1080 * Constuctor.
1081 * @param int $draftitemid the draft area to save the files from.
1082 * @param string $component the component for the file area to save into.
1083 * @param string $filearea the name of the file area to save into.
1084 */
48d9c17d 1085 public function __construct($draftitemid, $component, $filearea, $text = null) {
217f9a61
TH
1086 $this->draftitemid = $draftitemid;
1087 $this->component = $component;
1088 $this->filearea = $filearea;
48d9c17d 1089 $this->value = $this->compute_value($draftitemid, $text);
217f9a61
TH
1090 }
1091
48d9c17d
TH
1092 /**
1093 * Compute the value that should be stored in the question_attempt_step_data
1094 * table. Contains a hash that (almost) uniquely encodes all the files.
1095 * @param int $draftitemid the draft file area itemid.
1096 * @param string $text optional content containing file links.
1097 */
1098 protected function compute_value($draftitemid, $text) {
cd3557e6
TH
1099 global $USER;
1100
cd3557e6
TH
1101 $fs = get_file_storage();
1102 $usercontext = get_context_instance(CONTEXT_USER, $USER->id);
1103
1104 $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
48d9c17d 1105 $draftitemid, 'sortorder, filepath, filename', false);
cd3557e6
TH
1106
1107 $string = '';
1108 foreach ($files as $file) {
1109 $string .= $file->get_filepath() . $file->get_filename() . '|' .
1110 $file->get_contenthash() . '|';
1111 }
1112
1113 if ($string) {
48d9c17d 1114 $hash = md5($string);
cd3557e6 1115 } else {
48d9c17d 1116 $hash = '';
cd3557e6
TH
1117 }
1118
48d9c17d
TH
1119 if (is_null($text)) {
1120 return $hash;
1121 }
1122
1123 // We add the file hash so a simple string comparison will say if the
1124 // files have been changed. First strip off any existing file hash.
1125 $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
1126 $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
1127 if ($hash) {
1128 $text .= '<!-- File hash: ' . $hash . ' -->';
1129 }
1130 return $text;
cd3557e6
TH
1131 }
1132
217f9a61 1133 public function __toString() {
48d9c17d 1134 return $this->value;
217f9a61
TH
1135 }
1136
1137 /**
1138 * Actually save the files.
1139 * @param integer $itemid the item id for the file area to save into.
1140 */
1141 public function save_files($itemid, $context) {
1142 file_save_draft_area_files($this->draftitemid, $context->id,
1143 $this->component, $this->filearea, $itemid);
1144 }
1145}
1146
1147
d1b7e03d
TH
1148/**
1149 * This class represents a restriction on the set of question_usage ids to include
1150 * in a larger database query. Depending of the how you are going to restrict the
1151 * list of usages, construct an appropriate subclass.
1152 *
1153 * If $qubaids is an instance of this class, example usage might be
1154 *
1155 * SELECT qa.id, qa.maxmark
1156 * FROM $qubaids->from_question_attempts('qa')
1157 * WHERE $qubaids->where() AND qa.slot = 1
1158 *
f7970e3c 1159 * @copyright 2010 The Open University
017bc1d9 1160 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1161 */
1162abstract class qubaid_condition {
1163
1164 /**
1165 * @return string the SQL that needs to go in the FROM clause when trying
1166 * to select records from the 'question_attempts' table based on the
1167 * qubaid_condition.
1168 */
1169 public abstract function from_question_attempts($alias);
1170
1171 /** @return string the SQL that needs to go in the where clause. */
1172 public abstract function where();
1173
1174 /**
1175 * @return the params needed by a query that uses
1176 * {@link from_question_attempts()} and {@link where()}.
1177 */
1178 public abstract function from_where_params();
1179
1180 /**
1181 * @return string SQL that can use used in a WHERE qubaid IN (...) query.
1182 * This method returns the "IN (...)" part.
1183 */
1184 public abstract function usage_id_in();
1185
1186 /**
1187 * @return the params needed by a query that uses {@link usage_id_in()}.
1188 */
1189 public abstract function usage_id_in_params();
1190}
1191
1192
1193/**
1194 * This class represents a restriction on the set of question_usage ids to include
1195 * in a larger database query based on an explicit list of ids.
1196 *
f7970e3c 1197 * @copyright 2010 The Open University
017bc1d9 1198 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1199 */
1200class qubaid_list extends qubaid_condition {
1201 /** @var array of ids. */
1202 protected $qubaids;
1203 protected $columntotest = null;
1204 protected $params;
1205
1206 /**
1207 * Constructor.
1208 * @param array $qubaids of question usage ids.
1209 */
1210 public function __construct(array $qubaids) {
1211 $this->qubaids = $qubaids;
1212 }
1213
1214 public function from_question_attempts($alias) {
d1b7e03d 1215 $this->columntotest = $alias . '.questionusageid';
9b40c540 1216 return '{question_attempts} ' . $alias;
d1b7e03d
TH
1217 }
1218
1219 public function where() {
1220 global $DB;
1221
1222 if (is_null($this->columntotest)) {
88f0eb15 1223 throw new coding_exception('Must call from_question_attempts before where().');
d1b7e03d
TH
1224 }
1225 if (empty($this->qubaids)) {
9b40c540 1226 $this->params = array();
d1b7e03d
TH
1227 return '1 = 0';
1228 }
d1b7e03d 1229
9b40c540 1230 return $this->columntotest . ' ' . $this->usage_id_in();
d1b7e03d
TH
1231 }
1232
1233 public function from_where_params() {
1234 return $this->params;
1235 }
1236
1237 public function usage_id_in() {
1238 global $DB;
1239
1240 if (empty($this->qubaids)) {
5f79a9bc 1241 $this->params = array();
d1b7e03d
TH
1242 return '= 0';
1243 }
9c197f44
TH
1244 list($where, $this->params) = $DB->get_in_or_equal(
1245 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid');
d1b7e03d
TH
1246 return $where;
1247 }
1248
1249 public function usage_id_in_params() {
1250 return $this->params;
1251 }
1252}
1253
1254
1255/**
1256 * This class represents a restriction on the set of question_usage ids to include
1257 * in a larger database query based on JOINing to some other tables.
1258 *
1259 * The general form of the query is something like
1260 *
1261 * SELECT qa.id, qa.maxmark
1262 * FROM $from
2a3bdbf9 1263 * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
d1b7e03d
TH
1264 * WHERE $where AND qa.slot = 1
1265 *
1266 * where $from, $usageidcolumn and $where are the arguments to the constructor.
1267 *
f7970e3c 1268 * @copyright 2010 The Open University
017bc1d9 1269 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
1270 */
1271class qubaid_join extends qubaid_condition {
1272 public $from;
1273 public $usageidcolumn;
1274 public $where;
1275 public $params;
1276
1277 /**
1278 * Constructor. The meaning of the arguments is explained in the class comment.
1279 * @param string $from SQL fragemnt to go in the FROM clause.
1280 * @param string $usageidcolumn the column in $from that should be
1281 * made equal to the usageid column in the JOIN clause.
1282 * @param string $where SQL fragment to go in the where clause.
e24ee794 1283 * @param array $params required by the SQL. You must use named parameters.
d1b7e03d
TH
1284 */
1285 public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
1286 $this->from = $from;
1287 $this->usageidcolumn = $usageidcolumn;
1288 $this->params = $params;
1289 if (empty($where)) {
1290 $where = '1 = 1';
1291 }
1292 $this->where = $where;
1293 }
1294
1295 public function from_question_attempts($alias) {
d1b7e03d 1296 return "$this->from
0f33deaf 1297 JOIN {question_attempts} {$alias} ON " .
d1b7e03d
TH
1298 "{$alias}.questionusageid = $this->usageidcolumn";
1299 }
1300
1301 public function where() {
1302 return $this->where;
1303 }
1304
1305 public function from_where_params() {
1306 return $this->params;
1307 }
1308
1309 public function usage_id_in() {
1310 return "IN (SELECT $this->usageidcolumn FROM $this->from WHERE $this->where)";
1311 }
1312
1313 public function usage_id_in_params() {
1314 return $this->params;
1315 }
1316}