Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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