Merge branch 'master_MDL-31360' of git://github.com/danmarsden/moodle
[moodle.git] / question / engine / datalib.php
1 <?php
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/>.
17 /**
18  * Code for loading and saving question attempts to and from the database.
19  *
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  *
39  * @package    moodlecore
40  * @subpackage questionengine
41  * @copyright  2009 The Open University
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
46 defined('MOODLE_INTERNAL') || die();
49 /**
50  * This class controls the loading and saving of question engine data to and from
51  * the database.
52  *
53  * @copyright  2009 The Open University
54  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55  */
56 class question_engine_data_mapper {
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;
63     /**
64      * @param moodle_database $db a database connectoin. Defaults to global $DB.
65      */
66     public function __construct($db = null) {
67         if (is_null($db)) {
68             global $DB;
69             $this->db = $DB;
70         } else {
71             $this->db = $db;
72         }
73     }
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) {
81         $record = new stdClass();
82         $record->contextid = $quba->get_owning_context()->id;
83         $record->component = $quba->get_owning_component();
84         $record->preferredbehaviour = $quba->get_preferred_behaviour();
86         $newid = $this->db->insert_record('question_usages', $record);
87         $quba->set_id_from_database($newid);
89         foreach ($quba->get_attempt_iterator() as $qa) {
90             $this->insert_question_attempt($qa, $quba->get_owning_context());
91         }
92     }
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.
98      * @param context $context the context of the owning question_usage_by_activity.
99      */
100     public function insert_question_attempt(question_attempt $qa, $context) {
101         $record = new stdClass();
102         $record->questionusageid = $qa->get_usage_id();
103         $record->slot = $qa->get_slot();
104         $record->behaviour = $qa->get_behaviour_name();
105         $record->questionid = $qa->get_question()->id;
106         $record->variant = $qa->get_variant();
107         $record->maxmark = $qa->get_max_mark();
108         $record->minfraction = $qa->get_min_fraction();
109         $record->flagged = $qa->is_flagged();
110         $record->questionsummary = $qa->get_question_summary();
111         if (textlib::strlen($record->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
112             // It seems some people write very long quesions! MDL-30760
113             $record->questionsummary = textlib::substr($record->questionsummary,
114                     0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
115         }
116         $record->rightanswer = $qa->get_right_answer_summary();
117         $record->responsesummary = $qa->get_response_summary();
118         $record->timemodified = time();
119         $record->id = $this->db->insert_record('question_attempts', $record);
121         foreach ($qa->get_step_iterator() as $seq => $step) {
122             $this->insert_question_attempt_step($step, $record->id, $seq, $context);
123         }
124     }
126     /**
127      * Helper method used by insert_question_attempt_step and update_question_attempt_step
128      * @param question_attempt_step $step the step to store.
129      * @param int $questionattemptid the question attept id this step belongs to.
130      * @param int $seq the sequence number of this stop.
131      * @return stdClass data to insert into the database.
132      */
133     protected function make_step_record(question_attempt_step $step, $questionattemptid, $seq) {
134         $record = new stdClass();
135         $record->questionattemptid = $questionattemptid;
136         $record->sequencenumber = $seq;
137         $record->state = (string) $step->get_state();
138         $record->fraction = $step->get_fraction();
139         $record->timecreated = $step->get_timecreated();
140         $record->userid = $step->get_user_id();
141         return $record;
142     }
144     /**
145      * Helper method used by insert_question_attempt_step and update_question_attempt_step
146      * @param question_attempt_step $step the step to store.
147      * @param int $stepid the id of the step.
148      * @param context $context the context of the owning question_usage_by_activity.
149      */
150     protected function insert_step_data(question_attempt_step $step, $stepid, $context) {
151         foreach ($step->get_all_data() as $name => $value) {
152             if ($value instanceof question_file_saver) {
153                 $value->save_files($stepid, $context);
154             }
156             $data = new stdClass();
157             $data->attemptstepid = $stepid;
158             $data->name = $name;
159             $data->value = $value;
160             $this->db->insert_record('question_attempt_step_data', $data, false);
161         }
162     }
164     /**
165      * Store a {@link question_attempt_step} in the database.
166      * @param question_attempt_step $step the step to store.
167      * @param int $questionattemptid the question attept id this step belongs to.
168      * @param int $seq the sequence number of this stop.
169      * @param context $context the context of the owning question_usage_by_activity.
170      */
171     public function insert_question_attempt_step(question_attempt_step $step,
172             $questionattemptid, $seq, $context) {
174         $record = $this->make_step_record($step, $questionattemptid, $seq);
175         $record->id = $this->db->insert_record('question_attempt_steps', $record);
177         $this->insert_step_data($step, $record->id, $context);
178     }
180     /**
181      * Update a {@link question_attempt_step} in the database.
182      * @param question_attempt_step $qa the step to store.
183      * @param int $questionattemptid the question attept id this step belongs to.
184      * @param int $seq the sequence number of this stop.
185      * @param context $context the context of the owning question_usage_by_activity.
186      */
187     public function update_question_attempt_step(question_attempt_step $step,
188             $questionattemptid, $seq, $context) {
190         $record = $this->make_step_record($step, $questionattemptid, $seq);
191         $record->id = $step->get_id();
192         $this->db->update_record('question_attempt_steps', $record);
194         $this->db->delete_records('question_attempt_step_data',
195                 array('attemptstepid' => $record->id));
196         $this->insert_step_data($step, $record->id, $context);
197     }
199     /**
200      * Load a {@link question_attempt_step} from the database.
201      * @param int $stepid the id of the step to load.
202      * @param question_attempt_step the step that was loaded.
203      */
204     public function load_question_attempt_step($stepid) {
205         $records = $this->db->get_recordset_sql("
206 SELECT
207     qas.id AS attemptstepid,
208     qas.questionattemptid,
209     qas.sequencenumber,
210     qas.state,
211     qas.fraction,
212     qas.timecreated,
213     qas.userid,
214     qasd.name,
215     qasd.value
217 FROM {question_attempt_steps} qas
218 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
220 WHERE
221     qas.id = :stepid
222         ", array('stepid' => $stepid));
224         if (!$records->valid()) {
225             throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
226         }
228         $step = question_attempt_step::load_from_records($records, $stepid);
229         $records->close();
231         return $step;
232     }
234     /**
235      * Load a {@link question_attempt} from the database, including all its
236      * steps.
237      * @param int $questionattemptid the id of the question attempt to load.
238      * @param question_attempt the question attempt that was loaded.
239      */
240     public function load_question_attempt($questionattemptid) {
241         $records = $this->db->get_recordset_sql("
242 SELECT
243     quba.contextid,
244     quba.preferredbehaviour,
245     qa.id AS questionattemptid,
246     qa.questionusageid,
247     qa.slot,
248     qa.behaviour,
249     qa.questionid,
250     qa.variant,
251     qa.maxmark,
252     qa.minfraction,
253     qa.flagged,
254     qa.questionsummary,
255     qa.rightanswer,
256     qa.responsesummary,
257     qa.timemodified,
258     qas.id AS attemptstepid,
259     qas.sequencenumber,
260     qas.state,
261     qas.fraction,
262     qas.timecreated,
263     qas.userid,
264     qasd.name,
265     qasd.value
267 FROM      {question_attempts           qa
268 JOIN      {question_usages}            quba ON quba.id               = qa.questionusageid
269 LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
270 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
272 WHERE
273     qa.id = :questionattemptid
275 ORDER BY
276     qas.sequencenumber
277         ", array('questionattemptid' => $questionattemptid));
279         if (!$records->valid()) {
280             throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
281         }
283         $record = current($records);
284         $qa = question_attempt::load_from_records($records, $questionattemptid,
285                 new question_usage_null_observer(), $record->preferredbehaviour);
286         $records->close();
288         return $qa;
289     }
291     /**
292      * Load a {@link question_usage_by_activity} from the database, including
293      * all its {@link question_attempt}s and all their steps.
294      * @param int $qubaid the id of the usage to load.
295      * @param question_usage_by_activity the usage that was loaded.
296      */
297     public function load_questions_usage_by_activity($qubaid) {
298         $records = $this->db->get_recordset_sql("
299 SELECT
300     quba.id AS qubaid,
301     quba.contextid,
302     quba.component,
303     quba.preferredbehaviour,
304     qa.id AS questionattemptid,
305     qa.questionusageid,
306     qa.slot,
307     qa.behaviour,
308     qa.questionid,
309     qa.variant,
310     qa.maxmark,
311     qa.minfraction,
312     qa.flagged,
313     qa.questionsummary,
314     qa.rightanswer,
315     qa.responsesummary,
316     qa.timemodified,
317     qas.id AS attemptstepid,
318     qas.sequencenumber,
319     qas.state,
320     qas.fraction,
321     qas.timecreated,
322     qas.userid,
323     qasd.name,
324     qasd.value
326 FROM      {question_usages}            quba
327 LEFT JOIN {question_attempts}          qa   ON qa.questionusageid    = quba.id
328 LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
329 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
331 WHERE
332     quba.id = :qubaid
334 ORDER BY
335     qa.slot,
336     qas.sequencenumber
337     ", array('qubaid' => $qubaid));
339         if (!$records->valid()) {
340             throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
341         }
343         $quba = question_usage_by_activity::load_from_records($records, $qubaid);
344         $records->close();
346         return $quba;
347     }
349     /**
350      * Load information about the latest state of each question from the database.
351      *
352      * @param qubaid_condition $qubaids used to restrict which usages are included
353      * in the query. See {@link qubaid_condition}.
354      * @param array $slots A list of slots for the questions you want to konw about.
355      * @return array of records. See the SQL in this function to see the fields available.
356      */
357     public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
358         list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
360         $records = $this->db->get_records_sql("
361 SELECT
362     qas.id,
363     qa.id AS questionattemptid,
364     qa.questionusageid,
365     qa.slot,
366     qa.behaviour,
367     qa.questionid,
368     qa.variant,
369     qa.maxmark,
370     qa.minfraction,
371     qa.flagged,
372     qa.questionsummary,
373     qa.rightanswer,
374     qa.responsesummary,
375     qa.timemodified,
376     qas.id AS attemptstepid,
377     qas.sequencenumber,
378     qas.state,
379     qas.fraction,
380     qas.timecreated,
381     qas.userid
383 FROM {$qubaids->from_question_attempts('qa')}
384 JOIN {question_attempt_steps} qas ON
385         qas.id = {$this->latest_step_for_qa_subquery()}
387 WHERE
388     {$qubaids->where()} AND
389     qa.slot $slottest
390         ", $params + $qubaids->from_where_params());
392         return $records;
393     }
395     /**
396      * Load summary information about the state of each question in a group of
397      * attempts. This is used, for example, by the quiz manual grading report,
398      * to show how many attempts at each question need to be graded.
399      *
400      * @param qubaid_condition $qubaids used to restrict which usages are included
401      * in the query. See {@link qubaid_condition}.
402      * @param array $slots A list of slots for the questions you want to konw about.
403      * @return array The array keys are slot,qestionid. The values are objects with
404      * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
405      * $manuallygraded and $all.
406      */
407     public function load_questions_usages_question_state_summary(
408             qubaid_condition $qubaids, $slots) {
409         list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
411         $rs = $this->db->get_recordset_sql("
412 SELECT
413     qa.slot,
414     qa.questionid,
415     q.name,
416     CASE qas.state
417         {$this->full_states_to_summary_state_sql()}
418     END AS summarystate,
419     COUNT(1) AS numattempts
421 FROM {$qubaids->from_question_attempts('qa')}
422 JOIN {question_attempt_steps} qas ON
423         qas.id = {$this->latest_step_for_qa_subquery()}
424 JOIN {question} q ON q.id = qa.questionid
426 WHERE
427     {$qubaids->where()} AND
428     qa.slot $slottest
430 GROUP BY
431     qa.slot,
432     qa.questionid,
433     q.name,
434     q.id,
435     CASE qas.state
436         {$this->full_states_to_summary_state_sql()}
437     END
439 ORDER BY
440     qa.slot,
441     qa.questionid,
442     q.name,
443     q.id
444         ", $params + $qubaids->from_where_params());
446         $results = array();
447         foreach ($rs as $row) {
448             $index = $row->slot . ',' . $row->questionid;
450             if (!array_key_exists($index, $results)) {
451                 $res = new stdClass();
452                 $res->slot = $row->slot;
453                 $res->questionid = $row->questionid;
454                 $res->name = $row->name;
455                 $res->inprogress = 0;
456                 $res->needsgrading = 0;
457                 $res->autograded = 0;
458                 $res->manuallygraded = 0;
459                 $res->all = 0;
460                 $results[$index] = $res;
461             }
463             $results[$index]->{$row->summarystate} = $row->numattempts;
464             $results[$index]->all += $row->numattempts;
465         }
466         $rs->close();
468         return $results;
469     }
471     /**
472      * Get a list of usage ids where the question with slot $slot, and optionally
473      * also with question id $questionid, is in summary state $summarystate. Also
474      * return the total count of such states.
475      *
476      * Only a subset of the ids can be returned by using $orderby, $limitfrom and
477      * $limitnum. A special value 'random' can be passed as $orderby, in which case
478      * $limitfrom is ignored.
479      *
480      * @param qubaid_condition $qubaids used to restrict which usages are included
481      * in the query. See {@link qubaid_condition}.
482      * @param int $slot The slot for the questions you want to konw about.
483      * @param int $questionid (optional) Only return attempts that were of this specific question.
484      * @param string $summarystate the summary state of interest, or 'all'.
485      * @param string $orderby the column to order by.
486      * @param array $params any params required by any of the SQL fragments.
487      * @param int $limitfrom implements paging of the results.
488      *      Ignored if $orderby = random or $limitnum is null.
489      * @param int $limitnum implements paging of the results. null = all.
490      * @return array with two elements, an array of usage ids, and a count of the total number.
491      */
492     public function load_questions_usages_where_question_in_state(
493             qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
494             $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) {
496         $extrawhere = '';
497         if ($questionid) {
498             $extrawhere .= ' AND qa.questionid = :questionid';
499             $params['questionid'] = $questionid;
500         }
501         if ($summarystate != 'all') {
502             list($test, $sparams) = $this->in_summary_state_test($summarystate);
503             $extrawhere .= ' AND qas.state ' . $test;
504             $params += $sparams;
505         }
507         if ($orderby == 'random') {
508             $sqlorderby = '';
509         } else if ($orderby) {
510             $sqlorderby = 'ORDER BY ' . $orderby;
511         } else {
512             $sqlorderby = '';
513         }
515         // We always want the total count, as well as the partcular list of ids,
516         // based on the paging and sort order. Becuase the list of ids is never
517         // going to be too rediculously long. My worst-case scenario is
518         // 10,000 students in the coures, each doing 5 quiz attempts. That
519         // is a 50,000 element int => int array, which PHP seems to use 5MB
520         // memeory to store on a 64 bit server.
521         $params += $qubaids->from_where_params();
522         $params['slot'] = $slot;
523         $qubaids = $this->db->get_records_sql_menu("
524 SELECT
525     qa.questionusageid,
526     1
528 FROM {$qubaids->from_question_attempts('qa')}
529 JOIN {question_attempt_steps} qas ON
530         qas.id = {$this->latest_step_for_qa_subquery()}
531 JOIN {question} q ON q.id = qa.questionid
533 WHERE
534     {$qubaids->where()} AND
535     qa.slot = :slot
536     $extrawhere
538 $sqlorderby
539         ", $params);
541         $qubaids = array_keys($qubaids);
542         $count = count($qubaids);
544         if ($orderby == 'random') {
545             shuffle($qubaids);
546             $limitfrom = 0;
547         }
549         if (!is_null($limitnum)) {
550             $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
551         }
553         return array($qubaids, $count);
554     }
556     /**
557      * Load a {@link question_usage_by_activity} from the database, including
558      * all its {@link question_attempt}s and all their steps.
559      * @param qubaid_condition $qubaids used to restrict which usages are included
560      * in the query. See {@link qubaid_condition}.
561      * @param array $slots if null, load info for all quesitions, otherwise only
562      * load the averages for the specified questions.
563      */
564     public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
565         if (!empty($slots)) {
566             list($slottest, $slotsparams) = $this->db->get_in_or_equal(
567                     $slots, SQL_PARAMS_NAMED, 'slot');
568             $slotwhere = " AND qa.slot $slottest";
569         } else {
570             $slotwhere = '';
571             $params = array();
572         }
574         list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
575                 (string) question_state::$gaveup,
576                 (string) question_state::$gradedwrong,
577                 (string) question_state::$gradedpartial,
578                 (string) question_state::$gradedright,
579                 (string) question_state::$mangaveup,
580                 (string) question_state::$mangrwrong,
581                 (string) question_state::$mangrpartial,
582                 (string) question_state::$mangrright), SQL_PARAMS_NAMED, 'st');
584         return $this->db->get_records_sql("
585 SELECT
586     qa.slot,
587     AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
588     COUNT(1) AS numaveraged
590 FROM {$qubaids->from_question_attempts('qa')}
591 JOIN {question_attempt_steps} qas ON
592         qas.id = {$this->latest_step_for_qa_subquery()}
594 WHERE
595     {$qubaids->where()}
596     $slotwhere
597     AND qas.state $statetest
599 GROUP BY qa.slot
601 ORDER BY qa.slot
602         ", $slotsparams + $stateparams + $qubaids->from_where_params());
603     }
605     /**
606      * Load a {@link question_attempt} from the database, including all its
607      * steps.
608      * @param int $questionid the question to load all the attempts fors.
609      * @param qubaid_condition $qubaids used to restrict which usages are included
610      * in the query. See {@link qubaid_condition}.
611      * @return array of question_attempts.
612      */
613     public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
614         global $DB;
616         $params = $qubaids->from_where_params();
617         $params['questionid'] = $questionid;
619         $records = $DB->get_recordset_sql("
620 SELECT
621     quba.contextid,
622     quba.preferredbehaviour,
623     qa.id AS questionattemptid,
624     qa.questionusageid,
625     qa.slot,
626     qa.behaviour,
627     qa.questionid,
628     qa.variant,
629     qa.maxmark,
630     qa.minfraction,
631     qa.flagged,
632     qa.questionsummary,
633     qa.rightanswer,
634     qa.responsesummary,
635     qa.timemodified,
636     qas.id AS attemptstepid,
637     qas.sequencenumber,
638     qas.state,
639     qas.fraction,
640     qas.timecreated,
641     qas.userid,
642     qasd.name,
643     qasd.value
645 FROM {$qubaids->from_question_attempts('qa')}
646 JOIN {question_usages} quba ON quba.id = qa.questionusageid
647 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
648 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
650 WHERE
651     {$qubaids->where()} AND
652     qa.questionid = :questionid
654 ORDER BY
655     quba.id,
656     qa.id,
657     qas.sequencenumber
658         ", $params);
660         $questionattempts = array();
661         while ($records->valid()) {
662             $record = $records->current();
663             $questionattempts[$record->questionattemptid] =
664                     question_attempt::load_from_records($records,
665                     $record->questionattemptid, new question_usage_null_observer(),
666                     $record->preferredbehaviour);
667         }
668         $records->close();
670         return $questionattempts;
671     }
673     /**
674      * Update a question_usages row to refect any changes in a usage (but not
675      * any of its question_attempts.
676      * @param question_usage_by_activity $quba the usage that has changed.
677      */
678     public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
679         $record = new stdClass();
680         $record->id = $quba->get_id();
681         $record->contextid = $quba->get_owning_context()->id;
682         $record->component = $quba->get_owning_component();
683         $record->preferredbehaviour = $quba->get_preferred_behaviour();
685         $this->db->update_record('question_usages', $record);
686     }
688     /**
689      * Update a question_attempts row to refect any changes in a question_attempt
690      * (but not any of its steps).
691      * @param question_attempt $qa the question attempt that has changed.
692      */
693     public function update_question_attempt(question_attempt $qa) {
694         $record = new stdClass();
695         $record->id = $qa->get_database_id();
696         $record->maxmark = $qa->get_max_mark();
697         $record->minfraction = $qa->get_min_fraction();
698         $record->flagged = $qa->is_flagged();
699         $record->questionsummary = $qa->get_question_summary();
700         $record->rightanswer = $qa->get_right_answer_summary();
701         $record->responsesummary = $qa->get_response_summary();
702         $record->timemodified = time();
704         $this->db->update_record('question_attempts', $record);
705     }
707     /**
708      * Delete a question_usage_by_activity and all its associated
709      * {@link question_attempts} and {@link question_attempt_steps} from the
710      * database.
711      * @param qubaid_condition $qubaids identifies which question useages to delete.
712      */
713     public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
714         $where = "qa.questionusageid {$qubaids->usage_id_in()}";
715         $params = $qubaids->usage_id_in_params();
717         $contextids = $this->db->get_records_sql_menu("
718                 SELECT DISTINCT contextid, 1
719                 FROM {question_usages}
720                 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
721         foreach ($contextids as $contextid => $notused) {
722             $this->delete_response_files($contextid, "IN (
723                     SELECT qas.id
724                     FROM {question_attempts} qa
725                     JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
726                     WHERE $where)", $params);
727         }
729         if ($this->db->get_dbfamily() == 'mysql') {
730             $this->delete_usage_records_for_mysql($qubaids);
731             return;
732         }
734         $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
735                 SELECT qas.id
736                 FROM {question_attempts} qa
737                 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
738                 WHERE $where)", $params);
740         $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
741                 SELECT qa.id
742                 FROM {question_attempts} qa
743                 WHERE $where)", $params);
745         $this->db->delete_records_select('question_attempts',
746                 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
747                 $qubaids->usage_id_in_params());
749         $this->db->delete_records_select('question_usages',
750                 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
751     }
753     /**
754      * This function is a work-around for poor MySQL performance with
755      * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
756      * syntax to get good performance. See MDL-29520.
757      * @param qubaid_condition $qubaids identifies which question useages to delete.
758      */
759     protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
760         // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
761         $this->db->execute('
762                 DELETE qu, qa, qas, qasd
763                   FROM {question_usages}            qu
764                   JOIN {question_attempts}          qa   ON qa.questionusageid = qu.id
765              LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
766              LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
767                  WHERE qu.id ' . $qubaids->usage_id_in(),
768                 $qubaids->usage_id_in_params());
769     }
771     /**
772      * This function is a work-around for poor MySQL performance with
773      * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
774      * syntax to get good performance. See MDL-29520.
775      * @param string $test sql fragment.
776      * @param array $params used by $test.
777      */
778     protected function delete_attempt_steps_for_mysql($test, $params) {
779         // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
780         $this->db->execute('
781                 DELETE qas, qasd
782                   FROM {question_attempt_steps}     qas
783              LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
784                  WHERE qas.questionattemptid ' . $test, $params);
785     }
787     /**
788      * Delete all the steps for a question attempt.
789      * @param int $qaids question_attempt id.
790      * @param context $context the context that the $quba belongs to.
791      */
792     public function delete_steps($stepids, $context) {
793         if (empty($stepids)) {
794             return;
795         }
796         list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED);
798         if ($deletefiles) {
799             $this->delete_response_files($context->id, $test, $params);
800         }
802         if ($this->db->get_dbfamily() == 'mysql') {
803             $this->delete_attempt_steps_for_mysql($test, $params);
804             return;
805         }
807         $this->db->delete_records_select('question_attempt_step_data',
808                 "attemptstepid $test", $params);
809         $this->db->delete_records_select('question_attempt_steps',
810                 "attemptstepid $test", $params);
811     }
813     /**
814      * Delete all the files belonging to the response variables in the gives
815      * question attempt steps.
816      * @param int $contextid the context these attempts belong to.
817      * @param string $itemidstest a bit of SQL that can be used in a
818      *      WHERE itemid $itemidstest clause. Must use named params.
819      * @param array $params any query parameters used in $itemidstest.
820      */
821     protected function delete_response_files($contextid, $itemidstest, $params) {
822         $fs = get_file_storage();
823         foreach (question_engine::get_all_response_file_areas() as $filearea) {
824             $fs->delete_area_files_select($contextid, 'question', $filearea,
825                     $itemidstest, $params);
826         }
827     }
829     /**
830      * Delete all the previews for a given question.
831      * @param int $questionid question id.
832      */
833     public function delete_previews($questionid) {
834         $previews = $this->db->get_records_sql_menu("
835                 SELECT DISTINCT quba.id, 1
836                 FROM {question_usages} quba
837                 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
838                 WHERE quba.component = 'core_question_preview' AND
839                     qa.questionid = ?", array($questionid));
840         if (empty($previews)) {
841             return;
842         }
843         $this->delete_questions_usage_by_activities(new qubaid_list($previews));
844     }
846     /**
847      * Update the flagged state of a question in the database.
848      * @param int $qubaid the question usage id.
849      * @param int $questionid the question id.
850      * @param int $sessionid the question_attempt id.
851      * @param bool $newstate the new state of the flag. true = flagged.
852      */
853     public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
854         if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
855                 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
856             throw new moodle_exception('errorsavingflags', 'question');
857         }
859         $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
860     }
862     /**
863      * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
864      * column to a summary state. Use this like
865      * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
866      * @param string SQL fragment.
867      */
868     protected function full_states_to_summary_state_sql() {
869         $sql = '';
870         foreach (question_state::get_all() as $state) {
871             $sql .= "WHEN '$state' THEN '{$state->get_summary_state()}'\n";
872         }
873         return $sql;
874     }
876     /**
877      * Get the SQL needed to test that question_attempt_steps.state is in a
878      * state corresponding to $summarystate.
879      * @param string $summarystate one of
880      * inprogress, needsgrading, manuallygraded or autograded
881      * @param bool $equal if false, do a NOT IN test. Default true.
882      * @return string SQL fragment.
883      */
884     public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
885         $states = question_state::get_all_for_summary_state($summarystate);
886         return $this->db->get_in_or_equal(array_map('strval', $states),
887                 SQL_PARAMS_NAMED, $prefix, $equal);
888     }
890     /**
891      * Change the maxmark for the question_attempt with number in usage $slot
892      * for all the specified question_attempts.
893      * @param qubaid_condition $qubaids Selects which usages are updated.
894      * @param int $slot the number is usage to affect.
895      * @param number $newmaxmark the new max mark to set.
896      */
897     public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
898         $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
899                 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
900                 $qubaids->usage_id_in_params() + array('slot' => $slot));
901     }
903     /**
904      * Return a subquery that computes the sum of the marks for all the questions
905      * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
906      * parameter.
907      *
908      * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
909      * this method.
910      *
911      * @param string $qubaid SQL fragment that controls which usage is summed.
912      * This will normally be the name of a column in the outer query. Not that this
913      * SQL fragment must not contain any placeholders.
914      * @return string SQL code for the subquery.
915      */
916     public function sum_usage_marks_subquery($qubaid) {
917         // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives
918         // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that.
919         // We always want to return a number, so the COALESCE is there to turn the
920         // NULL total into a 0.
921         return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0)
922             FROM {question_attempts} qa
923             JOIN {question_attempt_steps} qas ON qas.id = (
924                 SELECT MAX(summarks_qas.id)
925                   FROM {question_attempt_steps} summarks_qas
926                  WHERE summarks_qas.questionattemptid = qa.id
927             )
928             WHERE qa.questionusageid = $qubaid
929             HAVING COUNT(CASE
930                 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1
931                 ELSE NULL
932             END) = 0";
933     }
935     /**
936      * Get a subquery that returns the latest step of every qa in some qubas.
937      * Currently, this is only used by the quiz reports. See
938      * {@link quiz_attempt_report_table::add_latest_state_join()}.
939      * @param string $alias alias to use for this inline-view.
940      * @param qubaid_condition $qubaids restriction on which question_usages we
941      *      are interested in. This is important for performance.
942      * @return array with two elements, the SQL fragment and any params requried.
943      */
944     public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) {
945         return array("(
946                 SELECT {$alias}qa.id AS questionattemptid,
947                        {$alias}qa.questionusageid,
948                        {$alias}qa.slot,
949                        {$alias}qa.behaviour,
950                        {$alias}qa.questionid,
951                        {$alias}qa.variant,
952                        {$alias}qa.maxmark,
953                        {$alias}qa.minfraction,
954                        {$alias}qa.flagged,
955                        {$alias}qa.questionsummary,
956                        {$alias}qa.rightanswer,
957                        {$alias}qa.responsesummary,
958                        {$alias}qa.timemodified,
959                        {$alias}qas.id AS attemptstepid,
960                        {$alias}qas.sequencenumber,
961                        {$alias}qas.state,
962                        {$alias}qas.fraction,
963                        {$alias}qas.timecreated,
964                        {$alias}qas.userid
966                   FROM {$qubaids->from_question_attempts($alias . 'qa')}
967                   JOIN {question_attempt_steps} {$alias}qas ON
968                            {$alias}qas.id = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
969                  WHERE {$qubaids->where()}
970             ) $alias", $qubaids->from_where_params());
971     }
973     protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
974         return "(
975                 SELECT MAX(id)
976                 FROM {question_attempt_steps}
977                 WHERE questionattemptid = $questionattemptid
978             )";
979     }
981     /**
982      * @param array $questionids of question ids.
983      * @param qubaid_condition $qubaids ids of the usages to consider.
984      * @return boolean whether any of these questions are being used by any of
985      *      those usages.
986      */
987     public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
988         list($test, $params) = $this->db->get_in_or_equal($questionids);
989         return $this->db->record_exists_select('question_attempts',
990                 'questionid ' . $test . ' AND questionusageid ' .
991                 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
992     }
996 /**
997  * Implementation of the unit of work pattern for the question engine.
998  *
999  * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
1000  * changes to a {@link question_usage_by_activity}, and its constituent parts,
1001  * so that the changes can be saved to the database when {@link save()} is called.
1002  *
1003  * @copyright  2009 The Open University
1004  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1005  */
1006 class question_engine_unit_of_work implements question_usage_observer {
1007     /** @var question_usage_by_activity the usage being tracked. */
1008     protected $quba;
1010     /** @var boolean whether any of the fields of the usage have been changed. */
1011     protected $modified = false;
1013     /**
1014      * @var array list of slot => {@link question_attempt}s that
1015      * were already in the usage, and which have been modified.
1016      */
1017     protected $attemptsmodified = array();
1019     /**
1020      * @var array list of slot => {@link question_attempt}s that
1021      * have been added to the usage.
1022      */
1023     protected $attemptsadded = array();
1025     /**
1026      * @var array of array(question_attempt_step, question_attempt id, seq number)
1027      * of steps that have been added to question attempts in this usage.
1028      */
1029     protected $stepsadded = array();
1031     /**
1032      * @var array of array(question_attempt_step, question_attempt id, seq number)
1033      * of steps that have been modified in their attempt.
1034      */
1035     protected $stepsmodified = array();
1037     /**
1038      * @var array list of question_attempt_step.id => question_attempt_step of steps
1039      * that were previously stored in the database, but which are no longer required.
1040      */
1041     protected $stepsdeleted = array();
1043     /**
1044      * Constructor.
1045      * @param question_usage_by_activity $quba the usage to track.
1046      */
1047     public function __construct(question_usage_by_activity $quba) {
1048         $this->quba = $quba;
1049     }
1051     public function notify_modified() {
1052         $this->modified = true;
1053     }
1055     public function notify_attempt_modified(question_attempt $qa) {
1056         $slot = $qa->get_slot();
1057         if (!array_key_exists($slot, $this->attemptsadded)) {
1058             $this->attemptsmodified[$slot] = $qa;
1059         }
1060     }
1062     public function notify_attempt_added(question_attempt $qa) {
1063         $this->attemptsadded[$qa->get_slot()] = $qa;
1064     }
1066     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
1067         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1068             return;
1069         }
1071         if (($key = $this->is_step_added($step)) !== false) {
1072             return;
1073         }
1075         if (($key = $this->is_step_modified($step)) !== false) {
1076             throw new coding_exception('Cannot add a step that has already been modified.');
1077         }
1079         if (($key = $this->is_step_deleted($step)) !== false) {
1080             unset($this->stepsdeleted[$step->get_id()]);
1081             $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1082             return;
1083         }
1085         $stepid = $step->get_id();
1086         if ($stepid) {
1087             if (array_key_exists($stepid, $this->stepsdeleted)) {
1088                 unset($this->stepsdeleted[$stepid]);
1089             }
1090             $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1092         } else {
1093             $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
1094         }
1095     }
1097     public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
1098         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1099             return;
1100         }
1102         if (($key = $this->is_step_added($step)) !== false) {
1103             return;
1104         }
1106         if (($key = $this->is_step_deleted($step)) !== false) {
1107             throw new coding_exception('Cannot modify a step after it has been deleted.');
1108         }
1110         $stepid = $step->get_id();
1111         if (empty($stepid)) {
1112             throw new coding_exception('Cannot modify a step that has never been stored in the database.');
1113         }
1115         $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
1116     }
1118     public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
1119         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
1120             return;
1121         }
1123         if (($key = $this->is_step_added($step)) !== false) {
1124             unset($this->stepsadded[$key]);
1125             return;
1126         }
1128         if (($key = $this->is_step_modified($step)) !== false) {
1129             unset($this->stepsmodified[$key]);
1130         }
1132         $stepid = $step->get_id();
1133         if (empty($stepid)) {
1134             return; // Was never in the database.
1135         }
1137         $this->stepsdeleted[$stepid] = $step;
1138     }
1140     /**
1141      * @param question_attempt_step $step a step
1142      * @return int|false if the step is in the list of steps to be added, return
1143      *      the key, otherwise return false.
1144      */
1145     protected function is_step_added(question_attempt_step $step) {
1146         foreach ($this->stepsadded as $key => $data) {
1147             list($addedstep, $qaid, $seq) = $data;
1148             if ($addedstep === $step) {
1149                 return $key;
1150             }
1151         }
1152         return false;
1153     }
1155     /**
1156      * @param question_attempt_step $step a step
1157      * @return int|false if the step is in the list of steps to be modified, return
1158      *      the key, otherwise return false.
1159      */
1160     protected function is_step_modified(question_attempt_step $step) {
1161         foreach ($this->stepsmodified as $key => $data) {
1162             list($modifiedstep, $qaid, $seq) = $data;
1163             if ($modifiedstep === $step) {
1164                 return $key;
1165             }
1166         }
1167         return false;
1168     }
1170     /**
1171      * @param question_attempt_step $step a step
1172      * @return bool whether the step is in the list of steps to be deleted.
1173      */
1174     protected function is_step_deleted(question_attempt_step $step) {
1175         foreach ($this->stepsdeleted as $deletedstep) {
1176             if ($deletedstep === $step) {
1177                 return true;
1178             }
1179         }
1180         return false;
1181     }
1183     /**
1184      * Write all the changes we have recorded to the database.
1185      * @param question_engine_data_mapper $dm the mapper to use to update the database.
1186      */
1187     public function save(question_engine_data_mapper $dm) {
1188         $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context());
1190         foreach ($this->stepsmodified as $stepinfo) {
1191             list($step, $questionattemptid, $seq) = $stepinfo;
1192             $dm->update_question_attempt_step($step, $questionattemptid, $seq,
1193                     $this->quba->get_owning_context());
1194         }
1196         foreach ($this->stepsadded as $stepinfo) {
1197             list($step, $questionattemptid, $seq) = $stepinfo;
1198             $dm->insert_question_attempt_step($step, $questionattemptid, $seq,
1199                     $this->quba->get_owning_context());
1200         }
1202         foreach ($this->attemptsadded as $qa) {
1203             $dm->insert_question_attempt($qa, $this->quba->get_owning_context());
1204         }
1206         foreach ($this->attemptsmodified as $qa) {
1207             $dm->update_question_attempt($qa);
1208         }
1210         if ($this->modified) {
1211             $dm->update_questions_usage_by_activity($this->quba);
1212         }
1213     }
1217 /**
1218  * This class represents the promise to save some files from a particular draft
1219  * file area into a particular file area. It is used beause the necessary
1220  * information about what to save is to hand in the
1221  * {@link question_attempt::process_response_files()} method, but we don't know
1222  * if this question attempt will actually be saved in the database until later,
1223  * when the {@link question_engine_unit_of_work} is saved, if it is.
1224  *
1225  * @copyright  2011 The Open University
1226  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1227  */
1228 class question_file_saver {
1229     /** @var int the id of the draft file area to save files from. */
1230     protected $draftitemid;
1231     /** @var string the owning component name. */
1232     protected $component;
1233     /** @var string the file area name. */
1234     protected $filearea;
1236     /**
1237      * @var string the value to store in the question_attempt_step_data to
1238      * represent these files.
1239      */
1240     protected $value = null;
1242     /**
1243      * Constuctor.
1244      * @param int $draftitemid the draft area to save the files from.
1245      * @param string $component the component for the file area to save into.
1246      * @param string $filearea the name of the file area to save into.
1247      */
1248     public function __construct($draftitemid, $component, $filearea, $text = null) {
1249         $this->draftitemid = $draftitemid;
1250         $this->component = $component;
1251         $this->filearea = $filearea;
1252         $this->value = $this->compute_value($draftitemid, $text);
1253     }
1255     /**
1256      * Compute the value that should be stored in the question_attempt_step_data
1257      * table. Contains a hash that (almost) uniquely encodes all the files.
1258      * @param int $draftitemid the draft file area itemid.
1259      * @param string $text optional content containing file links.
1260      */
1261     protected function compute_value($draftitemid, $text) {
1262         global $USER;
1264         $fs = get_file_storage();
1265         $usercontext = get_context_instance(CONTEXT_USER, $USER->id);
1267         $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
1268                 $draftitemid, 'sortorder, filepath, filename', false);
1270         $string = '';
1271         foreach ($files as $file) {
1272             $string .= $file->get_filepath() . $file->get_filename() . '|' .
1273                     $file->get_contenthash() . '|';
1274         }
1276         if ($string) {
1277             $hash = md5($string);
1278         } else {
1279             $hash = '';
1280         }
1282         if (is_null($text)) {
1283             return $hash;
1284         }
1286         // We add the file hash so a simple string comparison will say if the
1287         // files have been changed. First strip off any existing file hash.
1288         $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
1289         $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
1290         if ($hash) {
1291             $text .= '<!-- File hash: ' . $hash . ' -->';
1292         }
1293         return $text;
1294     }
1296     public function __toString() {
1297         return $this->value;
1298     }
1300     /**
1301      * Actually save the files.
1302      * @param integer $itemid the item id for the file area to save into.
1303      */
1304     public function save_files($itemid, $context) {
1305         file_save_draft_area_files($this->draftitemid, $context->id,
1306                 $this->component, $this->filearea, $itemid);
1307     }
1311 /**
1312  * This class represents a restriction on the set of question_usage ids to include
1313  * in a larger database query. Depending of the how you are going to restrict the
1314  * list of usages, construct an appropriate subclass.
1315  *
1316  * If $qubaids is an instance of this class, example usage might be
1317  *
1318  * SELECT qa.id, qa.maxmark
1319  * FROM $qubaids->from_question_attempts('qa')
1320  * WHERE $qubaids->where() AND qa.slot = 1
1321  *
1322  * @copyright  2010 The Open University
1323  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1324  */
1325 abstract class qubaid_condition {
1327     /**
1328      * @return string the SQL that needs to go in the FROM clause when trying
1329      * to select records from the 'question_attempts' table based on the
1330      * qubaid_condition.
1331      */
1332     public abstract function from_question_attempts($alias);
1334     /** @return string the SQL that needs to go in the where clause. */
1335     public abstract function where();
1337     /**
1338      * @return the params needed by a query that uses
1339      * {@link from_question_attempts()} and {@link where()}.
1340      */
1341     public abstract function from_where_params();
1343     /**
1344      * @return string SQL that can use used in a WHERE qubaid IN (...) query.
1345      * This method returns the "IN (...)" part.
1346      */
1347     public abstract function usage_id_in();
1349     /**
1350      * @return the params needed by a query that uses {@link usage_id_in()}.
1351      */
1352     public abstract function usage_id_in_params();
1356 /**
1357  * This class represents a restriction on the set of question_usage ids to include
1358  * in a larger database query based on an explicit list of ids.
1359  *
1360  * @copyright  2010 The Open University
1361  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1362  */
1363 class qubaid_list extends qubaid_condition {
1364     /** @var array of ids. */
1365     protected $qubaids;
1366     protected $columntotest = null;
1367     protected $params;
1369     /**
1370      * Constructor.
1371      * @param array $qubaids of question usage ids.
1372      */
1373     public function __construct(array $qubaids) {
1374         $this->qubaids = $qubaids;
1375     }
1377     public function from_question_attempts($alias) {
1378         $this->columntotest = $alias . '.questionusageid';
1379         return '{question_attempts} ' . $alias;
1380     }
1382     public function where() {
1383         global $DB;
1385         if (is_null($this->columntotest)) {
1386             throw new coding_exception('Must call from_question_attempts before where().');
1387         }
1388         if (empty($this->qubaids)) {
1389             $this->params = array();
1390             return '1 = 0';
1391         }
1393         return $this->columntotest . ' ' . $this->usage_id_in();
1394     }
1396     public function from_where_params() {
1397         return $this->params;
1398     }
1400     public function usage_id_in() {
1401         global $DB;
1403         if (empty($this->qubaids)) {
1404             $this->params = array();
1405             return '= 0';
1406         }
1407         list($where, $this->params) = $DB->get_in_or_equal(
1408                 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid');
1409         return $where;
1410     }
1412     public function usage_id_in_params() {
1413         return $this->params;
1414     }
1418 /**
1419  * This class represents a restriction on the set of question_usage ids to include
1420  * in a larger database query based on JOINing to some other tables.
1421  *
1422  * The general form of the query is something like
1423  *
1424  * SELECT qa.id, qa.maxmark
1425  * FROM $from
1426  * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
1427  * WHERE $where AND qa.slot = 1
1428  *
1429  * where $from, $usageidcolumn and $where are the arguments to the constructor.
1430  *
1431  * @copyright  2010 The Open University
1432  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1433  */
1434 class qubaid_join extends qubaid_condition {
1435     public $from;
1436     public $usageidcolumn;
1437     public $where;
1438     public $params;
1440     /**
1441      * Constructor. The meaning of the arguments is explained in the class comment.
1442      * @param string $from SQL fragemnt to go in the FROM clause.
1443      * @param string $usageidcolumn the column in $from that should be
1444      * made equal to the usageid column in the JOIN clause.
1445      * @param string $where SQL fragment to go in the where clause.
1446      * @param array $params required by the SQL. You must use named parameters.
1447      */
1448     public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
1449         $this->from = $from;
1450         $this->usageidcolumn = $usageidcolumn;
1451         $this->params = $params;
1452         if (empty($where)) {
1453             $where = '1 = 1';
1454         }
1455         $this->where = $where;
1456     }
1458     public function from_question_attempts($alias) {
1459         return "$this->from
1460                 JOIN {question_attempts} {$alias} ON " .
1461                         "{$alias}.questionusageid = $this->usageidcolumn";
1462     }
1464     public function where() {
1465         return $this->where;
1466     }
1468     public function from_where_params() {
1469         return $this->params;
1470     }
1472     public function usage_id_in() {
1473         return "IN (SELECT $this->usageidcolumn FROM $this->from WHERE $this->where)";
1474     }
1476     public function usage_id_in_params() {
1477         return $this->params;
1478     }