[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/engine/ -> datalib.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1