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