Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   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  namespace core_question;
  18  
  19  use qubaid_list;
  20  use question_bank;
  21  use question_engine;
  22  use question_engine_data_mapper;
  23  use question_state;
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  global $CFG;
  28  require_once (__DIR__ . '/../lib.php');
  29  require_once (__DIR__ . '/helpers.php');
  30  
  31  /**
  32   * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
  33   *
  34   * @package   core_question
  35   * @category  test
  36   * @copyright 2013 The Open University
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class datalib_reporting_queries_test extends \qbehaviour_walkthrough_test_base {
  40  
  41      /** @var question_engine_data_mapper */
  42      protected $dm;
  43  
  44      /** @var qtype_shortanswer_question */
  45      protected $sa;
  46  
  47      /** @var qtype_essay_question */
  48      protected $essay;
  49  
  50      /** @var array */
  51      protected $usageids = array();
  52  
  53      /** @var qubaid_condition */
  54      protected $bothusages;
  55  
  56      /** @var array */
  57      protected $allslots = array();
  58  
  59      /**
  60       * Test the various methods that load data for reporting.
  61       *
  62       * Since these methods need an expensive set-up, and then only do read-only
  63       * operations on the data, we use a single method to do the set-up, which
  64       * calls diffents methods to test each query.
  65       */
  66      public function test_reporting_queries() {
  67          // We create two usages, each with two questions, a short-answer marked
  68          // out of 5, and and essay marked out of 10.
  69          //
  70          // In the first usage, the student answers the short-answer
  71          // question correctly, and enters something in the essay.
  72          //
  73          // In the second useage, the student answers the short-answer question
  74          // wrongly, and leaves the essay blank.
  75          $this->resetAfterTest();
  76          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  77          $cat = $generator->create_question_category();
  78          $this->sa = $generator->create_question('shortanswer', null,
  79                  array('category' => $cat->id));
  80          $this->essay = $generator->create_question('essay', null,
  81                  array('category' => $cat->id));
  82  
  83          $this->usageids = array();
  84  
  85          // Create the first usage.
  86          $q = question_bank::load_question($this->sa->id);
  87          $this->start_attempt_at_question($q, 'interactive', 5);
  88          $this->allslots[] = $this->slot;
  89          $this->process_submission(array('answer' => 'cat'));
  90          $this->process_submission(array('answer' => 'frog', '-submit' => 1));
  91  
  92          $q = question_bank::load_question($this->essay->id);
  93          $this->start_attempt_at_question($q, 'interactive', 10);
  94          $this->allslots[] = $this->slot;
  95          $this->process_submission(array('answer' => '<p>The cat sat on the mat.</p>', 'answerformat' => FORMAT_HTML));
  96  
  97          $this->finish();
  98          $this->save_quba();
  99          $this->usageids[] = $this->quba->get_id();
 100  
 101          // Create the second usage.
 102          $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
 103                  \context_system::instance());
 104  
 105          $q = question_bank::load_question($this->sa->id);
 106          $this->start_attempt_at_question($q, 'interactive', 5);
 107          $this->process_submission(array('answer' => 'fish'));
 108  
 109          $q = question_bank::load_question($this->essay->id);
 110          $this->start_attempt_at_question($q, 'interactive', 10);
 111  
 112          $this->finish();
 113          $this->save_quba();
 114          $this->usageids[] = $this->quba->get_id();
 115  
 116          // Set up some things the tests will need.
 117          $this->dm = new question_engine_data_mapper();
 118          $this->bothusages = new qubaid_list($this->usageids);
 119  
 120          // Now test the various queries.
 121          $this->dotest_load_questions_usages_latest_steps($this->allslots);
 122          $this->dotest_load_questions_usages_latest_steps(null);
 123          $this->dotest_load_questions_usages_question_state_summary($this->allslots);
 124          $this->dotest_load_questions_usages_question_state_summary(null);
 125          $this->dotest_load_questions_usages_where_question_in_state();
 126          $this->dotest_load_average_marks($this->allslots);
 127          $this->dotest_load_average_marks(null);
 128          $this->dotest_sum_usage_marks_subquery();
 129          $this->dotest_question_attempt_latest_state_view();
 130      }
 131  
 132      /**
 133       * This test is executed by {@link test_reporting_queries()}.
 134       *
 135       * @param array|null $slots list of slots to use in the call.
 136       */
 137      protected function dotest_load_questions_usages_latest_steps($slots) {
 138          $rawstates = $this->dm->load_questions_usages_latest_steps($this->bothusages, $slots,
 139                  'qa.id AS questionattemptid, qa.questionusageid, qa.slot, ' .
 140                  'qa.questionid, qa.maxmark, qas.sequencenumber, qas.state');
 141  
 142          $states = array();
 143          foreach ($rawstates as $state) {
 144              $states[$state->questionusageid][$state->slot] = $state;
 145              unset($state->questionattemptid);
 146              unset($state->questionusageid);
 147              unset($state->slot);
 148          }
 149  
 150          $state = $states[$this->usageids[0]][$this->allslots[0]];
 151          $this->assertEquals((object) array(
 152              'questionid'     => $this->sa->id,
 153              'maxmark'        => 5.0,
 154              'sequencenumber' => 2,
 155              'state'          => (string) question_state::$gradedright,
 156          ), $state);
 157  
 158          $state = $states[$this->usageids[0]][$this->allslots[1]];
 159          $this->assertEquals((object) array(
 160              'questionid'     => $this->essay->id,
 161              'maxmark'        => 10.0,
 162              'sequencenumber' => 2,
 163              'state'          => (string) question_state::$needsgrading,
 164          ), $state);
 165  
 166          $state = $states[$this->usageids[1]][$this->allslots[0]];
 167          $this->assertEquals((object) array(
 168              'questionid'     => $this->sa->id,
 169              'maxmark'        => 5.0,
 170              'sequencenumber' => 2,
 171              'state'          => (string) question_state::$gradedwrong,
 172          ), $state);
 173  
 174          $state = $states[$this->usageids[1]][$this->allslots[1]];
 175          $this->assertEquals((object) array(
 176              'questionid'     => $this->essay->id,
 177              'maxmark'        => 10.0,
 178              'sequencenumber' => 1,
 179              'state'          => (string) question_state::$gaveup,
 180          ), $state);
 181      }
 182  
 183      /**
 184       * This test is executed by {@link test_reporting_queries()}.
 185       *
 186       * @param array|null $slots list of slots to use in the call.
 187       */
 188      protected function dotest_load_questions_usages_question_state_summary($slots) {
 189          $summary = $this->dm->load_questions_usages_question_state_summary(
 190                  $this->bothusages, $slots);
 191  
 192          $this->assertEquals($summary[$this->allslots[0] . ',' . $this->sa->id],
 193                  (object) array(
 194                      'slot' => $this->allslots[0],
 195                      'questionid' => $this->sa->id,
 196                      'name' => $this->sa->name,
 197                      'inprogress' => 0,
 198                      'needsgrading' => 0,
 199                      'autograded' => 2,
 200                      'manuallygraded' => 0,
 201                      'all' => 2,
 202                  ));
 203          $this->assertEquals($summary[$this->allslots[1] . ',' . $this->essay->id],
 204                  (object) array(
 205                      'slot' => $this->allslots[1],
 206                      'questionid' => $this->essay->id,
 207                      'name' => $this->essay->name,
 208                      'inprogress' => 0,
 209                      'needsgrading' => 1,
 210                      'autograded' => 1,
 211                      'manuallygraded' => 0,
 212                      'all' => 2,
 213                  ));
 214      }
 215  
 216      /**
 217       * This test is executed by {@link test_reporting_queries()}.
 218       */
 219      protected function dotest_load_questions_usages_where_question_in_state() {
 220          $this->assertEquals(
 221                  array(array($this->usageids[0], $this->usageids[1]), 2),
 222                  $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
 223                  'all', $this->allslots[1], null, 'questionusageid'));
 224  
 225          $this->assertEquals(
 226                  array(array($this->usageids[0], $this->usageids[1]), 2),
 227                  $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
 228                  'autograded', $this->allslots[0], null, 'questionusageid'));
 229  
 230          $this->assertEquals(
 231                  array(array($this->usageids[0]), 1),
 232                  $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
 233                  'needsgrading', $this->allslots[1], null, 'questionusageid'));
 234      }
 235  
 236      /**
 237       * This test is executed by {@link test_reporting_queries()}.
 238       *
 239       * @param array|null $slots list of slots to use in the call.
 240       */
 241      protected function dotest_load_average_marks($slots) {
 242          $averages = $this->dm->load_average_marks($this->bothusages, $slots);
 243  
 244          $this->assertEquals(array(
 245              $this->allslots[0] => (object) array(
 246                  'slot'            => $this->allslots[0],
 247                  'averagefraction' => 0.5,
 248                  'numaveraged'     => 2,
 249              ),
 250              $this->allslots[1] => (object) array(
 251                  'slot'            => $this->allslots[1],
 252                  'averagefraction' => 0,
 253                  'numaveraged'     => 1,
 254              ),
 255          ), $averages);
 256      }
 257  
 258      /**
 259       * This test is executed by {@link test_reporting_queries()}.
 260       */
 261      protected function dotest_sum_usage_marks_subquery() {
 262          global $DB;
 263  
 264          $totals = $DB->get_records_sql_menu("SELECT qu.id, ({$this->dm->sum_usage_marks_subquery('qu.id')}) AS totalmark
 265                    FROM {question_usages} qu
 266                   WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})");
 267  
 268          $this->assertNull($totals[$this->usageids[0]]); // Since a question requires grading.
 269  
 270          $this->assertNotNull($totals[$this->usageids[1]]); // Grrr! PHP null == 0 makes this hard.
 271          $this->assertEquals(0, $totals[$this->usageids[1]]);
 272      }
 273  
 274      /**
 275       * This test is executed by {@link test_reporting_queries()}.
 276       */
 277      protected function dotest_question_attempt_latest_state_view() {
 278          global $DB;
 279  
 280          list($inlineview, $viewparams) = $this->dm->question_attempt_latest_state_view(
 281                  'lateststate', $this->bothusages);
 282  
 283          $rawstates = $DB->get_records_sql("
 284                  SELECT lateststate.questionattemptid,
 285                         qu.id AS questionusageid,
 286                         lateststate.slot,
 287                         lateststate.questionid,
 288                         lateststate.maxmark,
 289                         lateststate.sequencenumber,
 290                         lateststate.state
 291                    FROM {question_usages} qu
 292               LEFT JOIN $inlineview ON lateststate.questionusageid = qu.id
 293                   WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})", $viewparams);
 294  
 295          $states = array();
 296          foreach ($rawstates as $state) {
 297              $states[$state->questionusageid][$state->slot] = $state;
 298              unset($state->questionattemptid);
 299              unset($state->questionusageid);
 300              unset($state->slot);
 301          }
 302  
 303          $state = $states[$this->usageids[0]][$this->allslots[0]];
 304          $this->assertEquals((object) array(
 305              'questionid'     => $this->sa->id,
 306              'maxmark'        => 5.0,
 307              'sequencenumber' => 2,
 308              'state'          => (string) question_state::$gradedright,
 309          ), $state);
 310  
 311          $state = $states[$this->usageids[0]][$this->allslots[1]];
 312          $this->assertEquals((object) array(
 313              'questionid'     => $this->essay->id,
 314              'maxmark'        => 10.0,
 315              'sequencenumber' => 2,
 316              'state'          => (string) question_state::$needsgrading,
 317          ), $state);
 318  
 319          $state = $states[$this->usageids[1]][$this->allslots[0]];
 320          $this->assertEquals((object) array(
 321              'questionid'     => $this->sa->id,
 322              'maxmark'        => 5.0,
 323              'sequencenumber' => 2,
 324              'state'          => (string) question_state::$gradedwrong,
 325          ), $state);
 326  
 327          $state = $states[$this->usageids[1]][$this->allslots[1]];
 328          $this->assertEquals((object) array(
 329              'questionid'     => $this->essay->id,
 330              'maxmark'        => 10.0,
 331              'sequencenumber' => 1,
 332              'state'          => (string) question_state::$gaveup,
 333          ), $state);
 334      }
 335  }