| 1 | efrain | 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/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Code for loading and saving question attempts to and from the database.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * Note that many of the methods of this class should be considered private to
 | 
        
           |  |  | 21 |  * the question engine. They should be accessed through the
 | 
        
           |  |  | 22 |  * {@link question_engine} class. For example, you should call
 | 
        
           |  |  | 23 |  * {@link question_engine::save_questions_usage_by_activity()} rather than
 | 
        
           |  |  | 24 |  * {@link question_engine_data_mapper::insert_questions_usage_by_activity()}.
 | 
        
           |  |  | 25 |  * The exception to this is some of the reporting methods, like
 | 
        
           |  |  | 26 |  * {@link question_engine_data_mapper::load_attempts_at_question()}.
 | 
        
           |  |  | 27 |  *
 | 
        
           |  |  | 28 |  * A note for future reference. This code is pretty efficient but there are some
 | 
        
           |  |  | 29 |  * potential optimisations that could be contemplated, at the cost of making the
 | 
        
           |  |  | 30 |  * code more complex:
 | 
        
           |  |  | 31 |  *
 | 
        
           |  |  | 32 |  * 1. (This is probably not worth doing.) In the unit-of-work save method, we
 | 
        
           |  |  | 33 |  *    could get all the ids for steps due to be deleted or modified,
 | 
        
           |  |  | 34 |  *    and delete all the question_attempt_step_data for all of those steps in one
 | 
        
           |  |  | 35 |  *    query. That would save one DB query for each ->stepsupdated. However that number
 | 
        
           |  |  | 36 |  *    is 0 except when re-grading, and when regrading, there are many more inserts
 | 
        
           |  |  | 37 |  *    into question_attempt_step_data than deletes, so it is really hardly worth it.
 | 
        
           |  |  | 38 |  *
 | 
        
           |  |  | 39 |  * @package    core_question
 | 
        
           |  |  | 40 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 41 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 42 |  */
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 44 |   | 
        
           |  |  | 45 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 46 |   | 
        
           |  |  | 47 |   | 
        
           |  |  | 48 | /**
 | 
        
           |  |  | 49 |  * This class controls the loading and saving of question engine data to and from
 | 
        
           |  |  | 50 |  * the database.
 | 
        
           |  |  | 51 |  *
 | 
        
           |  |  | 52 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 53 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 54 |  */
 | 
        
           |  |  | 55 | class question_engine_data_mapper {
 | 
        
           |  |  | 56 |     /**
 | 
        
           |  |  | 57 |      * @var moodle_database normally points to global $DB, but I prefer not to
 | 
        
           |  |  | 58 |      * use globals if I can help it.
 | 
        
           |  |  | 59 |      */
 | 
        
           |  |  | 60 |     protected $db;
 | 
        
           |  |  | 61 |   | 
        
           |  |  | 62 |     /**
 | 
        
           |  |  | 63 |      * @param moodle_database $db a database connectoin. Defaults to global $DB.
 | 
        
           |  |  | 64 |      */
 | 
        
           | 1441 | ariadna | 65 |     public function __construct(?moodle_database $db = null) {
 | 
        
           | 1 | efrain | 66 |         if (is_null($db)) {
 | 
        
           |  |  | 67 |             global $DB;
 | 
        
           |  |  | 68 |             $this->db = $DB;
 | 
        
           |  |  | 69 |         } else {
 | 
        
           |  |  | 70 |             $this->db = $db;
 | 
        
           |  |  | 71 |         }
 | 
        
           |  |  | 72 |     }
 | 
        
           |  |  | 73 |   | 
        
           |  |  | 74 |     /**
 | 
        
           |  |  | 75 |      * Store an entire {@link question_usage_by_activity} in the database,
 | 
        
           |  |  | 76 |      * including all the question_attempts that comprise it.
 | 
        
           |  |  | 77 |      *
 | 
        
           |  |  | 78 |      * You should not call this method directly. You should use
 | 
        
           |  |  | 79 |      * @link question_engine::save_questions_usage_by_activity()}.
 | 
        
           |  |  | 80 |      *
 | 
        
           |  |  | 81 |      * @param question_usage_by_activity $quba the usage to store.
 | 
        
           |  |  | 82 |      */
 | 
        
           |  |  | 83 |     public function insert_questions_usage_by_activity(question_usage_by_activity $quba) {
 | 
        
           |  |  | 84 |         $record = new stdClass();
 | 
        
           |  |  | 85 |         $record->contextid = $quba->get_owning_context()->id;
 | 
        
           |  |  | 86 |         $record->component = $quba->get_owning_component();
 | 
        
           |  |  | 87 |         $record->preferredbehaviour = $quba->get_preferred_behaviour();
 | 
        
           |  |  | 88 |   | 
        
           |  |  | 89 |         $newid = $this->db->insert_record('question_usages', $record);
 | 
        
           |  |  | 90 |         $quba->set_id_from_database($newid);
 | 
        
           |  |  | 91 |   | 
        
           |  |  | 92 |         // Initially an array of array of question_attempt_step_objects.
 | 
        
           |  |  | 93 |         // Built as a nested array for efficiency, then flattened.
 | 
        
           |  |  | 94 |         $stepdata = array();
 | 
        
           |  |  | 95 |   | 
        
           |  |  | 96 |         foreach ($quba->get_attempt_iterator() as $qa) {
 | 
        
           |  |  | 97 |             $stepdata[] = $this->insert_question_attempt($qa, $quba->get_owning_context());
 | 
        
           |  |  | 98 |         }
 | 
        
           |  |  | 99 |   | 
        
           |  |  | 100 |         $this->insert_all_step_data($this->combine_step_data($stepdata));
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 |         $quba->set_observer(new question_engine_unit_of_work($quba));
 | 
        
           |  |  | 103 |     }
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 105 |     /**
 | 
        
           |  |  | 106 |      * Store an entire {@link question_attempt} in the database,
 | 
        
           |  |  | 107 |      * including all the question_attempt_steps that comprise it.
 | 
        
           |  |  | 108 |      *
 | 
        
           |  |  | 109 |      * You should not call this method directly. You should use
 | 
        
           |  |  | 110 |      * @link question_engine::save_questions_usage_by_activity()}.
 | 
        
           |  |  | 111 |      *
 | 
        
           |  |  | 112 |      * @param question_attempt $qa the question attempt to store.
 | 
        
           |  |  | 113 |      * @param context $context the context of the owning question_usage_by_activity.
 | 
        
           |  |  | 114 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 115 |      */
 | 
        
           |  |  | 116 |     public function insert_question_attempt(question_attempt $qa, $context) {
 | 
        
           |  |  | 117 |         $record = new stdClass();
 | 
        
           |  |  | 118 |         $record->questionusageid = $qa->get_usage_id();
 | 
        
           |  |  | 119 |         $record->slot = $qa->get_slot();
 | 
        
           |  |  | 120 |         $record->behaviour = $qa->get_behaviour_name();
 | 
        
           |  |  | 121 |         $record->questionid = $qa->get_question_id();
 | 
        
           |  |  | 122 |         $record->variant = $qa->get_variant();
 | 
        
           |  |  | 123 |         $record->maxmark = $qa->get_max_mark();
 | 
        
           |  |  | 124 |         $record->minfraction = $qa->get_min_fraction();
 | 
        
           |  |  | 125 |         $record->maxfraction = $qa->get_max_fraction();
 | 
        
           |  |  | 126 |         $record->flagged = $qa->is_flagged();
 | 
        
           |  |  | 127 |         $record->questionsummary = $qa->get_question_summary();
 | 
        
           |  |  | 128 |         if (core_text::strlen($record->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
 | 
        
           |  |  | 129 |             // It seems some people write very long quesions! MDL-30760
 | 
        
           |  |  | 130 |             $record->questionsummary = core_text::substr($record->questionsummary,
 | 
        
           |  |  | 131 |                     0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
 | 
        
           |  |  | 132 |         }
 | 
        
           |  |  | 133 |         $record->rightanswer = $qa->get_right_answer_summary();
 | 
        
           |  |  | 134 |         $record->responsesummary = $qa->get_response_summary();
 | 
        
           |  |  | 135 |         $record->timemodified = time();
 | 
        
           |  |  | 136 |         $record->id = $this->db->insert_record('question_attempts', $record);
 | 
        
           |  |  | 137 |         $qa->set_database_id($record->id);
 | 
        
           |  |  | 138 |   | 
        
           |  |  | 139 |         // Initially an array of array of question_attempt_step_objects.
 | 
        
           |  |  | 140 |         // Built as a nested array for efficiency, then flattened.
 | 
        
           |  |  | 141 |         $stepdata = array();
 | 
        
           |  |  | 142 |   | 
        
           |  |  | 143 |         foreach ($qa->get_step_iterator() as $seq => $step) {
 | 
        
           |  |  | 144 |             $stepdata[] = $this->insert_question_attempt_step($step, $record->id, $seq, $context);
 | 
        
           |  |  | 145 |         }
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |         return $this->combine_step_data($stepdata);
 | 
        
           |  |  | 148 |     }
 | 
        
           |  |  | 149 |   | 
        
           |  |  | 150 |     /**
 | 
        
           |  |  | 151 |      * Helper method used by insert_question_attempt_step and update_question_attempt_step
 | 
        
           |  |  | 152 |      *
 | 
        
           |  |  | 153 |      * @param question_attempt_step $step the step to store.
 | 
        
           |  |  | 154 |      * @param int $questionattemptid the question attept id this step belongs to.
 | 
        
           |  |  | 155 |      * @param int $seq the sequence number of this stop.
 | 
        
           |  |  | 156 |      * @return stdClass data to insert into the database.
 | 
        
           |  |  | 157 |      */
 | 
        
           |  |  | 158 |     protected function make_step_record(question_attempt_step $step, $questionattemptid, $seq) {
 | 
        
           |  |  | 159 |         $record = new stdClass();
 | 
        
           |  |  | 160 |         $record->questionattemptid = $questionattemptid;
 | 
        
           |  |  | 161 |         $record->sequencenumber = $seq;
 | 
        
           |  |  | 162 |         $record->state = $step->get_state()?->__toString();
 | 
        
           |  |  | 163 |         $record->fraction = $step->get_fraction();
 | 
        
           |  |  | 164 |         $record->timecreated = $step->get_timecreated();
 | 
        
           |  |  | 165 |         $record->userid = $step->get_user_id();
 | 
        
           |  |  | 166 |         return $record;
 | 
        
           |  |  | 167 |     }
 | 
        
           |  |  | 168 |   | 
        
           |  |  | 169 |     /**
 | 
        
           |  |  | 170 |      * Take an array of arrays, and flatten it, even if the outer array is empty.
 | 
        
           |  |  | 171 |      *
 | 
        
           |  |  | 172 |      * Only public so it can be called from the unit of work. Not part of the
 | 
        
           |  |  | 173 |      * public API of this class.
 | 
        
           |  |  | 174 |      *
 | 
        
           |  |  | 175 |      * @param array $stepdata array of zero or more arrays.
 | 
        
           |  |  | 176 |      * @return array made by concatenating all the separate arrays.
 | 
        
           |  |  | 177 |      */
 | 
        
           |  |  | 178 |     public function combine_step_data(array $stepdata): array {
 | 
        
           |  |  | 179 |         if (empty($stepdata)) {
 | 
        
           |  |  | 180 |             return [];
 | 
        
           |  |  | 181 |         }
 | 
        
           |  |  | 182 |         return call_user_func_array('array_merge', $stepdata);
 | 
        
           |  |  | 183 |     }
 | 
        
           |  |  | 184 |   | 
        
           |  |  | 185 |     /**
 | 
        
           |  |  | 186 |      * Helper method used by insert_question_attempt_step and update_question_attempt_step
 | 
        
           |  |  | 187 |      * @param question_attempt_step $step the step to store.
 | 
        
           |  |  | 188 |      * @param int $stepid the id of the step.
 | 
        
           |  |  | 189 |      * @param context $context the context of the owning question_usage_by_activity.
 | 
        
           |  |  | 190 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 191 |      */
 | 
        
           |  |  | 192 |     protected function prepare_step_data(question_attempt_step $step, $stepid, $context) {
 | 
        
           |  |  | 193 |         $rows = array();
 | 
        
           |  |  | 194 |         foreach ($step->get_all_data() as $name => $value) {
 | 
        
           |  |  | 195 |             if ($value instanceof question_file_saver) {
 | 
        
           |  |  | 196 |                 $value->save_files($stepid, $context);
 | 
        
           |  |  | 197 |             }
 | 
        
           |  |  | 198 |             if ($value instanceof question_response_files) {
 | 
        
           |  |  | 199 |                 $value = (string) $value;
 | 
        
           |  |  | 200 |             }
 | 
        
           |  |  | 201 |   | 
        
           |  |  | 202 |             $data = new stdClass();
 | 
        
           |  |  | 203 |             $data->attemptstepid = $stepid;
 | 
        
           |  |  | 204 |             $data->name = $name;
 | 
        
           |  |  | 205 |             $data->value = $value;
 | 
        
           |  |  | 206 |             $rows[] = $data;
 | 
        
           |  |  | 207 |         }
 | 
        
           |  |  | 208 |         return $rows;
 | 
        
           |  |  | 209 |     }
 | 
        
           |  |  | 210 |   | 
        
           |  |  | 211 |     /**
 | 
        
           |  |  | 212 |      * Insert a lot of records into question_attempt_step_data in one go.
 | 
        
           |  |  | 213 |      *
 | 
        
           |  |  | 214 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 215 |      *
 | 
        
           |  |  | 216 |      * @param array $rows the rows to insert.
 | 
        
           |  |  | 217 |      */
 | 
        
           |  |  | 218 |     public function insert_all_step_data(array $rows) {
 | 
        
           |  |  | 219 |         if (!$rows) {
 | 
        
           |  |  | 220 |             return;
 | 
        
           |  |  | 221 |         }
 | 
        
           |  |  | 222 |         $this->db->insert_records('question_attempt_step_data', $rows);
 | 
        
           |  |  | 223 |     }
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |     /**
 | 
        
           |  |  | 226 |      * Store a {@link question_attempt_step} in the database.
 | 
        
           |  |  | 227 |      *
 | 
        
           |  |  | 228 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 229 |      *
 | 
        
           |  |  | 230 |      * @param question_attempt_step $step the step to store.
 | 
        
           |  |  | 231 |      * @param int $questionattemptid the question attept id this step belongs to.
 | 
        
           |  |  | 232 |      * @param int $seq the sequence number of this stop.
 | 
        
           |  |  | 233 |      * @param context $context the context of the owning question_usage_by_activity.
 | 
        
           |  |  | 234 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 235 |      */
 | 
        
           |  |  | 236 |     public function insert_question_attempt_step(question_attempt_step $step,
 | 
        
           |  |  | 237 |             $questionattemptid, $seq, $context) {
 | 
        
           |  |  | 238 |   | 
        
           |  |  | 239 |         $record = $this->make_step_record($step, $questionattemptid, $seq);
 | 
        
           |  |  | 240 |         $record->id = $this->db->insert_record('question_attempt_steps', $record);
 | 
        
           |  |  | 241 |   | 
        
           |  |  | 242 |         return $this->prepare_step_data($step, $record->id, $context);
 | 
        
           |  |  | 243 |     }
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |     /**
 | 
        
           |  |  | 246 |      * Update a {@link question_attempt_step} in the database.
 | 
        
           |  |  | 247 |      *
 | 
        
           |  |  | 248 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 249 |      *
 | 
        
           |  |  | 250 |      * @param question_attempt_step $step the step to store.
 | 
        
           |  |  | 251 |      * @param int $questionattemptid the question attept id this step belongs to.
 | 
        
           |  |  | 252 |      * @param int $seq the sequence number of this stop.
 | 
        
           |  |  | 253 |      * @param context $context the context of the owning question_usage_by_activity.
 | 
        
           |  |  | 254 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 255 |      */
 | 
        
           |  |  | 256 |     public function update_question_attempt_step(question_attempt_step $step,
 | 
        
           |  |  | 257 |             $questionattemptid, $seq, $context) {
 | 
        
           |  |  | 258 |   | 
        
           |  |  | 259 |         $record = $this->make_step_record($step, $questionattemptid, $seq);
 | 
        
           |  |  | 260 |         $record->id = $step->get_id();
 | 
        
           |  |  | 261 |         $this->db->update_record('question_attempt_steps', $record);
 | 
        
           |  |  | 262 |   | 
        
           |  |  | 263 |         $this->db->delete_records('question_attempt_step_data',
 | 
        
           |  |  | 264 |                 array('attemptstepid' => $record->id));
 | 
        
           |  |  | 265 |         return $this->prepare_step_data($step, $record->id, $context);
 | 
        
           |  |  | 266 |     }
 | 
        
           |  |  | 267 |   | 
        
           |  |  | 268 |     /**
 | 
        
           |  |  | 269 |      * Store new metadata for an existing {@link question_attempt} in the database.
 | 
        
           |  |  | 270 |      *
 | 
        
           |  |  | 271 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 272 |      *
 | 
        
           |  |  | 273 |      * @param question_attempt $qa the question attempt to store meta data for.
 | 
        
           |  |  | 274 |      * @param array $names the names of the metadata variables to store.
 | 
        
           |  |  | 275 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 276 |      */
 | 
        
           |  |  | 277 |     public function insert_question_attempt_metadata(question_attempt $qa, array $names) {
 | 
        
           |  |  | 278 |         $firststep = $qa->get_step(0);
 | 
        
           |  |  | 279 |   | 
        
           |  |  | 280 |         $rows = array();
 | 
        
           |  |  | 281 |         foreach ($names as $name) {
 | 
        
           |  |  | 282 |             $data = new stdClass();
 | 
        
           |  |  | 283 |             $data->attemptstepid = $firststep->get_id();
 | 
        
           |  |  | 284 |             $data->name = ':_' . $name;
 | 
        
           |  |  | 285 |             $data->value = $firststep->get_metadata_var($name);
 | 
        
           |  |  | 286 |             $rows[] = $data;
 | 
        
           |  |  | 287 |         }
 | 
        
           |  |  | 288 |   | 
        
           |  |  | 289 |         return $rows;
 | 
        
           |  |  | 290 |     }
 | 
        
           |  |  | 291 |   | 
        
           |  |  | 292 |     /**
 | 
        
           |  |  | 293 |      * Updates existing metadata for an existing {@link question_attempt} in the database.
 | 
        
           |  |  | 294 |      *
 | 
        
           |  |  | 295 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 296 |      *
 | 
        
           |  |  | 297 |      * @param question_attempt $qa the question attempt to store meta data for.
 | 
        
           |  |  | 298 |      * @param array $names the names of the metadata variables to store.
 | 
        
           |  |  | 299 |      * @return array of question_attempt_step_data rows, that still need to be inserted.
 | 
        
           |  |  | 300 |      */
 | 
        
           |  |  | 301 |     public function update_question_attempt_metadata(question_attempt $qa, array $names) {
 | 
        
           |  |  | 302 |         global $DB;
 | 
        
           |  |  | 303 |         if (!$names) {
 | 
        
           |  |  | 304 |             return [];
 | 
        
           |  |  | 305 |         }
 | 
        
           |  |  | 306 |         // Use case-sensitive function sql_equal() and not get_in_or_equal().
 | 
        
           |  |  | 307 |         // Some databases may use case-insensitive collation, we don't want to delete 'X' instead of 'x'.
 | 
        
           |  |  | 308 |         $sqls = [];
 | 
        
           |  |  | 309 |         $params = [$qa->get_step(0)->get_id()];
 | 
        
           |  |  | 310 |         foreach ($names as $name) {
 | 
        
           |  |  | 311 |             $sqls[] = $DB->sql_equal('name', '?');
 | 
        
           |  |  | 312 |             $params[] = $name;
 | 
        
           |  |  | 313 |         }
 | 
        
           |  |  | 314 |         $DB->delete_records_select('question_attempt_step_data',
 | 
        
           |  |  | 315 |             'attemptstepid = ? AND (' . join(' OR ', $sqls) . ')', $params);
 | 
        
           |  |  | 316 |         return $this->insert_question_attempt_metadata($qa, $names);
 | 
        
           |  |  | 317 |     }
 | 
        
           |  |  | 318 |   | 
        
           |  |  | 319 |     /**
 | 
        
           |  |  | 320 |      * Load a {@link question_attempt_step} from the database.
 | 
        
           |  |  | 321 |      *
 | 
        
           |  |  | 322 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 323 |      *
 | 
        
           |  |  | 324 |      * @param int $stepid the id of the step to load.
 | 
        
           |  |  | 325 |      * @return question_attempt_step the step that was loaded.
 | 
        
           |  |  | 326 |      */
 | 
        
           |  |  | 327 |     public function load_question_attempt_step($stepid) {
 | 
        
           |  |  | 328 |         $records = $this->db->get_recordset_sql("
 | 
        
           |  |  | 329 | SELECT
 | 
        
           |  |  | 330 |     quba.contextid,
 | 
        
           |  |  | 331 |     COALESCE(q.qtype, 'missingtype') AS qtype,
 | 
        
           |  |  | 332 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 333 |     qas.questionattemptid,
 | 
        
           |  |  | 334 |     qas.sequencenumber,
 | 
        
           |  |  | 335 |     qas.state,
 | 
        
           |  |  | 336 |     qas.fraction,
 | 
        
           |  |  | 337 |     qas.timecreated,
 | 
        
           |  |  | 338 |     qas.userid,
 | 
        
           |  |  | 339 |     qasd.name,
 | 
        
           |  |  | 340 |     qasd.value
 | 
        
           |  |  | 341 |   | 
        
           |  |  | 342 | FROM      {question_attempt_steps}     qas
 | 
        
           |  |  | 343 | JOIN      {question_attempts}          qa   ON qa.id              = qas.questionattemptid
 | 
        
           |  |  | 344 | JOIN      {question_usages}            quba ON quba.id            = qa.questionusageid
 | 
        
           |  |  | 345 | LEFT JOIN {question}                   q    ON q.id               = qa.questionid
 | 
        
           |  |  | 346 | LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 | 
        
           |  |  | 347 |   | 
        
           |  |  | 348 | WHERE
 | 
        
           |  |  | 349 |     qas.id = :stepid
 | 
        
           |  |  | 350 |         ", array('stepid' => $stepid));
 | 
        
           |  |  | 351 |   | 
        
           |  |  | 352 |         if (!$records->valid()) {
 | 
        
           |  |  | 353 |             throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
 | 
        
           |  |  | 354 |         }
 | 
        
           |  |  | 355 |   | 
        
           |  |  | 356 |         $step = question_attempt_step::load_from_records($records, $stepid);
 | 
        
           |  |  | 357 |         $records->close();
 | 
        
           |  |  | 358 |   | 
        
           |  |  | 359 |         return $step;
 | 
        
           |  |  | 360 |     }
 | 
        
           |  |  | 361 |   | 
        
           |  |  | 362 |     /**
 | 
        
           |  |  | 363 |      * Load a {@link question_attempt} from the database, including all its
 | 
        
           |  |  | 364 |      * steps.
 | 
        
           |  |  | 365 |      *
 | 
        
           |  |  | 366 |      * Normally, you should use {@link question_engine::load_questions_usage_by_activity()}
 | 
        
           |  |  | 367 |      * but there may be rare occasions where for performance reasons, you only
 | 
        
           |  |  | 368 |      * wish to load one qa, in which case you may call this method.
 | 
        
           |  |  | 369 |      *
 | 
        
           |  |  | 370 |      * @param int $questionattemptid the id of the question attempt to load.
 | 
        
           |  |  | 371 |      * @return question_attempt the question attempt that was loaded.
 | 
        
           |  |  | 372 |      */
 | 
        
           |  |  | 373 |     public function load_question_attempt($questionattemptid) {
 | 
        
           |  |  | 374 |         $records = $this->db->get_recordset_sql("
 | 
        
           |  |  | 375 | SELECT
 | 
        
           |  |  | 376 |     quba.contextid,
 | 
        
           |  |  | 377 |     quba.preferredbehaviour,
 | 
        
           |  |  | 378 |     qa.id AS questionattemptid,
 | 
        
           |  |  | 379 |     qa.questionusageid,
 | 
        
           |  |  | 380 |     qa.slot,
 | 
        
           |  |  | 381 |     qa.behaviour,
 | 
        
           |  |  | 382 |     qa.questionid,
 | 
        
           |  |  | 383 |     qa.variant,
 | 
        
           |  |  | 384 |     qa.maxmark,
 | 
        
           |  |  | 385 |     qa.minfraction,
 | 
        
           |  |  | 386 |     qa.maxfraction,
 | 
        
           |  |  | 387 |     qa.flagged,
 | 
        
           |  |  | 388 |     qa.questionsummary,
 | 
        
           |  |  | 389 |     qa.rightanswer,
 | 
        
           |  |  | 390 |     qa.responsesummary,
 | 
        
           |  |  | 391 |     qa.timemodified,
 | 
        
           |  |  | 392 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 393 |     qas.sequencenumber,
 | 
        
           |  |  | 394 |     qas.state,
 | 
        
           |  |  | 395 |     qas.fraction,
 | 
        
           |  |  | 396 |     qas.timecreated,
 | 
        
           |  |  | 397 |     qas.userid,
 | 
        
           |  |  | 398 |     qasd.name,
 | 
        
           |  |  | 399 |     qasd.value
 | 
        
           |  |  | 400 |   | 
        
           |  |  | 401 | FROM      {question_attempts}          qa
 | 
        
           |  |  | 402 | JOIN      {question_usages}            quba ON quba.id               = qa.questionusageid
 | 
        
           |  |  | 403 | LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 404 | LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 | 
        
           |  |  | 405 |   | 
        
           |  |  | 406 | WHERE
 | 
        
           |  |  | 407 |     qa.id = :questionattemptid
 | 
        
           |  |  | 408 |   | 
        
           |  |  | 409 | ORDER BY
 | 
        
           |  |  | 410 |     qas.sequencenumber
 | 
        
           |  |  | 411 |         ", array('questionattemptid' => $questionattemptid));
 | 
        
           |  |  | 412 |   | 
        
           |  |  | 413 |         if (!$records->valid()) {
 | 
        
           |  |  | 414 |             throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
 | 
        
           |  |  | 415 |         }
 | 
        
           |  |  | 416 |   | 
        
           |  |  | 417 |         $record = $records->current();
 | 
        
           |  |  | 418 |         $qa = question_attempt::load_from_records($records, $questionattemptid,
 | 
        
           |  |  | 419 |                 new question_usage_null_observer(), $record->preferredbehaviour);
 | 
        
           |  |  | 420 |         $records->close();
 | 
        
           |  |  | 421 |   | 
        
           |  |  | 422 |         return $qa;
 | 
        
           |  |  | 423 |     }
 | 
        
           |  |  | 424 |   | 
        
           |  |  | 425 |     /**
 | 
        
           |  |  | 426 |      * Load a {@link question_usage_by_activity} from the database, including
 | 
        
           |  |  | 427 |      * all its {@link question_attempt}s and all their steps.
 | 
        
           |  |  | 428 |      *
 | 
        
           |  |  | 429 |      * You should call {@link question_engine::load_questions_usage_by_activity()}
 | 
        
           |  |  | 430 |      * rather than calling this method directly.
 | 
        
           |  |  | 431 |      *
 | 
        
           |  |  | 432 |      * @param int $qubaid the id of the usage to load.
 | 
        
           |  |  | 433 |      * @return question_usage_by_activity the usage that was loaded.
 | 
        
           |  |  | 434 |      */
 | 
        
           |  |  | 435 |     public function load_questions_usage_by_activity($qubaid) {
 | 
        
           |  |  | 436 |         $records = $this->db->get_recordset_sql("
 | 
        
           |  |  | 437 | SELECT
 | 
        
           |  |  | 438 |     quba.id AS qubaid,
 | 
        
           |  |  | 439 |     quba.contextid,
 | 
        
           |  |  | 440 |     quba.component,
 | 
        
           |  |  | 441 |     quba.preferredbehaviour,
 | 
        
           |  |  | 442 |     qa.id AS questionattemptid,
 | 
        
           |  |  | 443 |     qa.questionusageid,
 | 
        
           |  |  | 444 |     qa.slot,
 | 
        
           |  |  | 445 |     qa.behaviour,
 | 
        
           |  |  | 446 |     qa.questionid,
 | 
        
           |  |  | 447 |     qa.variant,
 | 
        
           |  |  | 448 |     qa.maxmark,
 | 
        
           |  |  | 449 |     qa.minfraction,
 | 
        
           |  |  | 450 |     qa.maxfraction,
 | 
        
           |  |  | 451 |     qa.flagged,
 | 
        
           |  |  | 452 |     qa.questionsummary,
 | 
        
           |  |  | 453 |     qa.rightanswer,
 | 
        
           |  |  | 454 |     qa.responsesummary,
 | 
        
           |  |  | 455 |     qa.timemodified,
 | 
        
           |  |  | 456 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 457 |     qas.sequencenumber,
 | 
        
           |  |  | 458 |     qas.state,
 | 
        
           |  |  | 459 |     qas.fraction,
 | 
        
           |  |  | 460 |     qas.timecreated,
 | 
        
           |  |  | 461 |     qas.userid,
 | 
        
           |  |  | 462 |     qasd.name,
 | 
        
           |  |  | 463 |     qasd.value
 | 
        
           |  |  | 464 |   | 
        
           |  |  | 465 | FROM      {question_usages}            quba
 | 
        
           |  |  | 466 | LEFT JOIN {question_attempts}          qa   ON qa.questionusageid    = quba.id
 | 
        
           |  |  | 467 | LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 468 | LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 | 
        
           |  |  | 469 |   | 
        
           |  |  | 470 | WHERE
 | 
        
           |  |  | 471 |     quba.id = :qubaid
 | 
        
           |  |  | 472 |   | 
        
           |  |  | 473 | ORDER BY
 | 
        
           |  |  | 474 |     qa.slot,
 | 
        
           |  |  | 475 |     qas.sequencenumber
 | 
        
           |  |  | 476 |     ", array('qubaid' => $qubaid));
 | 
        
           |  |  | 477 |   | 
        
           |  |  | 478 |         if (!$records->valid()) {
 | 
        
           |  |  | 479 |             throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
 | 
        
           |  |  | 480 |         }
 | 
        
           |  |  | 481 |   | 
        
           |  |  | 482 |         $quba = question_usage_by_activity::load_from_records($records, $qubaid);
 | 
        
           |  |  | 483 |         $records->close();
 | 
        
           |  |  | 484 |   | 
        
           |  |  | 485 |         return $quba;
 | 
        
           |  |  | 486 |     }
 | 
        
           |  |  | 487 |   | 
        
           |  |  | 488 |     /**
 | 
        
           |  |  | 489 |      * Load all {@link question_usage_by_activity} from the database for one qubaid_condition
 | 
        
           |  |  | 490 |      * Include all its {@link question_attempt}s and all their steps.
 | 
        
           |  |  | 491 |      *
 | 
        
           |  |  | 492 |      * This method may be called publicly.
 | 
        
           |  |  | 493 |      *
 | 
        
           |  |  | 494 |      * @param qubaid_condition $qubaids the condition that tells us which usages to load.
 | 
        
           |  |  | 495 |      * @return question_usage_by_activity[] the usages that were loaded.
 | 
        
           |  |  | 496 |      */
 | 
        
           |  |  | 497 |     public function load_questions_usages_by_activity($qubaids) {
 | 
        
           |  |  | 498 |         $records = $this->db->get_recordset_sql("
 | 
        
           |  |  | 499 | SELECT
 | 
        
           |  |  | 500 |     quba.id AS qubaid,
 | 
        
           |  |  | 501 |     quba.contextid,
 | 
        
           |  |  | 502 |     quba.component,
 | 
        
           |  |  | 503 |     quba.preferredbehaviour,
 | 
        
           |  |  | 504 |     qa.id AS questionattemptid,
 | 
        
           |  |  | 505 |     qa.questionusageid,
 | 
        
           |  |  | 506 |     qa.slot,
 | 
        
           |  |  | 507 |     qa.behaviour,
 | 
        
           |  |  | 508 |     qa.questionid,
 | 
        
           |  |  | 509 |     qa.variant,
 | 
        
           |  |  | 510 |     qa.maxmark,
 | 
        
           |  |  | 511 |     qa.minfraction,
 | 
        
           |  |  | 512 |     qa.maxfraction,
 | 
        
           |  |  | 513 |     qa.flagged,
 | 
        
           |  |  | 514 |     qa.questionsummary,
 | 
        
           |  |  | 515 |     qa.rightanswer,
 | 
        
           |  |  | 516 |     qa.responsesummary,
 | 
        
           |  |  | 517 |     qa.timemodified,
 | 
        
           |  |  | 518 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 519 |     qas.sequencenumber,
 | 
        
           |  |  | 520 |     qas.state,
 | 
        
           |  |  | 521 |     qas.fraction,
 | 
        
           |  |  | 522 |     qas.timecreated,
 | 
        
           |  |  | 523 |     qas.userid,
 | 
        
           |  |  | 524 |     qasd.name,
 | 
        
           |  |  | 525 |     qasd.value
 | 
        
           |  |  | 526 |   | 
        
           |  |  | 527 | FROM      {question_usages}            quba
 | 
        
           |  |  | 528 | LEFT JOIN {question_attempts}          qa   ON qa.questionusageid    = quba.id
 | 
        
           |  |  | 529 | LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 530 | LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 | 
        
           |  |  | 531 |   | 
        
           |  |  | 532 | WHERE
 | 
        
           |  |  | 533 |     quba.id {$qubaids->usage_id_in()}
 | 
        
           |  |  | 534 |   | 
        
           |  |  | 535 | ORDER BY
 | 
        
           |  |  | 536 |     quba.id,
 | 
        
           |  |  | 537 |     qa.slot,
 | 
        
           |  |  | 538 |     qas.sequencenumber
 | 
        
           |  |  | 539 |     ", $qubaids->usage_id_in_params());
 | 
        
           |  |  | 540 |   | 
        
           |  |  | 541 |         $qubas = array();
 | 
        
           |  |  | 542 |         while ($records->valid()) {
 | 
        
           |  |  | 543 |             $record = $records->current();
 | 
        
           |  |  | 544 |             $qubas[$record->qubaid] = question_usage_by_activity::load_from_records($records, $record->qubaid);
 | 
        
           |  |  | 545 |         }
 | 
        
           |  |  | 546 |   | 
        
           |  |  | 547 |         $records->close();
 | 
        
           |  |  | 548 |   | 
        
           |  |  | 549 |         return $qubas;
 | 
        
           |  |  | 550 |     }
 | 
        
           |  |  | 551 |   | 
        
           |  |  | 552 |     /**
 | 
        
           |  |  | 553 |      * Load information about the latest state of each question from the database.
 | 
        
           |  |  | 554 |      *
 | 
        
           |  |  | 555 |      * This method may be called publicly.
 | 
        
           |  |  | 556 |      *
 | 
        
           |  |  | 557 |      * @param qubaid_condition $qubaids used to restrict which usages are included
 | 
        
           |  |  | 558 |      *                                  in the query. See {@link qubaid_condition}.
 | 
        
           |  |  | 559 |      * @param array|null       $slots   (optional) list of slots for which to return information. Default all slots.
 | 
        
           |  |  | 560 |      * @param string|null      $fields
 | 
        
           |  |  | 561 |      * @return array of records. See the SQL in this function to see the fields available.
 | 
        
           |  |  | 562 |      */
 | 
        
           |  |  | 563 |     public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots = null, $fields = null) {
 | 
        
           |  |  | 564 |         if ($slots === []) {
 | 
        
           |  |  | 565 |             return [];
 | 
        
           |  |  | 566 |         } else if ($slots !== null) {
 | 
        
           |  |  | 567 |             [$slottest, $params] = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 | 
        
           |  |  | 568 |             $slotwhere = " AND qa.slot {$slottest}";
 | 
        
           |  |  | 569 |         } else {
 | 
        
           |  |  | 570 |             $slotwhere = '';
 | 
        
           |  |  | 571 |             $params = [];
 | 
        
           |  |  | 572 |         }
 | 
        
           |  |  | 573 |   | 
        
           |  |  | 574 |         if ($fields === null) {
 | 
        
           |  |  | 575 |             $fields = "qas.id,
 | 
        
           |  |  | 576 |     qa.id AS questionattemptid,
 | 
        
           |  |  | 577 |     qa.questionusageid,
 | 
        
           |  |  | 578 |     qa.slot,
 | 
        
           |  |  | 579 |     qa.behaviour,
 | 
        
           |  |  | 580 |     qa.questionid,
 | 
        
           |  |  | 581 |     qa.variant,
 | 
        
           |  |  | 582 |     qa.maxmark,
 | 
        
           |  |  | 583 |     qa.minfraction,
 | 
        
           |  |  | 584 |     qa.maxfraction,
 | 
        
           |  |  | 585 |     qa.flagged,
 | 
        
           |  |  | 586 |     qa.questionsummary,
 | 
        
           |  |  | 587 |     qa.rightanswer,
 | 
        
           |  |  | 588 |     qa.responsesummary,
 | 
        
           |  |  | 589 |     qa.timemodified,
 | 
        
           |  |  | 590 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 591 |     qas.sequencenumber,
 | 
        
           |  |  | 592 |     qas.state,
 | 
        
           |  |  | 593 |     qas.fraction,
 | 
        
           |  |  | 594 |     qas.timecreated,
 | 
        
           |  |  | 595 |     qas.userid";
 | 
        
           |  |  | 596 |   | 
        
           |  |  | 597 |         }
 | 
        
           |  |  | 598 |   | 
        
           |  |  | 599 |         $records = $this->db->get_records_sql("
 | 
        
           |  |  | 600 | SELECT
 | 
        
           |  |  | 601 |     {$fields}
 | 
        
           |  |  | 602 |   | 
        
           |  |  | 603 | FROM {$qubaids->from_question_attempts('qa')}
 | 
        
           |  |  | 604 | JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 605 |         AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
 | 
        
           |  |  | 606 |   | 
        
           |  |  | 607 | WHERE
 | 
        
           |  |  | 608 |     {$qubaids->where()}
 | 
        
           |  |  | 609 |     $slotwhere
 | 
        
           |  |  | 610 |         ", $params + $qubaids->from_where_params());
 | 
        
           |  |  | 611 |   | 
        
           |  |  | 612 |         return $records;
 | 
        
           |  |  | 613 |     }
 | 
        
           |  |  | 614 |   | 
        
           |  |  | 615 |     /**
 | 
        
           |  |  | 616 |      * Load summary information about the state of each question in a group of
 | 
        
           |  |  | 617 |      * attempts. This is used, for example, by the quiz manual grading report,
 | 
        
           |  |  | 618 |      * to show how many attempts at each question need to be graded.
 | 
        
           |  |  | 619 |      *
 | 
        
           |  |  | 620 |      * This method may be called publicly.
 | 
        
           |  |  | 621 |      *
 | 
        
           |  |  | 622 |      * @param qubaid_condition $qubaids used to restrict which usages are included
 | 
        
           |  |  | 623 |      * in the query. See {@link qubaid_condition}.
 | 
        
           |  |  | 624 |      * @param array|null $slots (optional) list of slots for which to return information. Default all slots.
 | 
        
           |  |  | 625 |      * @return array The array keys are 'slot,questionid'. The values are objects with
 | 
        
           |  |  | 626 |      * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
 | 
        
           |  |  | 627 |      * $manuallygraded and $all.
 | 
        
           |  |  | 628 |      */
 | 
        
           |  |  | 629 |     public function load_questions_usages_question_state_summary(qubaid_condition $qubaids, $slots = null) {
 | 
        
           |  |  | 630 |         if ($slots !== null) {
 | 
        
           |  |  | 631 |             [$slottest, $params] = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 | 
        
           |  |  | 632 |             $slotwhere = " AND qa.slot {$slottest}";
 | 
        
           |  |  | 633 |         } else {
 | 
        
           |  |  | 634 |             $slotwhere = '';
 | 
        
           |  |  | 635 |             $params = [];
 | 
        
           |  |  | 636 |         }
 | 
        
           |  |  | 637 |   | 
        
           |  |  | 638 |         $rs = $this->db->get_recordset_sql("
 | 
        
           |  |  | 639 | SELECT
 | 
        
           |  |  | 640 |     qa.slot,
 | 
        
           |  |  | 641 |     qa.questionid,
 | 
        
           |  |  | 642 |     q.name,
 | 
        
           |  |  | 643 |     CASE qas.state
 | 
        
           |  |  | 644 |         {$this->full_states_to_summary_state_sql()}
 | 
        
           |  |  | 645 |     END AS summarystate,
 | 
        
           |  |  | 646 |     COUNT(1) AS numattempts
 | 
        
           |  |  | 647 |   | 
        
           |  |  | 648 | FROM {$qubaids->from_question_attempts('qa')}
 | 
        
           |  |  | 649 | JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 650 |         AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
 | 
        
           |  |  | 651 | JOIN {question} q ON q.id = qa.questionid
 | 
        
           |  |  | 652 |   | 
        
           |  |  | 653 | WHERE
 | 
        
           |  |  | 654 |     {$qubaids->where()}
 | 
        
           |  |  | 655 |     $slotwhere
 | 
        
           |  |  | 656 |   | 
        
           |  |  | 657 | GROUP BY
 | 
        
           |  |  | 658 |     qa.slot,
 | 
        
           |  |  | 659 |     qa.questionid,
 | 
        
           |  |  | 660 |     q.name,
 | 
        
           |  |  | 661 |     q.id,
 | 
        
           |  |  | 662 |     CASE qas.state
 | 
        
           |  |  | 663 |         {$this->full_states_to_summary_state_sql()}
 | 
        
           |  |  | 664 |     END
 | 
        
           |  |  | 665 |   | 
        
           |  |  | 666 | ORDER BY
 | 
        
           |  |  | 667 |     qa.slot,
 | 
        
           |  |  | 668 |     qa.questionid,
 | 
        
           |  |  | 669 |     q.name,
 | 
        
           |  |  | 670 |     q.id
 | 
        
           |  |  | 671 |         ", $params + $qubaids->from_where_params());
 | 
        
           |  |  | 672 |   | 
        
           |  |  | 673 |         $results = array();
 | 
        
           |  |  | 674 |         foreach ($rs as $row) {
 | 
        
           |  |  | 675 |             $index = $row->slot . ',' . $row->questionid;
 | 
        
           |  |  | 676 |   | 
        
           |  |  | 677 |             if (!array_key_exists($index, $results)) {
 | 
        
           |  |  | 678 |                 $res = new stdClass();
 | 
        
           |  |  | 679 |                 $res->slot = $row->slot;
 | 
        
           |  |  | 680 |                 $res->questionid = $row->questionid;
 | 
        
           |  |  | 681 |                 $res->name = $row->name;
 | 
        
           |  |  | 682 |                 $res->inprogress = 0;
 | 
        
           |  |  | 683 |                 $res->needsgrading = 0;
 | 
        
           |  |  | 684 |                 $res->autograded = 0;
 | 
        
           |  |  | 685 |                 $res->manuallygraded = 0;
 | 
        
           |  |  | 686 |                 $res->all = 0;
 | 
        
           |  |  | 687 |                 $results[$index] = $res;
 | 
        
           |  |  | 688 |             }
 | 
        
           |  |  | 689 |   | 
        
           |  |  | 690 |             $results[$index]->{$row->summarystate} = $row->numattempts;
 | 
        
           |  |  | 691 |             $results[$index]->all += $row->numattempts;
 | 
        
           |  |  | 692 |         }
 | 
        
           |  |  | 693 |         $rs->close();
 | 
        
           |  |  | 694 |   | 
        
           |  |  | 695 |         return $results;
 | 
        
           |  |  | 696 |     }
 | 
        
           |  |  | 697 |   | 
        
           |  |  | 698 |     /**
 | 
        
           |  |  | 699 |      * Get a list of usage ids where the question with slot $slot, and optionally
 | 
        
           |  |  | 700 |      * also with question id $questionid, is in summary state $summarystate. Also
 | 
        
           |  |  | 701 |      * return the total count of such states.
 | 
        
           |  |  | 702 |      *
 | 
        
           |  |  | 703 |      * Only a subset of the ids can be returned by using $orderby, $limitfrom and
 | 
        
           |  |  | 704 |      * $limitnum. A special value 'random' can be passed as $orderby, in which case
 | 
        
           |  |  | 705 |      * $limitfrom is ignored.
 | 
        
           |  |  | 706 |      *
 | 
        
           |  |  | 707 |      * This method may be called publicly.
 | 
        
           |  |  | 708 |      *
 | 
        
           |  |  | 709 |      * @param qubaid_condition $qubaids used to restrict which usages are included
 | 
        
           |  |  | 710 |      * in the query. See {@link qubaid_condition}.
 | 
        
           |  |  | 711 |      * @param int $slot The slot for the questions you want to know about.
 | 
        
           |  |  | 712 |      * @param int $questionid (optional) Only return attempts that were of this specific question.
 | 
        
           |  |  | 713 |      * @param string $summarystate the summary state of interest, or 'all'.
 | 
        
           |  |  | 714 |      * @param string $orderby the column to order by.
 | 
        
           |  |  | 715 |      * @param array $params any params required by any of the SQL fragments.
 | 
        
           |  |  | 716 |      * @param int $limitfrom implements paging of the results.
 | 
        
           |  |  | 717 |      *      Ignored if $orderby = random or $limitnum is null.
 | 
        
           |  |  | 718 |      * @param int $limitnum implements paging of the results. null = all.
 | 
        
           |  |  | 719 |      * @param string $extraselect anything passed here will be added to the SELECT list, use this to return extra data.
 | 
        
           |  |  | 720 |      * @return array with two elements, an array of usage ids, and a count of the total number.
 | 
        
           |  |  | 721 |      */
 | 
        
           |  |  | 722 |     public function load_questions_usages_where_question_in_state(
 | 
        
           |  |  | 723 |             qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
 | 
        
           |  |  | 724 |             $orderby = 'random', $params = array(), $limitfrom = 0, $limitnum = null, $extraselect = '') {
 | 
        
           |  |  | 725 |   | 
        
           |  |  | 726 |         $extrawhere = '';
 | 
        
           |  |  | 727 |         if ($questionid) {
 | 
        
           |  |  | 728 |             $extrawhere .= ' AND qa.questionid = :questionid';
 | 
        
           |  |  | 729 |             $params['questionid'] = $questionid;
 | 
        
           |  |  | 730 |         }
 | 
        
           |  |  | 731 |         if ($summarystate !== 'all') {
 | 
        
           |  |  | 732 |             list($test, $sparams) = $this->in_summary_state_test($summarystate);
 | 
        
           |  |  | 733 |             $extrawhere .= ' AND qas.state ' . $test;
 | 
        
           |  |  | 734 |             $params += $sparams;
 | 
        
           |  |  | 735 |         }
 | 
        
           |  |  | 736 |   | 
        
           |  |  | 737 |         if (!empty($extraselect)) {
 | 
        
           |  |  | 738 |             $extraselect = ', ' . $extraselect;
 | 
        
           |  |  | 739 |         }
 | 
        
           |  |  | 740 |   | 
        
           |  |  | 741 |         if ($orderby === 'random') {
 | 
        
           |  |  | 742 |             $sqlorderby = '';
 | 
        
           |  |  | 743 |         } else if ($orderby) {
 | 
        
           |  |  | 744 |             $sqlorderby = 'ORDER BY ' . $orderby;
 | 
        
           |  |  | 745 |         } else {
 | 
        
           |  |  | 746 |             $sqlorderby = '';
 | 
        
           |  |  | 747 |         }
 | 
        
           |  |  | 748 |   | 
        
           |  |  | 749 |         // We always want the total count, as well as the partcular list of ids
 | 
        
           |  |  | 750 |         // based on the paging and sort order. Because the list of ids is never
 | 
        
           |  |  | 751 |         // going to be too ridiculously long. My worst-case scenario is
 | 
        
           |  |  | 752 |         // 10,000 students in the course, each doing 5 quiz attempts. That
 | 
        
           |  |  | 753 |         // is a 50,000 element int => int array, which PHP seems to use 5MB
 | 
        
           |  |  | 754 |         // memory to store on a 64 bit server.
 | 
        
           |  |  | 755 |         $qubaidswhere = $qubaids->where(); // Must call this before params.
 | 
        
           |  |  | 756 |         $params += $qubaids->from_where_params();
 | 
        
           |  |  | 757 |         $params['slot'] = $slot;
 | 
        
           |  |  | 758 |         $sql = "SELECT qa.questionusageid,
 | 
        
           |  |  | 759 |                        1
 | 
        
           |  |  | 760 |                        $extraselect
 | 
        
           |  |  | 761 |                   FROM {$qubaids->from_question_attempts('qa')}
 | 
        
           |  |  | 762 |                   JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 763 |                    AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
 | 
        
           |  |  | 764 |                   JOIN {question} q ON q.id = qa.questionid
 | 
        
           |  |  | 765 |                  WHERE {$qubaidswhere}
 | 
        
           |  |  | 766 |                    AND qa.slot = :slot
 | 
        
           |  |  | 767 |                        $extrawhere
 | 
        
           |  |  | 768 |                        $sqlorderby";
 | 
        
           |  |  | 769 |   | 
        
           |  |  | 770 |         $qubaids = $this->db->get_records_sql_menu($sql, $params);
 | 
        
           |  |  | 771 |   | 
        
           |  |  | 772 |         $qubaids = array_keys($qubaids);
 | 
        
           |  |  | 773 |         $count = count($qubaids);
 | 
        
           |  |  | 774 |   | 
        
           |  |  | 775 |         if ($orderby === 'random') {
 | 
        
           |  |  | 776 |             shuffle($qubaids);
 | 
        
           |  |  | 777 |             $limitfrom = 0;
 | 
        
           |  |  | 778 |         }
 | 
        
           |  |  | 779 |   | 
        
           |  |  | 780 |         if (!is_null($limitnum)) {
 | 
        
           |  |  | 781 |             $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
 | 
        
           |  |  | 782 |         }
 | 
        
           |  |  | 783 |   | 
        
           |  |  | 784 |         return array($qubaids, $count);
 | 
        
           |  |  | 785 |     }
 | 
        
           |  |  | 786 |   | 
        
           |  |  | 787 |     /**
 | 
        
           |  |  | 788 |      * Load the average mark, and number of attempts, for each slot in a set of
 | 
        
           |  |  | 789 |      * question usages..
 | 
        
           |  |  | 790 |      *
 | 
        
           |  |  | 791 |      * This method may be called publicly.
 | 
        
           |  |  | 792 |      *
 | 
        
           |  |  | 793 |      * @param qubaid_condition $qubaids used to restrict which usages are included
 | 
        
           |  |  | 794 |      * in the query. See {@link qubaid_condition}.
 | 
        
           |  |  | 795 |      * @param array|null $slots if null, load info for all quesitions, otherwise only
 | 
        
           |  |  | 796 |      * load the averages for the specified questions.
 | 
        
           |  |  | 797 |      * @return array of objects with fields ->slot, ->averagefraction and ->numaveraged.
 | 
        
           |  |  | 798 |      */
 | 
        
           |  |  | 799 |     public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
 | 
        
           |  |  | 800 |         if (!empty($slots)) {
 | 
        
           |  |  | 801 |             list($slottest, $slotsparams) = $this->db->get_in_or_equal(
 | 
        
           |  |  | 802 |                     $slots, SQL_PARAMS_NAMED, 'slot');
 | 
        
           |  |  | 803 |             $slotwhere = " AND qa.slot {$slottest}";
 | 
        
           |  |  | 804 |         } else {
 | 
        
           |  |  | 805 |             $slotwhere = '';
 | 
        
           |  |  | 806 |             $slotsparams = array();
 | 
        
           |  |  | 807 |         }
 | 
        
           |  |  | 808 |   | 
        
           |  |  | 809 |         list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
 | 
        
           |  |  | 810 |                 (string) question_state::$gaveup,
 | 
        
           |  |  | 811 |                 (string) question_state::$gradedwrong,
 | 
        
           |  |  | 812 |                 (string) question_state::$gradedpartial,
 | 
        
           |  |  | 813 |                 (string) question_state::$gradedright,
 | 
        
           |  |  | 814 |                 (string) question_state::$mangaveup,
 | 
        
           |  |  | 815 |                 (string) question_state::$mangrwrong,
 | 
        
           |  |  | 816 |                 (string) question_state::$mangrpartial,
 | 
        
           |  |  | 817 |                 (string) question_state::$mangrright), SQL_PARAMS_NAMED, 'st');
 | 
        
           |  |  | 818 |   | 
        
           |  |  | 819 |         return $this->db->get_records_sql("
 | 
        
           |  |  | 820 | SELECT
 | 
        
           |  |  | 821 |     qa.slot,
 | 
        
           |  |  | 822 |     AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
 | 
        
           |  |  | 823 |     COUNT(1) AS numaveraged
 | 
        
           |  |  | 824 |   | 
        
           |  |  | 825 | FROM {$qubaids->from_question_attempts('qa')}
 | 
        
           |  |  | 826 | JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 827 |         AND qas.sequencenumber = {$this->latest_step_for_qa_subquery()}
 | 
        
           |  |  | 828 |   | 
        
           |  |  | 829 | WHERE
 | 
        
           |  |  | 830 |     {$qubaids->where()}
 | 
        
           |  |  | 831 |     $slotwhere
 | 
        
           |  |  | 832 |     AND qas.state $statetest
 | 
        
           |  |  | 833 |   | 
        
           |  |  | 834 | GROUP BY qa.slot
 | 
        
           |  |  | 835 |   | 
        
           |  |  | 836 | ORDER BY qa.slot
 | 
        
           |  |  | 837 |         ", $slotsparams + $stateparams + $qubaids->from_where_params());
 | 
        
           |  |  | 838 |     }
 | 
        
           |  |  | 839 |   | 
        
           |  |  | 840 |     /**
 | 
        
           |  |  | 841 |      * Load all the attempts at a given queston from a set of question_usages.
 | 
        
           |  |  | 842 |      * steps.
 | 
        
           |  |  | 843 |      *
 | 
        
           |  |  | 844 |      * This method may be called publicly.
 | 
        
           |  |  | 845 |      *
 | 
        
           |  |  | 846 |      * @param int $questionid the question to load all the attempts fors.
 | 
        
           |  |  | 847 |      * @param qubaid_condition $qubaids used to restrict which usages are included
 | 
        
           |  |  | 848 |      * in the query. See {@link qubaid_condition}.
 | 
        
           |  |  | 849 |      * @return question_attempt[] array of question_attempts that were loaded.
 | 
        
           |  |  | 850 |      */
 | 
        
           |  |  | 851 |     public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
 | 
        
           |  |  | 852 |         $sql = "
 | 
        
           |  |  | 853 | SELECT
 | 
        
           |  |  | 854 |     quba.contextid,
 | 
        
           |  |  | 855 |     quba.preferredbehaviour,
 | 
        
           |  |  | 856 |     qa.id AS questionattemptid,
 | 
        
           |  |  | 857 |     qa.questionusageid,
 | 
        
           |  |  | 858 |     qa.slot,
 | 
        
           |  |  | 859 |     qa.behaviour,
 | 
        
           |  |  | 860 |     qa.questionid,
 | 
        
           |  |  | 861 |     qa.variant,
 | 
        
           |  |  | 862 |     qa.maxmark,
 | 
        
           |  |  | 863 |     qa.minfraction,
 | 
        
           |  |  | 864 |     qa.maxfraction,
 | 
        
           |  |  | 865 |     qa.flagged,
 | 
        
           |  |  | 866 |     qa.questionsummary,
 | 
        
           |  |  | 867 |     qa.rightanswer,
 | 
        
           |  |  | 868 |     qa.responsesummary,
 | 
        
           |  |  | 869 |     qa.timemodified,
 | 
        
           |  |  | 870 |     qas.id AS attemptstepid,
 | 
        
           |  |  | 871 |     qas.sequencenumber,
 | 
        
           |  |  | 872 |     qas.state,
 | 
        
           |  |  | 873 |     qas.fraction,
 | 
        
           |  |  | 874 |     qas.timecreated,
 | 
        
           |  |  | 875 |     qas.userid,
 | 
        
           |  |  | 876 |     qasd.name,
 | 
        
           |  |  | 877 |     qasd.value
 | 
        
           |  |  | 878 |   | 
        
           |  |  | 879 | FROM {$qubaids->from_question_attempts('qa')}
 | 
        
           |  |  | 880 | JOIN {question_usages} quba ON quba.id = qa.questionusageid
 | 
        
           |  |  | 881 | LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 882 | LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 | 
        
           |  |  | 883 |   | 
        
           |  |  | 884 | WHERE
 | 
        
           |  |  | 885 |     {$qubaids->where()} AND
 | 
        
           |  |  | 886 |     qa.questionid = :questionid
 | 
        
           |  |  | 887 |   | 
        
           |  |  | 888 | ORDER BY
 | 
        
           |  |  | 889 |     quba.id,
 | 
        
           |  |  | 890 |     qa.id,
 | 
        
           |  |  | 891 |     qas.sequencenumber";
 | 
        
           |  |  | 892 |   | 
        
           |  |  | 893 |         // For qubaid_list must call this after calling methods that generate sql.
 | 
        
           |  |  | 894 |         $params = $qubaids->from_where_params();
 | 
        
           |  |  | 895 |         $params['questionid'] = $questionid;
 | 
        
           |  |  | 896 |   | 
        
           |  |  | 897 |         $records = $this->db->get_recordset_sql($sql, $params);
 | 
        
           |  |  | 898 |   | 
        
           |  |  | 899 |         $questionattempts = array();
 | 
        
           |  |  | 900 |         while ($records->valid()) {
 | 
        
           |  |  | 901 |             $record = $records->current();
 | 
        
           |  |  | 902 |             $questionattempts[$record->questionattemptid] =
 | 
        
           |  |  | 903 |                     question_attempt::load_from_records($records,
 | 
        
           |  |  | 904 |                     $record->questionattemptid, new question_usage_null_observer(),
 | 
        
           |  |  | 905 |                     $record->preferredbehaviour);
 | 
        
           |  |  | 906 |         }
 | 
        
           |  |  | 907 |         $records->close();
 | 
        
           |  |  | 908 |   | 
        
           |  |  | 909 |         return $questionattempts;
 | 
        
           |  |  | 910 |     }
 | 
        
           |  |  | 911 |   | 
        
           |  |  | 912 |     /**
 | 
        
           |  |  | 913 |      * Update a question_usages row to refect any changes in a usage (but not
 | 
        
           |  |  | 914 |      * any of its question_attempts.
 | 
        
           |  |  | 915 |      *
 | 
        
           |  |  | 916 |      * You should not call this method directly. You should use
 | 
        
           |  |  | 917 |      * @link question_engine::save_questions_usage_by_activity()}.
 | 
        
           |  |  | 918 |      *
 | 
        
           |  |  | 919 |      * @param question_usage_by_activity $quba the usage that has changed.
 | 
        
           |  |  | 920 |      */
 | 
        
           |  |  | 921 |     public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
 | 
        
           |  |  | 922 |         $record = new stdClass();
 | 
        
           |  |  | 923 |         $record->id = $quba->get_id();
 | 
        
           |  |  | 924 |         $record->contextid = $quba->get_owning_context()->id;
 | 
        
           |  |  | 925 |         $record->component = $quba->get_owning_component();
 | 
        
           |  |  | 926 |         $record->preferredbehaviour = $quba->get_preferred_behaviour();
 | 
        
           |  |  | 927 |   | 
        
           |  |  | 928 |         $this->db->update_record('question_usages', $record);
 | 
        
           |  |  | 929 |     }
 | 
        
           |  |  | 930 |   | 
        
           |  |  | 931 |     /**
 | 
        
           |  |  | 932 |      * Update a question_attempts row to refect any changes in a question_attempt
 | 
        
           |  |  | 933 |      * (but not any of its steps).
 | 
        
           |  |  | 934 |      *
 | 
        
           |  |  | 935 |      * You should not call this method directly. You should use
 | 
        
           |  |  | 936 |      * @link question_engine::save_questions_usage_by_activity()}.
 | 
        
           |  |  | 937 |      *
 | 
        
           |  |  | 938 |      * @param question_attempt $qa the question attempt that has changed.
 | 
        
           |  |  | 939 |      */
 | 
        
           |  |  | 940 |     public function update_question_attempt(question_attempt $qa) {
 | 
        
           |  |  | 941 |         $record = new stdClass();
 | 
        
           |  |  | 942 |         $record->id = $qa->get_database_id();
 | 
        
           |  |  | 943 |         $record->slot = $qa->get_slot();
 | 
        
           |  |  | 944 |         $record->questionid = $qa->get_question(false)->id;
 | 
        
           |  |  | 945 |         $record->variant = $qa->get_variant();
 | 
        
           |  |  | 946 |         $record->maxmark = $qa->get_max_mark();
 | 
        
           |  |  | 947 |         $record->minfraction = $qa->get_min_fraction();
 | 
        
           |  |  | 948 |         $record->maxfraction = $qa->get_max_fraction();
 | 
        
           |  |  | 949 |         $record->flagged = $qa->is_flagged();
 | 
        
           |  |  | 950 |         $record->questionsummary = $qa->get_question_summary();
 | 
        
           |  |  | 951 |         $record->rightanswer = $qa->get_right_answer_summary();
 | 
        
           |  |  | 952 |         $record->responsesummary = $qa->get_response_summary();
 | 
        
           |  |  | 953 |         $record->timemodified = time();
 | 
        
           |  |  | 954 |   | 
        
           |  |  | 955 |         $this->db->update_record('question_attempts', $record);
 | 
        
           |  |  | 956 |     }
 | 
        
           |  |  | 957 |   | 
        
           |  |  | 958 |     /**
 | 
        
           |  |  | 959 |      * Delete a question_usage_by_activity and all its associated
 | 
        
           |  |  | 960 |      *
 | 
        
           |  |  | 961 |      * You should not call this method directly. You should use
 | 
        
           |  |  | 962 |      * @link question_engine::delete_questions_usage_by_activities()}.
 | 
        
           |  |  | 963 |      *
 | 
        
           |  |  | 964 |      * {@link question_attempts} and {@link question_attempt_steps} from the
 | 
        
           |  |  | 965 |      * database.
 | 
        
           |  |  | 966 |      * @param qubaid_condition $qubaids identifies which question useages to delete.
 | 
        
           |  |  | 967 |      */
 | 
        
           |  |  | 968 |     public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
 | 
        
           |  |  | 969 |         $where = "qa.questionusageid {$qubaids->usage_id_in()}";
 | 
        
           |  |  | 970 |         $params = $qubaids->usage_id_in_params();
 | 
        
           |  |  | 971 |   | 
        
           |  |  | 972 |         $contextids = $this->db->get_records_sql_menu("
 | 
        
           |  |  | 973 |                 SELECT DISTINCT contextid, 1
 | 
        
           |  |  | 974 |                 FROM {question_usages}
 | 
        
           |  |  | 975 |                 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
 | 
        
           |  |  | 976 |         foreach ($contextids as $contextid => $notused) {
 | 
        
           |  |  | 977 |             $this->delete_response_files($contextid, "IN (
 | 
        
           |  |  | 978 |                     SELECT qas.id
 | 
        
           |  |  | 979 |                     FROM {question_attempts} qa
 | 
        
           |  |  | 980 |                     JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 981 |                     WHERE $where)", $params);
 | 
        
           |  |  | 982 |         }
 | 
        
           |  |  | 983 |   | 
        
           |  |  | 984 |         if ($this->db->get_dbfamily() == 'mysql') {
 | 
        
           |  |  | 985 |             $this->delete_usage_records_for_mysql($qubaids);
 | 
        
           |  |  | 986 |             return;
 | 
        
           |  |  | 987 |         }
 | 
        
           |  |  | 988 |   | 
        
           |  |  | 989 |         $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
 | 
        
           |  |  | 990 |                 SELECT qas.id
 | 
        
           |  |  | 991 |                 FROM {question_attempts} qa
 | 
        
           |  |  | 992 |                 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 993 |                 WHERE $where)", $params);
 | 
        
           |  |  | 994 |   | 
        
           |  |  | 995 |         $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
 | 
        
           |  |  | 996 |                 SELECT qa.id
 | 
        
           |  |  | 997 |                 FROM {question_attempts} qa
 | 
        
           |  |  | 998 |                 WHERE $where)", $params);
 | 
        
           |  |  | 999 |   | 
        
           |  |  | 1000 |         $this->db->delete_records_select('question_attempts',
 | 
        
           |  |  | 1001 |                 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
 | 
        
           |  |  | 1002 |                 $qubaids->usage_id_in_params());
 | 
        
           |  |  | 1003 |   | 
        
           |  |  | 1004 |         $this->db->delete_records_select('question_usages',
 | 
        
           |  |  | 1005 |                 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
 | 
        
           |  |  | 1006 |     }
 | 
        
           |  |  | 1007 |   | 
        
           |  |  | 1008 |     /**
 | 
        
           |  |  | 1009 |      * This function is a work-around for poor MySQL performance with
 | 
        
           |  |  | 1010 |      * DELETE FROM x WHERE id IN (SELECT ...). We have to use a non-standard
 | 
        
           |  |  | 1011 |      * syntax to get good performance. See MDL-29520.
 | 
        
           |  |  | 1012 |      * @param qubaid_condition $qubaids identifies which question useages to delete.
 | 
        
           |  |  | 1013 |      */
 | 
        
           |  |  | 1014 |     protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
 | 
        
           |  |  | 1015 |         // Get the list of question attempts to delete and delete them in chunks.
 | 
        
           |  |  | 1016 |         $allids = $this->db->get_records_sql_menu("
 | 
        
           |  |  | 1017 |                 SELECT DISTINCT id, id AS id2
 | 
        
           |  |  | 1018 |                   FROM {question_usages}
 | 
        
           |  |  | 1019 |                  WHERE id " . $qubaids->usage_id_in(),
 | 
        
           |  |  | 1020 |                 $qubaids->usage_id_in_params());
 | 
        
           |  |  | 1021 |   | 
        
           |  |  | 1022 |         foreach (array_chunk($allids, 1000) as $todelete) {
 | 
        
           |  |  | 1023 |             list($idsql, $idparams) = $this->db->get_in_or_equal($todelete);
 | 
        
           |  |  | 1024 |             $this->db->execute('
 | 
        
           |  |  | 1025 |                     DELETE qu, qa, qas, qasd
 | 
        
           |  |  | 1026 |                       FROM {question_usages}            qu
 | 
        
           |  |  | 1027 |                       JOIN {question_attempts}          qa   ON qa.questionusageid = qu.id
 | 
        
           |  |  | 1028 |                  LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 1029 |                  LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 | 
        
           |  |  | 1030 |                      WHERE qu.id ' . $idsql,
 | 
        
           |  |  | 1031 |                     $idparams);
 | 
        
           |  |  | 1032 |         }
 | 
        
           |  |  | 1033 |     }
 | 
        
           |  |  | 1034 |   | 
        
           |  |  | 1035 |     /**
 | 
        
           |  |  | 1036 |      * Delete some steps of a question attempt.
 | 
        
           |  |  | 1037 |      *
 | 
        
           |  |  | 1038 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 1039 |      *
 | 
        
           |  |  | 1040 |      * @param array $stepids array of step ids to delete.
 | 
        
           |  |  | 1041 |      * @param context $context the context that the $quba belongs to.
 | 
        
           |  |  | 1042 |      */
 | 
        
           |  |  | 1043 |     public function delete_steps($stepids, $context) {
 | 
        
           |  |  | 1044 |         if (empty($stepids)) {
 | 
        
           |  |  | 1045 |             return;
 | 
        
           |  |  | 1046 |         }
 | 
        
           |  |  | 1047 |         list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 1048 |   | 
        
           |  |  | 1049 |         $this->delete_response_files($context->id, $test, $params);
 | 
        
           |  |  | 1050 |   | 
        
           |  |  | 1051 |         $this->db->delete_records_select('question_attempt_step_data',
 | 
        
           |  |  | 1052 |                 "attemptstepid {$test}", $params);
 | 
        
           |  |  | 1053 |         $this->db->delete_records_select('question_attempt_steps',
 | 
        
           |  |  | 1054 |                 "id {$test}", $params);
 | 
        
           |  |  | 1055 |     }
 | 
        
           |  |  | 1056 |   | 
        
           |  |  | 1057 |     /**
 | 
        
           |  |  | 1058 |      * Delete all the files belonging to the response variables in the gives
 | 
        
           |  |  | 1059 |      * question attempt steps.
 | 
        
           |  |  | 1060 |      * @param int $contextid the context these attempts belong to.
 | 
        
           |  |  | 1061 |      * @param string $itemidstest a bit of SQL that can be used in a
 | 
        
           |  |  | 1062 |      *      WHERE itemid $itemidstest clause. Must use named params.
 | 
        
           |  |  | 1063 |      * @param array $params any query parameters used in $itemidstest.
 | 
        
           |  |  | 1064 |      */
 | 
        
           |  |  | 1065 |     protected function delete_response_files($contextid, $itemidstest, $params) {
 | 
        
           |  |  | 1066 |         $fs = get_file_storage();
 | 
        
           |  |  | 1067 |         foreach (question_engine::get_all_response_file_areas() as $filearea) {
 | 
        
           |  |  | 1068 |             $fs->delete_area_files_select($contextid, 'question', $filearea,
 | 
        
           |  |  | 1069 |                     $itemidstest, $params);
 | 
        
           |  |  | 1070 |         }
 | 
        
           |  |  | 1071 |     }
 | 
        
           |  |  | 1072 |   | 
        
           |  |  | 1073 |     /**
 | 
        
           |  |  | 1074 |      * Delete all the previews for a given question.
 | 
        
           |  |  | 1075 |      *
 | 
        
           |  |  | 1076 |      * Private method, only for use by other parts of the question engine.
 | 
        
           |  |  | 1077 |      *
 | 
        
           |  |  | 1078 |      * @param int $questionid question id.
 | 
        
           |  |  | 1079 |      */
 | 
        
           |  |  | 1080 |     public function delete_previews($questionid) {
 | 
        
           |  |  | 1081 |         $previews = $this->db->get_records_sql_menu("
 | 
        
           |  |  | 1082 |                 SELECT DISTINCT quba.id, 1
 | 
        
           |  |  | 1083 |                 FROM {question_usages} quba
 | 
        
           |  |  | 1084 |                 JOIN {question_attempts} qa ON qa.questionusageid = quba.id
 | 
        
           |  |  | 1085 |                 WHERE quba.component = 'core_question_preview' AND
 | 
        
           |  |  | 1086 |                     qa.questionid = ?", array($questionid));
 | 
        
           |  |  | 1087 |         if (empty($previews)) {
 | 
        
           |  |  | 1088 |             return;
 | 
        
           |  |  | 1089 |         }
 | 
        
           |  |  | 1090 |         $this->delete_questions_usage_by_activities(new qubaid_list($previews));
 | 
        
           |  |  | 1091 |     }
 | 
        
           |  |  | 1092 |   | 
        
           |  |  | 1093 |     /**
 | 
        
           |  |  | 1094 |      * Update the flagged state of a question in the database.
 | 
        
           |  |  | 1095 |      *
 | 
        
           |  |  | 1096 |      * You should call {@link question_engine::update_flag()()}
 | 
        
           |  |  | 1097 |      * rather than calling this method directly.
 | 
        
           |  |  | 1098 |      *
 | 
        
           |  |  | 1099 |      * @param int $qubaid the question usage id.
 | 
        
           |  |  | 1100 |      * @param int $questionid the question id.
 | 
        
           |  |  | 1101 |      * @param int $qaid the question_attempt id.
 | 
        
           |  |  | 1102 |      * @param int $slot the slot number of the question attempt to update.
 | 
        
           |  |  | 1103 |      * @param bool $newstate the new state of the flag. true = flagged.
 | 
        
           |  |  | 1104 |      */
 | 
        
           |  |  | 1105 |     public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
 | 
        
           |  |  | 1106 |         if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
 | 
        
           |  |  | 1107 |                 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
 | 
        
           |  |  | 1108 |             throw new moodle_exception('errorsavingflags', 'question');
 | 
        
           |  |  | 1109 |         }
 | 
        
           |  |  | 1110 |   | 
        
           |  |  | 1111 |         $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
 | 
        
           |  |  | 1112 |     }
 | 
        
           |  |  | 1113 |   | 
        
           |  |  | 1114 |     /**
 | 
        
           |  |  | 1115 |      * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
 | 
        
           |  |  | 1116 |      * column to a summary state. Use this like
 | 
        
           |  |  | 1117 |      * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
 | 
        
           |  |  | 1118 |      *
 | 
        
           |  |  | 1119 |      * @return string SQL fragment.
 | 
        
           |  |  | 1120 |      */
 | 
        
           |  |  | 1121 |     protected function full_states_to_summary_state_sql() {
 | 
        
           |  |  | 1122 |         $sql = '';
 | 
        
           |  |  | 1123 |         foreach (question_state::get_all() as $state) {
 | 
        
           |  |  | 1124 |             $sql .= "WHEN '{$state}' THEN '{$state->get_summary_state()}'\n";
 | 
        
           |  |  | 1125 |         }
 | 
        
           |  |  | 1126 |         return $sql;
 | 
        
           |  |  | 1127 |     }
 | 
        
           |  |  | 1128 |   | 
        
           |  |  | 1129 |     /**
 | 
        
           |  |  | 1130 |      * Get the SQL needed to test that question_attempt_steps.state is in a
 | 
        
           |  |  | 1131 |      * state corresponding to $summarystate.
 | 
        
           |  |  | 1132 |      *
 | 
        
           |  |  | 1133 |      * This method may be called publicly.
 | 
        
           |  |  | 1134 |      *
 | 
        
           |  |  | 1135 |      * @param string $summarystate one of
 | 
        
           |  |  | 1136 |      * inprogress, needsgrading, manuallygraded or autograded
 | 
        
           |  |  | 1137 |      * @param bool $equal if false, do a NOT IN test. Default true.
 | 
        
           |  |  | 1138 |      * @param string $prefix used in the call to $DB->get_in_or_equal().
 | 
        
           |  |  | 1139 |      * @return array as returned by $DB->get_in_or_equal().
 | 
        
           |  |  | 1140 |      */
 | 
        
           |  |  | 1141 |     public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
 | 
        
           |  |  | 1142 |         $states = question_state::get_all_for_summary_state($summarystate);
 | 
        
           |  |  | 1143 |         return $this->db->get_in_or_equal(array_map('strval', $states),
 | 
        
           |  |  | 1144 |                 SQL_PARAMS_NAMED, $prefix, $equal);
 | 
        
           |  |  | 1145 |     }
 | 
        
           |  |  | 1146 |   | 
        
           |  |  | 1147 |     /**
 | 
        
           |  |  | 1148 |      * Change the maxmark for the question_attempt with number in usage $slot
 | 
        
           |  |  | 1149 |      * for all the specified question_attempts.
 | 
        
           |  |  | 1150 |      *
 | 
        
           |  |  | 1151 |      * You should call {@link question_engine::set_max_mark_in_attempts()}
 | 
        
           |  |  | 1152 |      * rather than calling this method directly.
 | 
        
           |  |  | 1153 |      *
 | 
        
           |  |  | 1154 |      * @param qubaid_condition $qubaids Selects which usages are updated.
 | 
        
           |  |  | 1155 |      * @param int $slot the number is usage to affect.
 | 
        
           |  |  | 1156 |      * @param number $newmaxmark the new max mark to set.
 | 
        
           |  |  | 1157 |      */
 | 
        
           |  |  | 1158 |     public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
 | 
        
           |  |  | 1159 |         if ($this->db->get_dbfamily() == 'mysql') {
 | 
        
           |  |  | 1160 |             // MySQL's query optimiser completely fails to cope with the
 | 
        
           |  |  | 1161 |             // set_field_select call below, so we have to give it a clue. See MDL-32616.
 | 
        
           |  |  | 1162 |             // TODO MDL-29589 encapsulate this MySQL-specific code with a $DB method.
 | 
        
           |  |  | 1163 |             $this->db->execute("
 | 
        
           |  |  | 1164 |                     UPDATE " . $qubaids->from_question_attempts('qa') . "
 | 
        
           |  |  | 1165 |                        SET qa.maxmark = :newmaxmark
 | 
        
           |  |  | 1166 |                      WHERE " . $qubaids->where() . "
 | 
        
           |  |  | 1167 |                        AND slot = :slot
 | 
        
           |  |  | 1168 |                     ", $qubaids->from_where_params() + array('newmaxmark' => $newmaxmark, 'slot' => $slot));
 | 
        
           |  |  | 1169 |             return;
 | 
        
           |  |  | 1170 |         }
 | 
        
           |  |  | 1171 |   | 
        
           |  |  | 1172 |         // Normal databases.
 | 
        
           |  |  | 1173 |         $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
 | 
        
           |  |  | 1174 |                 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
 | 
        
           |  |  | 1175 |                 $qubaids->usage_id_in_params() + array('slot' => $slot));
 | 
        
           |  |  | 1176 |     }
 | 
        
           |  |  | 1177 |   | 
        
           |  |  | 1178 |     /**
 | 
        
           |  |  | 1179 |      * Return a sub-query that computes the sum of the marks for all the questions
 | 
        
           |  |  | 1180 |      * in a usage. Which usage to compute the sum for is controlled by the $qubaid
 | 
        
           |  |  | 1181 |      * parameter.
 | 
        
           |  |  | 1182 |      *
 | 
        
           |  |  | 1183 |      * See {@see \mod_quiz\grade_calculator::recompute_all_attempt_sumgrades()} for an example of the usage of
 | 
        
           |  |  | 1184 |      * this method.
 | 
        
           |  |  | 1185 |      *
 | 
        
           |  |  | 1186 |      * This method may be called publicly.
 | 
        
           |  |  | 1187 |      *
 | 
        
           |  |  | 1188 |      * @param string $qubaid SQL fragment that controls which usage is summed.
 | 
        
           |  |  | 1189 |      * This will normally be the name of a column in the outer query. Not that this
 | 
        
           |  |  | 1190 |      * SQL fragment must not contain any placeholders.
 | 
        
           |  |  | 1191 |      * @return string SQL code for the subquery.
 | 
        
           |  |  | 1192 |      */
 | 
        
           |  |  | 1193 |     public function sum_usage_marks_subquery($qubaid) {
 | 
        
           |  |  | 1194 |         // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives
 | 
        
           |  |  | 1195 |         // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that.
 | 
        
           |  |  | 1196 |         // We always want to return a number, so the COALESCE is there to turn the
 | 
        
           |  |  | 1197 |         // NULL total into a 0.
 | 
        
           |  |  | 1198 |         return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0)
 | 
        
           |  |  | 1199 |             FROM {question_attempts} qa
 | 
        
           |  |  | 1200 |             JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 | 
        
           |  |  | 1201 |                     AND qas.sequencenumber = (
 | 
        
           |  |  | 1202 |                             SELECT MAX(summarks_qas.sequencenumber)
 | 
        
           |  |  | 1203 |                               FROM {question_attempt_steps} summarks_qas
 | 
        
           |  |  | 1204 |                              WHERE summarks_qas.questionattemptid = qa.id
 | 
        
           |  |  | 1205 |             )
 | 
        
           |  |  | 1206 |             WHERE qa.questionusageid = $qubaid
 | 
        
           |  |  | 1207 |             HAVING COUNT(CASE
 | 
        
           |  |  | 1208 |                 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1
 | 
        
           |  |  | 1209 |                 ELSE NULL
 | 
        
           |  |  | 1210 |             END) = 0";
 | 
        
           |  |  | 1211 |     }
 | 
        
           |  |  | 1212 |   | 
        
           |  |  | 1213 |     /**
 | 
        
           |  |  | 1214 |      * Get a subquery that returns the latest step of every qa in some qubas.
 | 
        
           |  |  | 1215 |      * Currently, this is only used by the quiz reports. See
 | 
        
           |  |  | 1216 |      * {@see \mod_quiz\local\reports\attempts_report_table::add_latest_state_join()}.
 | 
        
           |  |  | 1217 |      *
 | 
        
           |  |  | 1218 |      * This method may be called publicly.
 | 
        
           |  |  | 1219 |      *
 | 
        
           |  |  | 1220 |      * @param string $alias alias to use for this inline-view.
 | 
        
           |  |  | 1221 |      * @param qubaid_condition $qubaids restriction on which question_usages we
 | 
        
           |  |  | 1222 |      *      are interested in. This is important for performance.
 | 
        
           |  |  | 1223 |      * @return array with two elements, the SQL fragment and any params requried.
 | 
        
           |  |  | 1224 |      */
 | 
        
           |  |  | 1225 |     public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) {
 | 
        
           |  |  | 1226 |         return array("(
 | 
        
           |  |  | 1227 |                 SELECT {$alias}qa.id AS questionattemptid,
 | 
        
           |  |  | 1228 |                        {$alias}qa.questionusageid,
 | 
        
           |  |  | 1229 |                        {$alias}qa.slot,
 | 
        
           |  |  | 1230 |                        {$alias}qa.behaviour,
 | 
        
           |  |  | 1231 |                        {$alias}qa.questionid,
 | 
        
           |  |  | 1232 |                        {$alias}qa.variant,
 | 
        
           |  |  | 1233 |                        {$alias}qa.maxmark,
 | 
        
           |  |  | 1234 |                        {$alias}qa.minfraction,
 | 
        
           |  |  | 1235 |                        {$alias}qa.maxfraction,
 | 
        
           |  |  | 1236 |                        {$alias}qa.flagged,
 | 
        
           |  |  | 1237 |                        {$alias}qa.questionsummary,
 | 
        
           |  |  | 1238 |                        {$alias}qa.rightanswer,
 | 
        
           |  |  | 1239 |                        {$alias}qa.responsesummary,
 | 
        
           |  |  | 1240 |                        {$alias}qa.timemodified,
 | 
        
           |  |  | 1241 |                        {$alias}qas.id AS attemptstepid,
 | 
        
           |  |  | 1242 |                        {$alias}qas.sequencenumber,
 | 
        
           |  |  | 1243 |                        {$alias}qas.state,
 | 
        
           |  |  | 1244 |                        {$alias}qas.fraction,
 | 
        
           |  |  | 1245 |                        {$alias}qas.timecreated,
 | 
        
           |  |  | 1246 |                        {$alias}qas.userid
 | 
        
           |  |  | 1247 |   | 
        
           |  |  | 1248 |                   FROM {$qubaids->from_question_attempts($alias . 'qa')}
 | 
        
           |  |  | 1249 |                   JOIN {question_attempt_steps} {$alias}qas ON {$alias}qas.questionattemptid = {$alias}qa.id
 | 
        
           |  |  | 1250 |                             AND {$alias}qas.sequencenumber = {$this->latest_step_for_qa_subquery($alias . 'qa.id')}
 | 
        
           |  |  | 1251 |                  WHERE {$qubaids->where()}
 | 
        
           |  |  | 1252 |             ) {$alias}", $qubaids->from_where_params());
 | 
        
           |  |  | 1253 |     }
 | 
        
           |  |  | 1254 |   | 
        
           |  |  | 1255 |     /**
 | 
        
           |  |  | 1256 |      * Get the subquery which selects the latest step for each question_attempt.
 | 
        
           |  |  | 1257 |      *
 | 
        
           |  |  | 1258 |      * @param string $questionattemptid column alias for the column to join on which is question_attempt.id.
 | 
        
           |  |  | 1259 |      * @return string SQL fragment to include in the query. Has not placeholders.
 | 
        
           |  |  | 1260 |      */
 | 
        
           |  |  | 1261 |     public function latest_step_for_qa_subquery($questionattemptid = 'qa.id') {
 | 
        
           |  |  | 1262 |         return "(
 | 
        
           |  |  | 1263 |                 SELECT MAX(sequencenumber)
 | 
        
           |  |  | 1264 |                 FROM {question_attempt_steps}
 | 
        
           |  |  | 1265 |                 WHERE questionattemptid = $questionattemptid
 | 
        
           |  |  | 1266 |             )";
 | 
        
           |  |  | 1267 |     }
 | 
        
           |  |  | 1268 |   | 
        
           |  |  | 1269 |     /**
 | 
        
           |  |  | 1270 |      * Are any of these questions are currently in use?
 | 
        
           |  |  | 1271 |      *
 | 
        
           |  |  | 1272 |      * You should call {@link question_engine::questions_in_use()}
 | 
        
           |  |  | 1273 |      * rather than calling this method directly.
 | 
        
           |  |  | 1274 |      *
 | 
        
           |  |  | 1275 |      * @param array $questionids of question ids.
 | 
        
           |  |  | 1276 |      * @param qubaid_condition $qubaids ids of the usages to consider.
 | 
        
           |  |  | 1277 |      * @return bool whether any of these questions are being used by any of
 | 
        
           |  |  | 1278 |      *      those usages.
 | 
        
           |  |  | 1279 |      */
 | 
        
           |  |  | 1280 |     public function questions_in_use(array $questionids, qubaid_condition $qubaids) {
 | 
        
           |  |  | 1281 |         list($test, $params) = $this->db->get_in_or_equal($questionids);
 | 
        
           |  |  | 1282 |         return $this->db->record_exists_select('question_attempts',
 | 
        
           |  |  | 1283 |                 'questionid ' . $test . ' AND questionusageid ' .
 | 
        
           |  |  | 1284 |                 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
 | 
        
           |  |  | 1285 |     }
 | 
        
           |  |  | 1286 |   | 
        
           |  |  | 1287 |     /**
 | 
        
           |  |  | 1288 |      * Get the number of times each variant has been used for each question in a list
 | 
        
           |  |  | 1289 |      * in a set of usages.
 | 
        
           |  |  | 1290 |      * @param array $questionids of question ids.
 | 
        
           |  |  | 1291 |      * @param qubaid_condition $qubaids ids of the usages to consider.
 | 
        
           |  |  | 1292 |      * @return array questionid => variant number => num uses.
 | 
        
           |  |  | 1293 |      */
 | 
        
           |  |  | 1294 |     public function load_used_variants(array $questionids, qubaid_condition $qubaids) {
 | 
        
           |  |  | 1295 |         list($test, $params) = $this->db->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'qid');
 | 
        
           |  |  | 1296 |         $recordset = $this->db->get_recordset_sql("
 | 
        
           |  |  | 1297 |                 SELECT qa.questionid, qa.variant, COUNT(1) AS usescount
 | 
        
           |  |  | 1298 |                   FROM " . $qubaids->from_question_attempts('qa') . "
 | 
        
           |  |  | 1299 |                  WHERE qa.questionid $test
 | 
        
           |  |  | 1300 |                    AND " . $qubaids->where() . "
 | 
        
           |  |  | 1301 |               GROUP BY qa.questionid, qa.variant
 | 
        
           |  |  | 1302 |               ORDER BY COUNT(1) ASC
 | 
        
           |  |  | 1303 |                 ", $params + $qubaids->from_where_params());
 | 
        
           |  |  | 1304 |   | 
        
           |  |  | 1305 |         $usedvariants = array_combine($questionids, array_fill(0, count($questionids), array()));
 | 
        
           |  |  | 1306 |         foreach ($recordset as $row) {
 | 
        
           |  |  | 1307 |             $usedvariants[$row->questionid][$row->variant] = $row->usescount;
 | 
        
           |  |  | 1308 |         }
 | 
        
           |  |  | 1309 |         $recordset->close();
 | 
        
           |  |  | 1310 |         return $usedvariants;
 | 
        
           |  |  | 1311 |     }
 | 
        
           |  |  | 1312 | }
 | 
        
           |  |  | 1313 |   | 
        
           |  |  | 1314 |   | 
        
           |  |  | 1315 | /**
 | 
        
           |  |  | 1316 |  * Implementation of the unit of work pattern for the question engine.
 | 
        
           |  |  | 1317 |  *
 | 
        
           |  |  | 1318 |  * See http://martinfowler.com/eaaCatalog/unitOfWork.html. This tracks all the
 | 
        
           |  |  | 1319 |  * changes to a {@link question_usage_by_activity}, and its constituent parts,
 | 
        
           |  |  | 1320 |  * so that the changes can be saved to the database when {@link save()} is called.
 | 
        
           |  |  | 1321 |  *
 | 
        
           |  |  | 1322 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 1323 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1324 |  */
 | 
        
           |  |  | 1325 | class question_engine_unit_of_work implements question_usage_observer {
 | 
        
           |  |  | 1326 |     /** @var question_usage_by_activity the usage being tracked. */
 | 
        
           |  |  | 1327 |     protected $quba;
 | 
        
           |  |  | 1328 |   | 
        
           |  |  | 1329 |     /** @var boolean whether any of the fields of the usage have been changed. */
 | 
        
           |  |  | 1330 |     protected $modified = false;
 | 
        
           |  |  | 1331 |   | 
        
           |  |  | 1332 |     /**
 | 
        
           |  |  | 1333 |      * @var question_attempt[] list of slot => {@link question_attempt}s that
 | 
        
           |  |  | 1334 |      * have been added to the usage.
 | 
        
           |  |  | 1335 |      */
 | 
        
           |  |  | 1336 |     protected $attemptsadded = array();
 | 
        
           |  |  | 1337 |   | 
        
           |  |  | 1338 |     /**
 | 
        
           |  |  | 1339 |      * @var question_attempt[] list of slot => {@link question_attempt}s that
 | 
        
           |  |  | 1340 |      * were already in the usage, and which have been modified.
 | 
        
           |  |  | 1341 |      */
 | 
        
           |  |  | 1342 |     protected $attemptsmodified = array();
 | 
        
           |  |  | 1343 |   | 
        
           |  |  | 1344 |     /**
 | 
        
           |  |  | 1345 |      * @var question_attempt[] list of slot => {@link question_attempt}s that
 | 
        
           |  |  | 1346 |      * have been added to the usage.
 | 
        
           |  |  | 1347 |      */
 | 
        
           |  |  | 1348 |     protected $attemptsdeleted = array();
 | 
        
           |  |  | 1349 |   | 
        
           |  |  | 1350 |     /**
 | 
        
           |  |  | 1351 |      * @var array of array(question_attempt_step, question_attempt id, seq number)
 | 
        
           |  |  | 1352 |      * of steps that have been added to question attempts in this usage.
 | 
        
           |  |  | 1353 |      */
 | 
        
           |  |  | 1354 |     protected $stepsadded = array();
 | 
        
           |  |  | 1355 |   | 
        
           |  |  | 1356 |     /**
 | 
        
           |  |  | 1357 |      * @var array of array(question_attempt_step, question_attempt id, seq number)
 | 
        
           |  |  | 1358 |      * of steps that have been modified in their attempt.
 | 
        
           |  |  | 1359 |      */
 | 
        
           |  |  | 1360 |     protected $stepsmodified = array();
 | 
        
           |  |  | 1361 |   | 
        
           |  |  | 1362 |     /**
 | 
        
           |  |  | 1363 |      * @var question_attempt_step[] list of question_attempt_step.id => question_attempt_step of steps
 | 
        
           |  |  | 1364 |      * that were previously stored in the database, but which are no longer required.
 | 
        
           |  |  | 1365 |      */
 | 
        
           |  |  | 1366 |     protected $stepsdeleted = array();
 | 
        
           |  |  | 1367 |   | 
        
           |  |  | 1368 |     /**
 | 
        
           |  |  | 1369 |      * @var array int slot => string name => question_attempt.
 | 
        
           |  |  | 1370 |      */
 | 
        
           |  |  | 1371 |     protected $metadataadded = array();
 | 
        
           |  |  | 1372 |   | 
        
           |  |  | 1373 |     /**
 | 
        
           |  |  | 1374 |      * @var array int slot => string name => question_attempt.
 | 
        
           |  |  | 1375 |      */
 | 
        
           |  |  | 1376 |     protected $metadatamodified = array();
 | 
        
           |  |  | 1377 |   | 
        
           |  |  | 1378 |     /**
 | 
        
           |  |  | 1379 |      * Constructor.
 | 
        
           |  |  | 1380 |      * @param question_usage_by_activity $quba the usage to track.
 | 
        
           |  |  | 1381 |      */
 | 
        
           |  |  | 1382 |     public function __construct(question_usage_by_activity $quba) {
 | 
        
           |  |  | 1383 |         $this->quba = $quba;
 | 
        
           |  |  | 1384 |     }
 | 
        
           |  |  | 1385 |   | 
        
           |  |  | 1386 |     public function notify_modified() {
 | 
        
           |  |  | 1387 |         $this->modified = true;
 | 
        
           |  |  | 1388 |     }
 | 
        
           |  |  | 1389 |   | 
        
           |  |  | 1390 |     public function notify_attempt_added(question_attempt $qa) {
 | 
        
           |  |  | 1391 |         $this->attemptsadded[$qa->get_slot()] = $qa;
 | 
        
           |  |  | 1392 |     }
 | 
        
           |  |  | 1393 |   | 
        
           |  |  | 1394 |     public function notify_attempt_modified(question_attempt $qa) {
 | 
        
           |  |  | 1395 |         $slot = $qa->get_slot();
 | 
        
           |  |  | 1396 |         if (!array_key_exists($slot, $this->attemptsadded)) {
 | 
        
           |  |  | 1397 |             $this->attemptsmodified[$slot] = $qa;
 | 
        
           |  |  | 1398 |         }
 | 
        
           |  |  | 1399 |     }
 | 
        
           |  |  | 1400 |   | 
        
           |  |  | 1401 |     public function notify_attempt_moved(question_attempt $qa, $oldslot) {
 | 
        
           |  |  | 1402 |         $newslot = $qa->get_slot();
 | 
        
           |  |  | 1403 |   | 
        
           |  |  | 1404 |         if (array_key_exists($oldslot, $this->attemptsadded)) {
 | 
        
           |  |  | 1405 |             unset($this->attemptsadded[$oldslot]);
 | 
        
           |  |  | 1406 |             $this->attemptsadded[$newslot] = $qa;
 | 
        
           |  |  | 1407 |             return;
 | 
        
           |  |  | 1408 |         }
 | 
        
           |  |  | 1409 |   | 
        
           |  |  | 1410 |         if (array_key_exists($oldslot, $this->attemptsmodified)) {
 | 
        
           |  |  | 1411 |             unset($this->attemptsmodified[$oldslot]);
 | 
        
           |  |  | 1412 |         }
 | 
        
           |  |  | 1413 |         $this->attemptsmodified[$newslot] = $qa;
 | 
        
           |  |  | 1414 |   | 
        
           |  |  | 1415 |         if (array_key_exists($oldslot, $this->metadataadded)) {
 | 
        
           |  |  | 1416 |             $this->metadataadded[$newslot] = $this->metadataadded[$oldslot];
 | 
        
           |  |  | 1417 |             unset($this->metadataadded[$oldslot]);
 | 
        
           |  |  | 1418 |         }
 | 
        
           |  |  | 1419 |         if (array_key_exists($oldslot, $this->metadatamodified)) {
 | 
        
           |  |  | 1420 |             $this->metadatamodified[$newslot] = $this->metadatamodified[$oldslot];
 | 
        
           |  |  | 1421 |             unset($this->metadatamodified[$oldslot]);
 | 
        
           |  |  | 1422 |         }
 | 
        
           |  |  | 1423 |     }
 | 
        
           |  |  | 1424 |   | 
        
           |  |  | 1425 |     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
 | 
        
           |  |  | 1426 |         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
 | 
        
           |  |  | 1427 |             return;
 | 
        
           |  |  | 1428 |         }
 | 
        
           |  |  | 1429 |   | 
        
           |  |  | 1430 |         if (($key = $this->is_step_added($step)) !== false) {
 | 
        
           |  |  | 1431 |             return;
 | 
        
           |  |  | 1432 |         }
 | 
        
           |  |  | 1433 |   | 
        
           |  |  | 1434 |         if (($key = $this->is_step_modified($step)) !== false) {
 | 
        
           |  |  | 1435 |             throw new coding_exception('Cannot add a step that has already been modified.');
 | 
        
           |  |  | 1436 |         }
 | 
        
           |  |  | 1437 |   | 
        
           |  |  | 1438 |         if (($key = $this->is_step_deleted($step)) !== false) {
 | 
        
           |  |  | 1439 |             unset($this->stepsdeleted[$step->get_id()]);
 | 
        
           |  |  | 1440 |             $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
 | 
        
           |  |  | 1441 |             return;
 | 
        
           |  |  | 1442 |         }
 | 
        
           |  |  | 1443 |   | 
        
           |  |  | 1444 |         $stepid = $step->get_id();
 | 
        
           |  |  | 1445 |         if ($stepid) {
 | 
        
           |  |  | 1446 |             if (array_key_exists($stepid, $this->stepsdeleted)) {
 | 
        
           |  |  | 1447 |                 unset($this->stepsdeleted[$stepid]);
 | 
        
           |  |  | 1448 |             }
 | 
        
           |  |  | 1449 |             $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
 | 
        
           |  |  | 1450 |   | 
        
           |  |  | 1451 |         } else {
 | 
        
           |  |  | 1452 |             $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
 | 
        
           |  |  | 1453 |         }
 | 
        
           |  |  | 1454 |     }
 | 
        
           |  |  | 1455 |   | 
        
           |  |  | 1456 |     public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
 | 
        
           |  |  | 1457 |         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
 | 
        
           |  |  | 1458 |             return;
 | 
        
           |  |  | 1459 |         }
 | 
        
           |  |  | 1460 |   | 
        
           |  |  | 1461 |         if (($key = $this->is_step_added($step)) !== false) {
 | 
        
           |  |  | 1462 |             return;
 | 
        
           |  |  | 1463 |         }
 | 
        
           |  |  | 1464 |   | 
        
           |  |  | 1465 |         if (($key = $this->is_step_deleted($step)) !== false) {
 | 
        
           |  |  | 1466 |             throw new coding_exception('Cannot modify a step after it has been deleted.');
 | 
        
           |  |  | 1467 |         }
 | 
        
           |  |  | 1468 |   | 
        
           |  |  | 1469 |         $stepid = $step->get_id();
 | 
        
           |  |  | 1470 |         if (empty($stepid)) {
 | 
        
           |  |  | 1471 |             throw new coding_exception('Cannot modify a step that has never been stored in the database.');
 | 
        
           |  |  | 1472 |         }
 | 
        
           |  |  | 1473 |   | 
        
           |  |  | 1474 |         $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
 | 
        
           |  |  | 1475 |     }
 | 
        
           |  |  | 1476 |   | 
        
           |  |  | 1477 |     public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
 | 
        
           |  |  | 1478 |         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
 | 
        
           |  |  | 1479 |             return;
 | 
        
           |  |  | 1480 |         }
 | 
        
           |  |  | 1481 |   | 
        
           |  |  | 1482 |         if (($key = $this->is_step_added($step)) !== false) {
 | 
        
           |  |  | 1483 |             unset($this->stepsadded[$key]);
 | 
        
           |  |  | 1484 |             return;
 | 
        
           |  |  | 1485 |         }
 | 
        
           |  |  | 1486 |   | 
        
           |  |  | 1487 |         if (($key = $this->is_step_modified($step)) !== false) {
 | 
        
           |  |  | 1488 |             unset($this->stepsmodified[$key]);
 | 
        
           |  |  | 1489 |         }
 | 
        
           |  |  | 1490 |   | 
        
           |  |  | 1491 |         $stepid = $step->get_id();
 | 
        
           |  |  | 1492 |         if (empty($stepid)) {
 | 
        
           |  |  | 1493 |             return; // Was never in the database.
 | 
        
           |  |  | 1494 |         }
 | 
        
           |  |  | 1495 |   | 
        
           |  |  | 1496 |         $this->stepsdeleted[$stepid] = $step;
 | 
        
           |  |  | 1497 |     }
 | 
        
           |  |  | 1498 |   | 
        
           |  |  | 1499 |     public function notify_metadata_added(question_attempt $qa, $name) {
 | 
        
           |  |  | 1500 |         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
 | 
        
           |  |  | 1501 |             return;
 | 
        
           |  |  | 1502 |         }
 | 
        
           |  |  | 1503 |   | 
        
           |  |  | 1504 |         if ($this->is_step_added($qa->get_step(0)) !== false) {
 | 
        
           |  |  | 1505 |             return;
 | 
        
           |  |  | 1506 |         }
 | 
        
           |  |  | 1507 |   | 
        
           |  |  | 1508 |         if (isset($this->metadataadded[$qa->get_slot()][$name])) {
 | 
        
           |  |  | 1509 |             return;
 | 
        
           |  |  | 1510 |         }
 | 
        
           |  |  | 1511 |   | 
        
           |  |  | 1512 |         $this->metadataadded[$qa->get_slot()][$name] = $qa;
 | 
        
           |  |  | 1513 |     }
 | 
        
           |  |  | 1514 |   | 
        
           |  |  | 1515 |     public function notify_metadata_modified(question_attempt $qa, $name) {
 | 
        
           |  |  | 1516 |         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
 | 
        
           |  |  | 1517 |             return;
 | 
        
           |  |  | 1518 |         }
 | 
        
           |  |  | 1519 |   | 
        
           |  |  | 1520 |         if ($this->is_step_added($qa->get_step(0)) !== false) {
 | 
        
           |  |  | 1521 |             return;
 | 
        
           |  |  | 1522 |         }
 | 
        
           |  |  | 1523 |   | 
        
           |  |  | 1524 |         if (isset($this->metadataadded[$qa->get_slot()][$name])) {
 | 
        
           |  |  | 1525 |             return;
 | 
        
           |  |  | 1526 |         }
 | 
        
           |  |  | 1527 |   | 
        
           |  |  | 1528 |         if (isset($this->metadatamodified[$qa->get_slot()][$name])) {
 | 
        
           |  |  | 1529 |             return;
 | 
        
           |  |  | 1530 |         }
 | 
        
           |  |  | 1531 |   | 
        
           |  |  | 1532 |         $this->metadatamodified[$qa->get_slot()][$name] = $qa;
 | 
        
           |  |  | 1533 |     }
 | 
        
           |  |  | 1534 |   | 
        
           |  |  | 1535 |     /**
 | 
        
           |  |  | 1536 |      * Determine if a step is new. If so get its array key.
 | 
        
           |  |  | 1537 |      *
 | 
        
           |  |  | 1538 |      * @param question_attempt_step $step a step
 | 
        
           |  |  | 1539 |      * @return int|false if the step is in the list of steps to be added, return
 | 
        
           |  |  | 1540 |      *      the key, otherwise return false.
 | 
        
           |  |  | 1541 |      */
 | 
        
           |  |  | 1542 |     protected function is_step_added(question_attempt_step $step) {
 | 
        
           |  |  | 1543 |         foreach ($this->stepsadded as $key => $data) {
 | 
        
           |  |  | 1544 |             list($addedstep) = $data;
 | 
        
           |  |  | 1545 |             if ($addedstep === $step) {
 | 
        
           |  |  | 1546 |                 return $key;
 | 
        
           |  |  | 1547 |             }
 | 
        
           |  |  | 1548 |         }
 | 
        
           |  |  | 1549 |         return false;
 | 
        
           |  |  | 1550 |     }
 | 
        
           |  |  | 1551 |   | 
        
           |  |  | 1552 |     /**
 | 
        
           |  |  | 1553 |      * Determine if a step is modified. If so get its array key.
 | 
        
           |  |  | 1554 |      *
 | 
        
           |  |  | 1555 |      * @param question_attempt_step $step a step
 | 
        
           |  |  | 1556 |      * @return int|false if the step is in the list of steps to be modified, return
 | 
        
           |  |  | 1557 |      *      the key, otherwise return false.
 | 
        
           |  |  | 1558 |      */
 | 
        
           |  |  | 1559 |     protected function is_step_modified(question_attempt_step $step) {
 | 
        
           |  |  | 1560 |         foreach ($this->stepsmodified as $key => $data) {
 | 
        
           |  |  | 1561 |             list($modifiedstep) = $data;
 | 
        
           |  |  | 1562 |             if ($modifiedstep === $step) {
 | 
        
           |  |  | 1563 |                 return $key;
 | 
        
           |  |  | 1564 |             }
 | 
        
           |  |  | 1565 |         }
 | 
        
           |  |  | 1566 |         return false;
 | 
        
           |  |  | 1567 |     }
 | 
        
           |  |  | 1568 |   | 
        
           |  |  | 1569 |     /**
 | 
        
           |  |  | 1570 |      * @param question_attempt_step $step a step
 | 
        
           |  |  | 1571 |      * @return bool whether the step is in the list of steps to be deleted.
 | 
        
           |  |  | 1572 |      */
 | 
        
           |  |  | 1573 |     protected function is_step_deleted(question_attempt_step $step) {
 | 
        
           |  |  | 1574 |         foreach ($this->stepsdeleted as $deletedstep) {
 | 
        
           |  |  | 1575 |             if ($deletedstep === $step) {
 | 
        
           |  |  | 1576 |                 return true;
 | 
        
           |  |  | 1577 |             }
 | 
        
           |  |  | 1578 |         }
 | 
        
           |  |  | 1579 |         return false;
 | 
        
           |  |  | 1580 |     }
 | 
        
           |  |  | 1581 |   | 
        
           |  |  | 1582 |     /**
 | 
        
           |  |  | 1583 |      * Write all the changes we have recorded to the database.
 | 
        
           |  |  | 1584 |      * @param question_engine_data_mapper $dm the mapper to use to update the database.
 | 
        
           |  |  | 1585 |      */
 | 
        
           |  |  | 1586 |     public function save(question_engine_data_mapper $dm) {
 | 
        
           |  |  | 1587 |         $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context());
 | 
        
           |  |  | 1588 |   | 
        
           |  |  | 1589 |         // Initially an array of array of question_attempt_step_objects.
 | 
        
           |  |  | 1590 |         // Built as a nested array for efficiency, then flattened.
 | 
        
           |  |  | 1591 |         $stepdata = array();
 | 
        
           |  |  | 1592 |   | 
        
           |  |  | 1593 |         foreach ($this->stepsmodified as $stepinfo) {
 | 
        
           |  |  | 1594 |             list($step, $questionattemptid, $seq) = $stepinfo;
 | 
        
           |  |  | 1595 |             $stepdata[] = $dm->update_question_attempt_step(
 | 
        
           |  |  | 1596 |                     $step, $questionattemptid, $seq, $this->quba->get_owning_context());
 | 
        
           |  |  | 1597 |         }
 | 
        
           |  |  | 1598 |   | 
        
           |  |  | 1599 |         foreach ($this->stepsadded as $stepinfo) {
 | 
        
           |  |  | 1600 |             list($step, $questionattemptid, $seq) = $stepinfo;
 | 
        
           |  |  | 1601 |             $stepdata[] = $dm->insert_question_attempt_step(
 | 
        
           |  |  | 1602 |                     $step, $questionattemptid, $seq, $this->quba->get_owning_context());
 | 
        
           |  |  | 1603 |         }
 | 
        
           |  |  | 1604 |   | 
        
           |  |  | 1605 |         foreach ($this->attemptsmodified as $qa) {
 | 
        
           |  |  | 1606 |             $dm->update_question_attempt($qa);
 | 
        
           |  |  | 1607 |         }
 | 
        
           |  |  | 1608 |   | 
        
           |  |  | 1609 |         foreach ($this->attemptsadded as $qa) {
 | 
        
           |  |  | 1610 |             $stepdata[] = $dm->insert_question_attempt(
 | 
        
           |  |  | 1611 |                     $qa, $this->quba->get_owning_context());
 | 
        
           |  |  | 1612 |         }
 | 
        
           |  |  | 1613 |   | 
        
           |  |  | 1614 |         foreach ($this->metadataadded as $info) {
 | 
        
           |  |  | 1615 |             $qa = reset($info);
 | 
        
           |  |  | 1616 |             $stepdata[] = $dm->insert_question_attempt_metadata($qa, array_keys($info));
 | 
        
           |  |  | 1617 |         }
 | 
        
           |  |  | 1618 |   | 
        
           |  |  | 1619 |         foreach ($this->metadatamodified as $info) {
 | 
        
           |  |  | 1620 |             $qa = reset($info);
 | 
        
           |  |  | 1621 |             $stepdata[] = $dm->update_question_attempt_metadata($qa, array_keys($info));
 | 
        
           |  |  | 1622 |         }
 | 
        
           |  |  | 1623 |   | 
        
           |  |  | 1624 |         if ($this->modified) {
 | 
        
           |  |  | 1625 |             $dm->update_questions_usage_by_activity($this->quba);
 | 
        
           |  |  | 1626 |         }
 | 
        
           |  |  | 1627 |   | 
        
           |  |  | 1628 |         $dm->insert_all_step_data($dm->combine_step_data($stepdata));
 | 
        
           |  |  | 1629 |   | 
        
           |  |  | 1630 |         $this->stepsdeleted = array();
 | 
        
           |  |  | 1631 |         $this->stepsmodified = array();
 | 
        
           |  |  | 1632 |         $this->stepsadded = array();
 | 
        
           |  |  | 1633 |         $this->attemptsdeleted = array();
 | 
        
           |  |  | 1634 |         $this->attemptsadded = array();
 | 
        
           |  |  | 1635 |         $this->attemptsmodified = array();
 | 
        
           |  |  | 1636 |         $this->modified = false;
 | 
        
           |  |  | 1637 |     }
 | 
        
           |  |  | 1638 | }
 | 
        
           |  |  | 1639 |   | 
        
           |  |  | 1640 |   | 
        
           |  |  | 1641 | /**
 | 
        
           |  |  | 1642 |  * The interface implemented by {@link question_file_saver} and {@link question_file_loader}.
 | 
        
           |  |  | 1643 |  *
 | 
        
           |  |  | 1644 |  * @copyright  2012 The Open University
 | 
        
           |  |  | 1645 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1646 |  */
 | 
        
           |  |  | 1647 | interface question_response_files {
 | 
        
           |  |  | 1648 |     /**
 | 
        
           |  |  | 1649 |      * Get the files that were submitted.
 | 
        
           |  |  | 1650 |      * @return array of stored_files objects.
 | 
        
           |  |  | 1651 |      */
 | 
        
           |  |  | 1652 |     public function get_files();
 | 
        
           |  |  | 1653 | }
 | 
        
           |  |  | 1654 |   | 
        
           |  |  | 1655 |   | 
        
           |  |  | 1656 | /**
 | 
        
           |  |  | 1657 |  * This class represents the promise to save some files from a particular draft
 | 
        
           |  |  | 1658 |  * file area into a particular file area. It is used beause the necessary
 | 
        
           |  |  | 1659 |  * information about what to save is to hand in the
 | 
        
           |  |  | 1660 |  * {@link question_attempt::process_response_files()} method, but we don't know
 | 
        
           |  |  | 1661 |  * if this question attempt will actually be saved in the database until later,
 | 
        
           |  |  | 1662 |  * when the {@link question_engine_unit_of_work} is saved, if it is.
 | 
        
           |  |  | 1663 |  *
 | 
        
           |  |  | 1664 |  * @copyright  2011 The Open University
 | 
        
           |  |  | 1665 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1666 |  */
 | 
        
           |  |  | 1667 | class question_file_saver implements question_response_files {
 | 
        
           |  |  | 1668 |     /** @var int the id of the draft file area to save files from. */
 | 
        
           |  |  | 1669 |     protected $draftitemid;
 | 
        
           |  |  | 1670 |     /** @var string the owning component name. */
 | 
        
           |  |  | 1671 |     protected $component;
 | 
        
           |  |  | 1672 |     /** @var string the file area name. */
 | 
        
           |  |  | 1673 |     protected $filearea;
 | 
        
           |  |  | 1674 |   | 
        
           |  |  | 1675 |     /**
 | 
        
           |  |  | 1676 |      * @var string the value to store in the question_attempt_step_data to
 | 
        
           |  |  | 1677 |      * represent these files.
 | 
        
           |  |  | 1678 |      */
 | 
        
           |  |  | 1679 |     protected $value = null;
 | 
        
           |  |  | 1680 |   | 
        
           |  |  | 1681 |     /**
 | 
        
           |  |  | 1682 |      * Constructor.
 | 
        
           |  |  | 1683 |      *
 | 
        
           |  |  | 1684 |      * @param int $draftitemid the draft area to save the files from.
 | 
        
           |  |  | 1685 |      * @param string $component the component for the file area to save into.
 | 
        
           |  |  | 1686 |      * @param string $uncleanedfilearea the name of the file area to save into - but before it has been cleaned up.
 | 
        
           |  |  | 1687 |      * @param string $text optional content containing file links.
 | 
        
           |  |  | 1688 |      */
 | 
        
           |  |  | 1689 |     public function __construct($draftitemid, $component, $uncleanedfilearea, $text = null) {
 | 
        
           |  |  | 1690 |         $this->draftitemid = $draftitemid;
 | 
        
           |  |  | 1691 |         $this->component = $component;
 | 
        
           |  |  | 1692 |         $this->filearea = self::clean_file_area_name($uncleanedfilearea);
 | 
        
           |  |  | 1693 |         $this->value = $this->compute_value($draftitemid, $text);
 | 
        
           |  |  | 1694 |     }
 | 
        
           |  |  | 1695 |   | 
        
           |  |  | 1696 |     /**
 | 
        
           |  |  | 1697 |      * Compute the value that should be stored in the question_attempt_step_data table.
 | 
        
           |  |  | 1698 |      *
 | 
        
           |  |  | 1699 |      * Contains a hash that (almost) uniquely encodes all the files.
 | 
        
           |  |  | 1700 |      *
 | 
        
           |  |  | 1701 |      * @param int $draftitemid the draft file area itemid.
 | 
        
           |  |  | 1702 |      * @param string $text optional content containing file links.
 | 
        
           |  |  | 1703 |      * @return string the value.
 | 
        
           |  |  | 1704 |      */
 | 
        
           |  |  | 1705 |     protected function compute_value($draftitemid, $text) {
 | 
        
           |  |  | 1706 |         global $USER;
 | 
        
           |  |  | 1707 |   | 
        
           |  |  | 1708 |         $fs = get_file_storage();
 | 
        
           |  |  | 1709 |         $usercontext = context_user::instance($USER->id);
 | 
        
           |  |  | 1710 |   | 
        
           |  |  | 1711 |         $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
 | 
        
           |  |  | 1712 |                 $draftitemid, 'sortorder, filepath, filename', false);
 | 
        
           |  |  | 1713 |   | 
        
           |  |  | 1714 |         $string = '';
 | 
        
           |  |  | 1715 |         foreach ($files as $file) {
 | 
        
           |  |  | 1716 |             $string .= $file->get_filepath() . $file->get_filename() . '|' .
 | 
        
           |  |  | 1717 |                     $file->get_contenthash() . '|';
 | 
        
           |  |  | 1718 |         }
 | 
        
           |  |  | 1719 |         $hash = md5($string);
 | 
        
           |  |  | 1720 |   | 
        
           |  |  | 1721 |         if (is_null($text)) {
 | 
        
           |  |  | 1722 |             if ($string) {
 | 
        
           |  |  | 1723 |                 return $hash;
 | 
        
           |  |  | 1724 |             } else {
 | 
        
           |  |  | 1725 |                 return '';
 | 
        
           |  |  | 1726 |             }
 | 
        
           |  |  | 1727 |         }
 | 
        
           |  |  | 1728 |   | 
        
           |  |  | 1729 |         // We add the file hash so a simple string comparison will say if the
 | 
        
           |  |  | 1730 |         // files have been changed. First strip off any existing file hash.
 | 
        
           |  |  | 1731 |         if ($text !== '') {
 | 
        
           |  |  | 1732 |             $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
 | 
        
           |  |  | 1733 |             $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
 | 
        
           |  |  | 1734 |             if ($string) {
 | 
        
           |  |  | 1735 |                 $text .= '<!-- File hash: ' . $hash . ' -->';
 | 
        
           |  |  | 1736 |             }
 | 
        
           |  |  | 1737 |         }
 | 
        
           |  |  | 1738 |         return $text;
 | 
        
           |  |  | 1739 |     }
 | 
        
           |  |  | 1740 |   | 
        
           |  |  | 1741 |     public function __toString() {
 | 
        
           |  |  | 1742 |         return $this->value;
 | 
        
           |  |  | 1743 |     }
 | 
        
           |  |  | 1744 |   | 
        
           |  |  | 1745 |     /**
 | 
        
           |  |  | 1746 |      * Actually save the files.
 | 
        
           |  |  | 1747 |      *
 | 
        
           |  |  | 1748 |      * @param integer $itemid the item id for the file area to save into.
 | 
        
           |  |  | 1749 |      * @param context $context the context where the files should be saved.
 | 
        
           |  |  | 1750 |      */
 | 
        
           |  |  | 1751 |     public function save_files($itemid, $context) {
 | 
        
           |  |  | 1752 |         file_save_draft_area_files($this->draftitemid, $context->id,
 | 
        
           |  |  | 1753 |                 $this->component, $this->filearea, $itemid);
 | 
        
           |  |  | 1754 |     }
 | 
        
           |  |  | 1755 |   | 
        
           |  |  | 1756 |     /**
 | 
        
           |  |  | 1757 |      * Clean up a possible file area name to ensure that it matches the required rules.
 | 
        
           |  |  | 1758 |      *
 | 
        
           |  |  | 1759 |      * @param string $uncleanedfilearea the proposed file area name (e.g. 'response_-attachments').
 | 
        
           |  |  | 1760 |      * @return string a similar valid file area name. E.g: response_attachments.
 | 
        
           |  |  | 1761 |      */
 | 
        
           |  |  | 1762 |     public static function clean_file_area_name(string $uncleanedfilearea): string {
 | 
        
           |  |  | 1763 |         $filearea = $uncleanedfilearea;
 | 
        
           |  |  | 1764 |         if ($filearea !== clean_param($filearea, PARAM_AREA)) {
 | 
        
           |  |  | 1765 |             // Only lowercase ascii letters, numbers and underscores are allowed.
 | 
        
           |  |  | 1766 |             // Remove the invalid character in the filearea string.
 | 
        
           |  |  | 1767 |             $filearea = preg_replace('~[^a-z0-9_]~', '', core_text::strtolower($filearea));
 | 
        
           |  |  | 1768 |             // Replace multiple underscore to a single underscore.
 | 
        
           |  |  | 1769 |             $filearea = preg_replace('~_+~', '_', $filearea);
 | 
        
           |  |  | 1770 |             // If, after attempted cleaning, the filearea is not valid, throw a clear error to avoid subtle bugs.
 | 
        
           |  |  | 1771 |             if ($filearea !== clean_param($filearea, PARAM_AREA)) {
 | 
        
           |  |  | 1772 |                 throw new coding_exception('Name ' . $filearea .
 | 
        
           |  |  | 1773 |                     ' cannot be used with question_file_saver because it does not match the rules for file area names');
 | 
        
           |  |  | 1774 |             }
 | 
        
           |  |  | 1775 |         }
 | 
        
           |  |  | 1776 |         return $filearea;
 | 
        
           |  |  | 1777 |     }
 | 
        
           |  |  | 1778 |   | 
        
           |  |  | 1779 |     /**
 | 
        
           |  |  | 1780 |      * Get the files that were submitted.
 | 
        
           |  |  | 1781 |      * @return array of stored_files objects.
 | 
        
           |  |  | 1782 |      */
 | 
        
           |  |  | 1783 |     public function get_files() {
 | 
        
           |  |  | 1784 |         global $USER;
 | 
        
           |  |  | 1785 |   | 
        
           |  |  | 1786 |         $fs = get_file_storage();
 | 
        
           |  |  | 1787 |         $usercontext = context_user::instance($USER->id);
 | 
        
           |  |  | 1788 |   | 
        
           |  |  | 1789 |         return $fs->get_area_files($usercontext->id, 'user', 'draft',
 | 
        
           |  |  | 1790 |                 $this->draftitemid, 'sortorder, filepath, filename', false);
 | 
        
           |  |  | 1791 |     }
 | 
        
           |  |  | 1792 | }
 | 
        
           |  |  | 1793 |   | 
        
           |  |  | 1794 |   | 
        
           |  |  | 1795 | /**
 | 
        
           |  |  | 1796 |  * This class is the mirror image of {@link question_file_saver}. It allows
 | 
        
           |  |  | 1797 |  * files to be accessed again later (e.g. when re-grading) using that same
 | 
        
           |  |  | 1798 |  * API as when doing the original grading.
 | 
        
           |  |  | 1799 |  *
 | 
        
           |  |  | 1800 |  * @copyright  2012 The Open University
 | 
        
           |  |  | 1801 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1802 |  */
 | 
        
           |  |  | 1803 | class question_file_loader implements question_response_files {
 | 
        
           |  |  | 1804 |     /** @var question_attempt_step the step that these files belong to. */
 | 
        
           |  |  | 1805 |     protected $step;
 | 
        
           |  |  | 1806 |   | 
        
           |  |  | 1807 |     /** @var string the field name for these files - which is used to construct the file area name. */
 | 
        
           |  |  | 1808 |     protected $name;
 | 
        
           |  |  | 1809 |   | 
        
           |  |  | 1810 |     /**
 | 
        
           |  |  | 1811 |      * @var string the value to stored in the question_attempt_step_data to
 | 
        
           |  |  | 1812 |      * represent these files.
 | 
        
           |  |  | 1813 |      */
 | 
        
           |  |  | 1814 |     protected $value;
 | 
        
           |  |  | 1815 |   | 
        
           |  |  | 1816 |     /** @var int the context id that the files belong to. */
 | 
        
           |  |  | 1817 |     protected $contextid;
 | 
        
           |  |  | 1818 |   | 
        
           |  |  | 1819 |     /**
 | 
        
           |  |  | 1820 |      * Constuctor.
 | 
        
           |  |  | 1821 |      * @param question_attempt_step $step the step that these files belong to.
 | 
        
           |  |  | 1822 |      * @param string $name string the field name for these files - which is used to construct the file area name.
 | 
        
           |  |  | 1823 |      * @param string $value the value to stored in the question_attempt_step_data to
 | 
        
           |  |  | 1824 |      *      represent these files.
 | 
        
           |  |  | 1825 |      * @param int $contextid the context id that the files belong to.
 | 
        
           |  |  | 1826 |      */
 | 
        
           |  |  | 1827 |     public function __construct(question_attempt_step $step, $name, $value, $contextid) {
 | 
        
           |  |  | 1828 |         $this->step = $step;
 | 
        
           |  |  | 1829 |         $this->name = $name;
 | 
        
           |  |  | 1830 |         $this->value = $value;
 | 
        
           |  |  | 1831 |         $this->contextid = $contextid;
 | 
        
           |  |  | 1832 |     }
 | 
        
           |  |  | 1833 |   | 
        
           |  |  | 1834 |     public function __toString() {
 | 
        
           |  |  | 1835 |         return $this->value;
 | 
        
           |  |  | 1836 |     }
 | 
        
           |  |  | 1837 |   | 
        
           |  |  | 1838 |     /**
 | 
        
           |  |  | 1839 |      * Get the files that were submitted.
 | 
        
           |  |  | 1840 |      * @return array of stored_files objects.
 | 
        
           |  |  | 1841 |      */
 | 
        
           |  |  | 1842 |     public function get_files() {
 | 
        
           |  |  | 1843 |         return $this->step->get_qt_files($this->name, $this->contextid);
 | 
        
           |  |  | 1844 |     }
 | 
        
           |  |  | 1845 |   | 
        
           |  |  | 1846 |     /**
 | 
        
           |  |  | 1847 |      * Copy these files into a draft area, and return the corresponding
 | 
        
           |  |  | 1848 |      * {@link question_file_saver} that can save them again.
 | 
        
           |  |  | 1849 |      *
 | 
        
           |  |  | 1850 |      * This is used by {@link question_attempt::start_based_on()}, which is used
 | 
        
           |  |  | 1851 |      * (for example) by the quizzes 'Each attempt builds on last' feature.
 | 
        
           |  |  | 1852 |      *
 | 
        
           |  |  | 1853 |      * @return question_file_saver that can re-save these files again.
 | 
        
           |  |  | 1854 |      */
 | 
        
           |  |  | 1855 |     public function get_question_file_saver() {
 | 
        
           |  |  | 1856 |   | 
        
           |  |  | 1857 |         // There are three possibilities here for what $value will look like:
 | 
        
           |  |  | 1858 |         // 1) some HTML content followed by an MD5 hash in a HTML comment;
 | 
        
           |  |  | 1859 |         // 2) a plain MD5 hash;
 | 
        
           |  |  | 1860 |         // 3) or some real content, without any hash.
 | 
        
           |  |  | 1861 |         // The problem is that 3) is ambiguous in the case where a student writes
 | 
        
           |  |  | 1862 |         // a response that looks exactly like an MD5 hash. For attempts made now,
 | 
        
           |  |  | 1863 |         // we avoid case 3) by always going for case 1) or 2) (except when the
 | 
        
           |  |  | 1864 |         // response is blank. However, there may be case 3) data in the database
 | 
        
           |  |  | 1865 |         // so we need to handle it as best we can.
 | 
        
           |  |  | 1866 |         if (preg_match('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', $this->value)) {
 | 
        
           |  |  | 1867 |             $value = preg_replace('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', '', $this->value);
 | 
        
           |  |  | 1868 |   | 
        
           |  |  | 1869 |         } else if (preg_match('/^[0-9a-zA-Z]{32}$/', $this->value)) {
 | 
        
           |  |  | 1870 |             $value = null;
 | 
        
           |  |  | 1871 |   | 
        
           |  |  | 1872 |         } else {
 | 
        
           |  |  | 1873 |             $value = $this->value;
 | 
        
           |  |  | 1874 |         }
 | 
        
           |  |  | 1875 |   | 
        
           |  |  | 1876 |         list($draftid, $text) = $this->step->prepare_response_files_draft_itemid_with_text(
 | 
        
           |  |  | 1877 |                 $this->name, $this->contextid, $value);
 | 
        
           |  |  | 1878 |         return new question_file_saver($draftid, 'question', 'response_' . $this->name, $text);
 | 
        
           |  |  | 1879 |     }
 | 
        
           |  |  | 1880 | }
 | 
        
           |  |  | 1881 |   | 
        
           |  |  | 1882 |   | 
        
           |  |  | 1883 | /**
 | 
        
           |  |  | 1884 |  * This class represents a restriction on the set of question_usage ids to include
 | 
        
           |  |  | 1885 |  * in a larger database query. Depending of the how you are going to restrict the
 | 
        
           |  |  | 1886 |  * list of usages, construct an appropriate subclass.
 | 
        
           |  |  | 1887 |  *
 | 
        
           |  |  | 1888 |  * If $qubaids is an instance of this class, example usage might be
 | 
        
           |  |  | 1889 |  *
 | 
        
           |  |  | 1890 |  * SELECT qa.id, qa.maxmark
 | 
        
           |  |  | 1891 |  * FROM $qubaids->from_question_attempts('qa')
 | 
        
           |  |  | 1892 |  * WHERE $qubaids->where() AND qa.slot = 1
 | 
        
           |  |  | 1893 |  *
 | 
        
           |  |  | 1894 |  * @copyright  2010 The Open University
 | 
        
           |  |  | 1895 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1896 |  */
 | 
        
           |  |  | 1897 | abstract class qubaid_condition {
 | 
        
           |  |  | 1898 |   | 
        
           |  |  | 1899 |     /**
 | 
        
           |  |  | 1900 |      * Get the SQL fragment to go in a FROM clause.
 | 
        
           |  |  | 1901 |      *
 | 
        
           |  |  | 1902 |      * The SQL that needs to go in the FROM clause when trying
 | 
        
           |  |  | 1903 |      * to select records from the 'question_attempts' table based on this
 | 
        
           |  |  | 1904 |      * qubaid_condition.
 | 
        
           |  |  | 1905 |      *
 | 
        
           |  |  | 1906 |      * @param string $alias
 | 
        
           |  |  | 1907 |      * @return string SQL fragment.
 | 
        
           |  |  | 1908 |      */
 | 
        
           |  |  | 1909 |     abstract public function from_question_attempts($alias);
 | 
        
           |  |  | 1910 |   | 
        
           |  |  | 1911 |     /** @return string the SQL that needs to go in the where clause. */
 | 
        
           |  |  | 1912 |     abstract public function where();
 | 
        
           |  |  | 1913 |   | 
        
           |  |  | 1914 |     /**
 | 
        
           |  |  | 1915 |      * @return array the params needed by a query that uses
 | 
        
           |  |  | 1916 |      * {@link from_question_attempts()} and {@link where()}.
 | 
        
           |  |  | 1917 |      */
 | 
        
           |  |  | 1918 |     abstract public function from_where_params();
 | 
        
           |  |  | 1919 |   | 
        
           |  |  | 1920 |     /**
 | 
        
           |  |  | 1921 |      * @return string SQL that can use used in a WHERE qubaid IN (...) query.
 | 
        
           |  |  | 1922 |      * This method returns the "IN (...)" part.
 | 
        
           |  |  | 1923 |      */
 | 
        
           |  |  | 1924 |     abstract public function usage_id_in();
 | 
        
           |  |  | 1925 |   | 
        
           |  |  | 1926 |     /**
 | 
        
           |  |  | 1927 |      * @return array the params needed by a query that uses {@link usage_id_in()}.
 | 
        
           |  |  | 1928 |      */
 | 
        
           |  |  | 1929 |     abstract public function usage_id_in_params();
 | 
        
           |  |  | 1930 |   | 
        
           |  |  | 1931 |     /**
 | 
        
           |  |  | 1932 |      * @return string 40-character hash code that uniquely identifies the combination of properties and class name of this qubaid
 | 
        
           |  |  | 1933 |      *                  condition.
 | 
        
           |  |  | 1934 |      */
 | 
        
           |  |  | 1935 |     public function get_hash_code() {
 | 
        
           |  |  | 1936 |         return sha1(serialize($this));
 | 
        
           |  |  | 1937 |     }
 | 
        
           |  |  | 1938 | }
 | 
        
           |  |  | 1939 |   | 
        
           |  |  | 1940 |   | 
        
           |  |  | 1941 | /**
 | 
        
           |  |  | 1942 |  * This class represents a restriction on the set of question_usage ids to include
 | 
        
           |  |  | 1943 |  * in a larger database query based on an explicit list of ids.
 | 
        
           |  |  | 1944 |  *
 | 
        
           |  |  | 1945 |  * @copyright  2010 The Open University
 | 
        
           |  |  | 1946 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1947 |  */
 | 
        
           |  |  | 1948 | class qubaid_list extends qubaid_condition {
 | 
        
           |  |  | 1949 |     /** @var array of ids. */
 | 
        
           |  |  | 1950 |     protected $qubaids;
 | 
        
           |  |  | 1951 |     protected $columntotest = null;
 | 
        
           |  |  | 1952 |     protected $params;
 | 
        
           |  |  | 1953 |   | 
        
           |  |  | 1954 |     /**
 | 
        
           |  |  | 1955 |      * Constructor.
 | 
        
           |  |  | 1956 |      * @param array $qubaids of question usage ids.
 | 
        
           |  |  | 1957 |      */
 | 
        
           |  |  | 1958 |     public function __construct(array $qubaids) {
 | 
        
           |  |  | 1959 |         $this->qubaids = $qubaids;
 | 
        
           |  |  | 1960 |     }
 | 
        
           |  |  | 1961 |   | 
        
           |  |  | 1962 |     public function from_question_attempts($alias) {
 | 
        
           |  |  | 1963 |         $this->columntotest = $alias . '.questionusageid';
 | 
        
           |  |  | 1964 |         return '{question_attempts} ' . $alias;
 | 
        
           |  |  | 1965 |     }
 | 
        
           |  |  | 1966 |   | 
        
           |  |  | 1967 |     public function where() {
 | 
        
           |  |  | 1968 |         if (is_null($this->columntotest)) {
 | 
        
           |  |  | 1969 |             throw new coding_exception('Must call from_question_attempts before where().');
 | 
        
           |  |  | 1970 |         }
 | 
        
           |  |  | 1971 |         if (empty($this->qubaids)) {
 | 
        
           |  |  | 1972 |             $this->params = array();
 | 
        
           |  |  | 1973 |             return '1 = 0';
 | 
        
           |  |  | 1974 |         }
 | 
        
           |  |  | 1975 |   | 
        
           |  |  | 1976 |         return $this->columntotest . ' ' . $this->usage_id_in();
 | 
        
           |  |  | 1977 |     }
 | 
        
           |  |  | 1978 |   | 
        
           |  |  | 1979 |     public function from_where_params() {
 | 
        
           |  |  | 1980 |         return $this->params;
 | 
        
           |  |  | 1981 |     }
 | 
        
           |  |  | 1982 |   | 
        
           |  |  | 1983 |     public function usage_id_in() {
 | 
        
           |  |  | 1984 |         global $DB;
 | 
        
           |  |  | 1985 |   | 
        
           |  |  | 1986 |         if (empty($this->qubaids)) {
 | 
        
           |  |  | 1987 |             $this->params = array();
 | 
        
           |  |  | 1988 |             return '= 0';
 | 
        
           |  |  | 1989 |         }
 | 
        
           |  |  | 1990 |         list($where, $this->params) = $DB->get_in_or_equal(
 | 
        
           |  |  | 1991 |                 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid');
 | 
        
           |  |  | 1992 |         return $where;
 | 
        
           |  |  | 1993 |     }
 | 
        
           |  |  | 1994 |   | 
        
           |  |  | 1995 |     public function usage_id_in_params() {
 | 
        
           |  |  | 1996 |         return $this->params;
 | 
        
           |  |  | 1997 |     }
 | 
        
           |  |  | 1998 | }
 | 
        
           |  |  | 1999 |   | 
        
           |  |  | 2000 |   | 
        
           |  |  | 2001 | /**
 | 
        
           |  |  | 2002 |  * This class represents a restriction on the set of question_usage ids to include
 | 
        
           |  |  | 2003 |  * in a larger database query based on JOINing to some other tables.
 | 
        
           |  |  | 2004 |  *
 | 
        
           |  |  | 2005 |  * The general form of the query is something like
 | 
        
           |  |  | 2006 |  *
 | 
        
           |  |  | 2007 |  * SELECT qa.id, qa.maxmark
 | 
        
           |  |  | 2008 |  * FROM $from
 | 
        
           |  |  | 2009 |  * JOIN {question_attempts} qa ON qa.questionusageid = $usageidcolumn
 | 
        
           |  |  | 2010 |  * WHERE $where AND qa.slot = 1
 | 
        
           |  |  | 2011 |  *
 | 
        
           |  |  | 2012 |  * where $from, $usageidcolumn and $where are the arguments to the constructor.
 | 
        
           |  |  | 2013 |  *
 | 
        
           |  |  | 2014 |  * @copyright  2010 The Open University
 | 
        
           |  |  | 2015 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 2016 |  */
 | 
        
           |  |  | 2017 | class qubaid_join extends qubaid_condition {
 | 
        
           |  |  | 2018 |     public $from;
 | 
        
           |  |  | 2019 |     public $usageidcolumn;
 | 
        
           |  |  | 2020 |     public $where;
 | 
        
           |  |  | 2021 |     public $params;
 | 
        
           |  |  | 2022 |   | 
        
           |  |  | 2023 |     /**
 | 
        
           |  |  | 2024 |      * Constructor. The meaning of the arguments is explained in the class comment.
 | 
        
           |  |  | 2025 |      * @param string $from SQL fragemnt to go in the FROM clause.
 | 
        
           |  |  | 2026 |      * @param string $usageidcolumn the column in $from that should be
 | 
        
           |  |  | 2027 |      * made equal to the usageid column in the JOIN clause.
 | 
        
           |  |  | 2028 |      * @param string $where SQL fragment to go in the where clause.
 | 
        
           |  |  | 2029 |      * @param array $params required by the SQL. You must use named parameters.
 | 
        
           |  |  | 2030 |      */
 | 
        
           |  |  | 2031 |     public function __construct($from, $usageidcolumn, $where = '', $params = array()) {
 | 
        
           |  |  | 2032 |         $this->from = $from;
 | 
        
           |  |  | 2033 |         $this->usageidcolumn = $usageidcolumn;
 | 
        
           |  |  | 2034 |         $this->params = $params;
 | 
        
           |  |  | 2035 |         if (empty($where)) {
 | 
        
           |  |  | 2036 |             $where = '1 = 1';
 | 
        
           |  |  | 2037 |         }
 | 
        
           |  |  | 2038 |         $this->where = $where;
 | 
        
           |  |  | 2039 |     }
 | 
        
           |  |  | 2040 |   | 
        
           |  |  | 2041 |     public function from_question_attempts($alias) {
 | 
        
           |  |  | 2042 |         return "{$this->from}
 | 
        
           |  |  | 2043 |                 JOIN {question_attempts} {$alias} ON " .
 | 
        
           |  |  | 2044 |                         "{$alias}.questionusageid = $this->usageidcolumn";
 | 
        
           |  |  | 2045 |     }
 | 
        
           |  |  | 2046 |   | 
        
           |  |  | 2047 |     public function where() {
 | 
        
           |  |  | 2048 |         return $this->where;
 | 
        
           |  |  | 2049 |     }
 | 
        
           |  |  | 2050 |   | 
        
           |  |  | 2051 |     public function from_where_params() {
 | 
        
           |  |  | 2052 |         return $this->params;
 | 
        
           |  |  | 2053 |     }
 | 
        
           |  |  | 2054 |   | 
        
           |  |  | 2055 |     public function usage_id_in() {
 | 
        
           |  |  | 2056 |         return "IN (SELECT {$this->usageidcolumn} FROM {$this->from} WHERE {$this->where})";
 | 
        
           |  |  | 2057 |     }
 | 
        
           |  |  | 2058 |   | 
        
           |  |  | 2059 |     public function usage_id_in_params() {
 | 
        
           |  |  | 2060 |         return $this->params;
 | 
        
           |  |  | 2061 |     }
 | 
        
           |  |  | 2062 | }
 |