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