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