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