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