3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Code for loading and saving question attempts to and from the database.
22 * @subpackage questionengine
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
32 * This class controls the loading and saving of question engine data to and from
35 * @copyright 2009 The Open University
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 class question_engine_data_mapper {
40 * @var moodle_database normally points to global $DB, but I prefer not to
41 * use globals if I can help it.
46 * @param moodle_database $db a database connectoin. Defaults to global $DB.
48 public function __construct($db = null) {
58 * Store an entire {@link question_usage_by_activity} in the database,
59 * including all the question_attempts that comprise it.
60 * @param question_usage_by_activity $quba the usage to store.
62 public function insert_questions_usage_by_activity(question_usage_by_activity $quba) {
63 $record = new stdClass();
64 $record->contextid = $quba->get_owning_context()->id;
65 $record->component = addslashes($quba->get_owning_component());
66 $record->preferredbehaviour = addslashes($quba->get_preferred_behaviour());
68 $newid = $this->db->insert_record('question_usages', $record);
69 $quba->set_id_from_database($newid);
71 foreach ($quba->get_attempt_iterator() as $qa) {
72 $this->insert_question_attempt($qa);
77 * Store an entire {@link question_attempt} in the database,
78 * including all the question_attempt_steps that comprise it.
79 * @param question_attempt $qa the question attempt to store.
81 public function insert_question_attempt(question_attempt $qa) {
82 $record = new stdClass();
83 $record->questionusageid = $qa->get_usage_id();
84 $record->slot = $qa->get_slot();
85 $record->behaviour = addslashes($qa->get_behaviour_name());
86 $record->questionid = $qa->get_question()->id;
87 $record->maxmark = $qa->get_max_mark();
88 $record->minfraction = $qa->get_min_fraction();
89 $record->flagged = $qa->is_flagged();
90 $record->questionsummary = addslashes($qa->get_question_summary());
91 $record->rightanswer = addslashes($qa->get_right_answer_summary());
92 $record->responsesummary = addslashes($qa->get_response_summary());
93 $record->timemodified = time();
94 $record->id = $this->db->insert_record('question_attempts', $record);
96 foreach ($qa->get_step_iterator() as $seq => $step) {
97 $this->insert_question_attempt_step($step, $record->id, $seq);
102 * Store a {@link question_attempt_step} in the database.
103 * @param question_attempt_step $qa the step to store.
105 public function insert_question_attempt_step(question_attempt_step $step,
106 $questionattemptid, $seq) {
107 $record = new stdClass();
108 $record->questionattemptid = $questionattemptid;
109 $record->sequencenumber = $seq;
110 $record->state = addslashes('' . $step->get_state());
111 $record->fraction = $step->get_fraction();
112 $record->timecreated = $step->get_timecreated();
113 $record->userid = $step->get_user_id();
115 $record->id = $this->db->insert_record('question_attempt_steps', $record);
117 foreach ($step->get_all_data() as $name => $value) {
118 $data = new stdClass();
119 $data->attemptstepid = $record->id;
120 $data->name = addslashes($name);
121 $data->value = addslashes($value);
122 $this->db->insert_record('question_attempt_step_data', $data, false);
127 * Load a {@link question_attempt_step} from the database.
128 * @param int $stepid the id of the step to load.
129 * @param question_attempt_step the step that was loaded.
131 public function load_question_attempt_step($stepid) {
132 $records = $this->db->get_records_sql("
134 COALESCE(qasd.id, -1 * qas.id) AS id,
135 qas.id AS attemptstepid,
136 qas.questionattemptid,
145 FROM {question_attempt_steps} qas
146 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
150 ", array('stepid' => $stepid));
153 throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
156 return question_attempt_step::load_from_records($records, $stepid);
160 * Load a {@link question_attempt} from the database, including all its
162 * @param int $questionattemptid the id of the question attempt to load.
163 * @param question_attempt the question attempt that was loaded.
165 public function load_question_attempt($questionattemptid) {
166 $records = $this->db->get_records_sql("
168 COALESCE(qasd.id, -1 * qas.id) AS id,
170 quba.preferredbehaviour,
171 qa.id AS questionattemptid,
183 qas.id AS attemptstepid,
192 FROM {question_attempts qa
193 JOIN {question_usages} quba ON quba.id = qa.questionusageid
194 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
195 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
198 qa.id = :questionattemptid
202 ", array('questionattemptid' => $questionattemptid));
205 throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
208 $record = current($records);
209 return question_attempt::load_from_records($records, $questionattemptid,
210 new question_usage_null_observer(), $record->preferredbehaviour);
214 * Load a {@link question_usage_by_activity} from the database, including
215 * all its {@link question_attempt}s and all their steps.
216 * @param int $qubaid the id of the usage to load.
217 * @param question_usage_by_activity the usage that was loaded.
219 public function load_questions_usage_by_activity($qubaid) {
220 $records = $this->db->get_records_sql("
222 COALESCE(qasd.id, -1 * qas.id) AS id,
226 quba.preferredbehaviour,
227 qa.id AS questionattemptid,
239 qas.id AS attemptstepid,
248 FROM {question_usages} quba
249 LEFT JOIN {question_attempts} qa ON qa.questionusageid = quba.id
250 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
251 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
259 ", array('qubaid' => $qubaid));
262 throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
265 return question_usage_by_activity::load_from_records($records, $qubaid);
269 * Load information about the latest state of each question from the database.
271 * @param qubaid_condition $qubaids used to restrict which usages are included
272 * in the query. See {@link qubaid_condition}.
273 * @param array $slots A list of slots for the questions you want to konw about.
274 * @return array of records. See the SQL in this function to see the fields available.
276 public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
277 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
279 $records = $this->db->get_records_sql("
282 qa.id AS questionattemptid,
294 qas.id AS attemptstepid,
301 FROM {$qubaids->from_question_attempts('qa')}
302 JOIN {question_attempt_steps} qas ON
303 qas.id = {$this->latest_step_for_qa_subquery()}
306 {$qubaids->where()} AND
308 ", $params + $qubaids->from_where_params());
314 * Load summary information about the state of each question in a group of
315 * attempts. This is used, for example, by the quiz manual grading report,
316 * to show how many attempts at each question need to be graded.
318 * @param qubaid_condition $qubaids used to restrict which usages are included
319 * in the query. See {@link qubaid_condition}.
320 * @param array $slots A list of slots for the questions you want to konw about.
321 * @return array The array keys are slot,qestionid. The values are objects with
322 * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
323 * $manuallygraded and $all.
325 public function load_questions_usages_question_state_summary(qubaid_condition $qubaids, $slots) {
326 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
328 $rs = $this->db->get_recordset_sql("
334 {$this->full_states_to_summary_state_sql()}
336 COUNT(1) AS numattempts
338 FROM {$qubaids->from_question_attempts('qa')}
339 JOIN {question_attempt_steps} qas ON
340 qas.id = {$this->latest_step_for_qa_subquery()}
341 JOIN {question} q ON q.id = qa.questionid
344 {$qubaids->where()} AND
359 ", $params + $qubaids->from_where_params());
362 foreach ($rs as $row) {
363 $index = $row->slot . ',' . $row->questionid;
365 if (!array_key_exists($index, $results)) {
366 $res = new stdClass();
367 $res->slot = $row->slot;
368 $res->questionid = $row->questionid;
369 $res->name = $row->name;
370 $res->inprogress = 0;
371 $res->needsgrading = 0;
372 $res->autograded = 0;
373 $res->manuallygraded = 0;
375 $results[$index] = $res;
378 $results[$index]->{$row->summarystate} = $row->numattempts;
379 $results[$index]->all += $row->numattempts;
387 * Get a list of usage ids where the question with slot $slot, and optionally
388 * also with question id $questionid, is in summary state $summarystate. Also
389 * return the total count of such states.
391 * Only a subset of the ids can be returned by using $orderby, $limitfrom and
392 * $limitnum. A special value 'random' can be passed as $orderby, in which case
393 * $limitfrom is ignored.
395 * @param qubaid_condition $qubaids used to restrict which usages are included
396 * in the query. See {@link qubaid_condition}.
397 * @param int $slot The slot for the questions you want to konw about.
398 * @param int $questionid (optional) Only return attempts that were of this specific question.
399 * @param string $summarystate the summary state of interest, or 'all'.
400 * @param string $orderby the column to order by.
401 * @param array $params any params required by any of the SQL fragments.
402 * @param int $limitfrom implements paging of the results.
403 * Ignored if $orderby = random or $limitnum is null.
404 * @param int $limitnum implements paging of the results. null = all.
405 * @return array with two elements, an array of usage ids, and a count of the total number.
407 public function load_questions_usages_where_question_in_state(
408 qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
409 $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) {
413 $extrawhere .= ' AND qa.questionid = :questionid';
414 $params['questionid'] = $questionid;
416 if ($summarystate != 'all') {
417 list($test, $sparams) = $this->in_summary_state_test($summarystate);
418 $extrawhere .= ' AND qas.state ' . $test;
422 if ($orderby == 'random') {
424 } else if ($orderby) {
425 $sqlorderby = 'ORDER BY ' . $orderby;
430 // We always want the total count, as well as the partcular list of ids,
431 // based on the paging and sort order. Becuase the list of ids is never
432 // going to be too rediculously long. My worst-case scenario is
433 // 10,000 students in the coures, each doing 5 quiz attempts. That
434 // is a 50,000 element int => int array, which PHP seems to use 5MB
435 // memeory to store on a 64 bit server.
436 $params += $qubaids->from_where_params();
437 $params['slot'] = $slot;
438 $qubaids = $this->db->get_records_sql_menu("
443 FROM {$qubaids->from_question_attempts('qa')}
444 JOIN {question_attempt_steps} qas ON
445 qas.id = {$this->latest_step_for_qa_subquery()}
446 JOIN {question} q ON q.id = qa.questionid
449 {$qubaids->where()} AND
456 $qubaids = array_keys($qubaids);
457 $count = count($qubaids);
459 if ($orderby == 'random') {
464 if (!is_null($limitnum)) {
465 $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
468 return array($qubaids, $count);
472 * Load a {@link question_usage_by_activity} from the database, including
473 * all its {@link question_attempt}s and all their steps.
474 * @param qubaid_condition $qubaids used to restrict which usages are included
475 * in the query. See {@link qubaid_condition}.
476 * @param array $slots if null, load info for all quesitions, otherwise only
477 * load the averages for the specified questions.
479 public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
480 if (!empty($slots)) {
481 list($slottest, $slotsparams) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
482 $slotwhere = " AND qa.slot $slottest";
488 list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
489 question_state::$gaveup,
490 question_state::$gradedwrong,
491 question_state::$gradedpartial,
492 question_state::$gradedright,
493 question_state::$mangaveup,
494 question_state::$mangrwrong,
495 question_state::$mangrpartial,
496 question_state::$mangrright), SQL_PARAMS_NAMED, 'st00');
498 return $this->db->get_records_sql("
501 AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
502 COUNT(1) AS numaveraged
504 FROM {$qubaids->from_question_attempts('qa')}
505 JOIN {question_attempt_steps} qas ON
506 qas.id = {$this->latest_step_for_qa_subquery()}
511 AND qas.state $statetest
516 ", $slotsparams + $stateparams + $qubaids->from_where_params());
520 * Load a {@link question_attempt} from the database, including all its
522 * @param int $questionid the question to load all the attempts fors.
523 * @param qubaid_condition $qubaids used to restrict which usages are included
524 * in the query. See {@link qubaid_condition}.
525 * @return array of question_attempts.
527 public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
530 $params = $qubaids->from_where_params();
531 $params['questionid'] = $questionid;
533 $records = $DB->get_records_sql("
535 COALESCE(qasd.id, -1 * qas.id) AS id,
537 quba.preferredbehaviour,
538 qa.id AS questionattemptid,
550 qas.id AS attemptstepid,
559 FROM {$qubaids->from_question_attempts('qa')}
560 JOIN {question_usages} quba ON quba.id = qa.questionusageid
561 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
562 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
565 {$qubaids->where()} AND
566 qa.questionid = :questionid
578 $questionattempts = array();
579 $record = current($records);
581 $questionattempts[$record->questionattemptid] =
582 question_attempt::load_from_records($records,
583 $record->questionattemptid, new question_usage_null_observer(),
584 $record->preferredbehaviour);
585 $record = current($records);
587 return $questionattempts;
591 * Update a question_usages row to refect any changes in a usage (but not
592 * any of its question_attempts.
593 * @param question_usage_by_activity $quba the usage that has changed.
595 public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
596 $record = new stdClass();
597 $record->id = $quba->get_id();
598 $record->contextid = $quba->get_owning_context()->id;
599 $record->component = addslashes($quba->get_owning_component());
600 $record->preferredbehaviour = addslashes($quba->get_preferred_behaviour());
602 $this->db->update_record('question_usages', $record);
606 * Update a question_attempts row to refect any changes in a question_attempt
607 * (but not any of its steps).
608 * @param question_attempt $qa the question attempt that has changed.
610 public function update_question_attempt(question_attempt $qa) {
611 $record = new stdClass();
612 $record->id = $qa->get_database_id();
613 $record->maxmark = $qa->get_max_mark();
614 $record->minfraction = $qa->get_min_fraction();
615 $record->flagged = $qa->is_flagged();
616 $record->questionsummary = addslashes($qa->get_question_summary());
617 $record->rightanswer = addslashes($qa->get_right_answer_summary());
618 $record->responsesummary = addslashes($qa->get_response_summary());
619 $record->timemodified = time();
621 $this->db->update_record('question_attempts', $record);
625 * Delete a question_usage_by_activity and all its associated
626 * {@link question_attempts} and {@link question_attempt_steps} from the
628 * @param string $where a where clause. Becuase of MySQL limitations, you
629 * must refer to {question_usages}.id in full like that.
630 * @param array $params values to substitute for placeholders in $where.
632 public function delete_questions_usage_by_activities($where, $params) {
633 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
635 FROM {question_attempts} qa
636 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
637 JOIN {question_usages} ON qa.questionusageid = {question_usages}.id
638 WHERE $where)", $params);
639 $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
641 FROM {question_attempts} qa
642 JOIN {question_usages} ON qa.questionusageid = {question_usages}.id
643 WHERE $where)", $params);
644 $this->db->delete_records_select('question_attempts', "questionusageid IN (
646 FROM {question_usages}
647 WHERE $where)", $params);
648 $this->db->delete_records_select('question_usages', $where, $params);
652 * Delete all the steps for a question attempt.
653 * @param int $qaids question_attempt id.
655 public function delete_steps_for_question_attempts($qaids) {
659 list($test, $params) = $this->db->get_in_or_equal($qaids);
660 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
662 FROM {question_attempt_steps} qas
663 WHERE questionattemptid $test)", $params);
664 $this->db->delete_records_select('question_attempt_steps', 'questionattemptid ' . $test, $params);
668 * Delete all the previews for a given question.
669 * @param int $questionid question id.
671 public function delete_previews($questionid) {
672 $previews = $this->db->get_records_sql_menu("
673 SELECT DISTINCT quba.id, 1
674 FROM {question_usages} quba
675 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
676 WHERE quba.component = 'core_question_preview' AND
677 qa.questionid = ?", array($questionid));
678 if (empty($previews)) {
681 list($test, $params) = $this->db->get_in_or_equal(array_keys($previews));
682 $this->delete_questions_usage_by_activities('question_usages.id ' . $test, $params);
686 * Update the flagged state of a question in the database.
687 * @param int $qubaid the question usage id.
688 * @param int $questionid the question id.
689 * @param int $sessionid the question_attempt id.
690 * @param bool $newstate the new state of the flag. true = flagged.
692 public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
693 if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
694 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
695 throw new moodle_exception('errorsavingflags', 'question');
698 $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
702 * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
703 * column to a summary state. Use this like
704 * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
705 * @param string SQL fragment.
707 protected function full_states_to_summary_state_sql() {
709 foreach (question_state::get_all() as $state) {
710 $sql .= "WHEN '$state' THEN '{$state->get_summary_state()}'\n";
716 * Get the SQL needed to test that question_attempt_steps.state is in a
717 * state corresponding to $summarystate.
718 * @param string $summarystate one of
719 * inprogress, needsgrading, manuallygraded or autograded
720 * @param bool $equal if false, do a NOT IN test. Default true.
721 * @return string SQL fragment.
723 public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
724 $states = question_state::get_all_for_summary_state($summarystate);
725 return $this->db->get_in_or_equal($states, SQL_PARAMS_NAMED, $prefix . '00', $equal);
729 * Change the maxmark for the question_attempt with number in usage $slot
730 * for all the specified question_attempts.
731 * @param qubaid_condition $qubaids Selects which usages are updated.
732 * @param int $slot the number is usage to affect.
733 * @param number $newmaxmark the new max mark to set.
735 public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
736 $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
737 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
738 $qubaids->usage_id_in_params() + array('slot' => $slot));
742 * Return a subquery that computes the sum of the marks for all the questions
743 * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
746 * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
749 * @param string $qubaid SQL fragment that controls which usage is summed.
750 * This will normally be the name of a column in the outer query. Not that this
751 * SQL fragment must not contain any placeholders.
752 * @return string SQL code for the subquery.
754 public function sum_usage_marks_subquery($qubaid) {
755 return "SELECT SUM(qa.maxmark * qas.fraction)
756 FROM {question_attempts} qa
758 SELECT summarks_qa.id AS questionattemptid, MAX(summarks_qas.id) AS latestid
759 FROM {question_attempt_steps} summarks_qas
760 JOIN {question_attempts} summarks_qa ON summarks_qa.id = summarks_qas.questionattemptid
761 WHERE summarks_qa.questionusageid = $qubaid
762 GROUP BY summarks_qa.id
763 ) lateststepid ON lateststepid.questionattemptid = qa.id
764 JOIN {question_attempt_steps} qas ON qas.id = lateststepid.latestid
765 WHERE qa.questionusageid = $qubaid
766 HAVING COUNT(CASE WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1 ELSE NULL END) = 0";
769 public function question_attempt_latest_state_view($alias) {
772 {$alias}qa.id AS questionattemptid,
773 {$alias}qa.questionusageid,
775 {$alias}qa.behaviour,
776 {$alias}qa.questionid,
778 {$alias}qa.minfraction,
780 {$alias}qa.questionsummary,
781 {$alias}qa.rightanswer,
782 {$alias}qa.responsesummary,
783 {$alias}qa.timemodified,
784 {$alias}qas.id AS attemptstepid,
785 {$alias}qas.sequencenumber,
787 {$alias}qas.fraction,
788 {$alias}qas.timecreated,
791 FROM {question_attempts} {$alias}qa
792 JOIN {question_attempt_steps} {$alias}qas ON
793 {$alias}qas.id = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
797 protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
800 FROM {question_attempt_steps}
801 WHERE questionattemptid = $questionattemptid
806 * @param array $questionids of question ids.
807 * @param qubaid_condition $qubaids ids of the usages to consider.
808 * @return boolean whether any of these questions are being used by any of
811 public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
812 list($test, $params) = $this->db->get_in_or_equal($questionids);
813 return $this->db->record_exists_select('question_attempts',
814 'questionid ' . $test . ' AND questionusageid ' .
815 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
820 * Implementation of the unit of work pattern for the question engine.
822 * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
823 * changes to a {@link question_usage_by_activity}, and its constituent parts,
824 * so that the changes can be saved to the database when {@link save()} is called.
826 * @copyright 2009 The Open University
827 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
829 class question_engine_unit_of_work implements question_usage_observer {
830 /** @var question_usage_by_activity the usage being tracked. */
833 /** @var boolean whether any of the fields of the usage have been changed. */
834 protected $modified = false;
837 * @var array list of number in usage => {@link question_attempt}s that
838 * were already in the usage, and which have been modified.
840 protected $attemptsmodified = array();
843 * @var array list of number in usage => {@link question_attempt}s that
844 * have been added to the usage.
846 protected $attemptsadded = array();
849 * @var array list of question attempt ids to delete the steps for, before
850 * inserting new steps.
852 protected $attemptstodeletestepsfor = array();
855 * @var array list of array(question_attempt_step, question_attempt id, seq number)
856 * of steps that have been added to question attempts in this usage.
858 protected $stepsadded = array();
862 * @param question_usage_by_activity $quba the usage to track.
864 public function __construct(question_usage_by_activity $quba) {
868 public function notify_modified() {
869 $this->modified = true;
872 public function notify_attempt_modified(question_attempt $qa) {
873 $no = $qa->get_slot();
874 if (!array_key_exists($no, $this->attemptsadded)) {
875 $this->attemptsmodified[$no] = $qa;
879 public function notify_attempt_added(question_attempt $qa) {
880 $this->attemptsadded[$qa->get_slot()] = $qa;
883 public function notify_delete_attempt_steps(question_attempt $qa) {
885 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
889 $qaid = $qa->get_database_id();
890 foreach ($this->stepsadded as $key => $stepinfo) {
891 if ($stepinfo[1] == $qaid) {
892 unset($this->stepsadded[$key]);
896 $this->attemptstodeletestepsfor[$qaid] = 1;
899 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
900 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
903 $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
907 * Write all the changes we have recorded to the database.
908 * @param question_engine_data_mapper $dm the mapper to use to update the database.
910 public function save(question_engine_data_mapper $dm) {
911 $dm->delete_steps_for_question_attempts(array_keys($this->attemptstodeletestepsfor));
912 foreach ($this->stepsadded as $stepinfo) {
913 list($step, $questionattemptid, $seq) = $stepinfo;
914 $dm->insert_question_attempt_step($step, $questionattemptid, $seq);
916 foreach ($this->attemptsadded as $qa) {
917 $dm->insert_question_attempt($qa);
919 foreach ($this->attemptsmodified as $qa) {
920 $dm->update_question_attempt($qa);
922 if ($this->modified) {
923 $dm->update_questions_usage_by_activity($this->quba);
930 * This class represents a restriction on the set of question_usage ids to include
931 * in a larger database query. Depending of the how you are going to restrict the
932 * list of usages, construct an appropriate subclass.
934 * If $qubaids is an instance of this class, example usage might be
936 * SELECT qa.id, qa.maxmark
937 * FROM $qubaids->from_question_attempts('qa')
938 * WHERE $qubaids->where() AND qa.slot = 1
940 * @copyright 2010 The Open University
941 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
943 abstract class qubaid_condition {
946 * @return string the SQL that needs to go in the FROM clause when trying
947 * to select records from the 'question_attempts' table based on the
950 public abstract function from_question_attempts($alias);
952 /** @return string the SQL that needs to go in the where clause. */
953 public abstract function where();
956 * @return the params needed by a query that uses
957 * {@link from_question_attempts()} and {@link where()}.
959 public abstract function from_where_params();
962 * @return string SQL that can use used in a WHERE qubaid IN (...) query.
963 * This method returns the "IN (...)" part.
965 public abstract function usage_id_in();
968 * @return the params needed by a query that uses {@link usage_id_in()}.
970 public abstract function usage_id_in_params();
975 * This class represents a restriction on the set of question_usage ids to include
976 * in a larger database query based on an explicit list of ids.
978 * @copyright 2010 The Open University
979 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
981 class qubaid_list extends qubaid_condition {
982 /** @var array of ids. */
984 protected $columntotest = null;
989 * @param array $qubaids of question usage ids.
991 public function __construct(array $qubaids) {
992 $this->qubaids = $qubaids;
995 public function from_question_attempts($alias) {
996 $this->columntotest = $alias . '.questionusageid';
997 return '{question_attempts} ' . $alias;
1000 public function where() {
1003 if (is_null($this->columntotest)) {
1004 throw new coding_exception('Must call from_question_attempts before where().');
1006 if (empty($this->qubaids)) {
1007 $this->params = array();
1010 list($where, $this->params) = $DB->get_in_or_equal($this->qubaids, SQL_PARAMS_NAMED, 'qubaid0000');
1012 return $this->columntotest . ' ' . $this->usage_id_in();
1015 public function from_where_params() {
1016 return $this->params;
1019 public function usage_id_in() {
1022 if (empty($this->qubaids)) {
1025 list($where, $this->params) = $DB->get_in_or_equal($this->qubaids, SQL_PARAMS_NAMED, 'qubaid0000');
1029 public function usage_id_in_params() {
1030 return $this->params;
1036 * This class represents a restriction on the set of question_usage ids to include
1037 * in a larger database query based on JOINing to some other tables.
1039 * The general form of the query is something like
1041 * SELECT qa.id, qa.maxmark
1043 * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
1044 * WHERE $where AND qa.slot = 1
1046 * where $from, $usageidcolumn and $where are the arguments to the constructor.
1048 * @copyright 2010 The Open University
1049 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1051 class qubaid_join extends qubaid_condition {
1053 public $usageidcolumn;
1058 * Constructor. The meaning of the arguments is explained in the class comment.
1059 * @param string $from SQL fragemnt to go in the FROM clause.
1060 * @param string $usageidcolumn the column in $from that should be
1061 * made equal to the usageid column in the JOIN clause.
1062 * @param string $where SQL fragment to go in the where clause.
1063 * @param array $params required by the SQL. You must use named parameters.
1065 public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
1066 $this->from = $from;
1067 $this->usageidcolumn = $usageidcolumn;
1068 $this->params = $params;
1069 if (empty($where)) {
1072 $this->where = $where;
1075 public function from_question_attempts($alias) {
1077 JOIN {question_attempts} {$alias} ON " .
1078 "{$alias}.questionusageid = $this->usageidcolumn";
1081 public function where() {
1082 return $this->where;
1085 public function from_where_params() {
1086 return $this->params;
1089 public function usage_id_in() {
1090 return "IN (SELECT $this->usageidcolumn FROM $this->from WHERE $this->where)";
1093 public function usage_id_in_params() {
1094 return $this->params;