Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 38 and 311] [Versions 39 and 311]

   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 quiz_overview;
  18  
  19  use question_engine;
  20  use quiz;
  21  use quiz_attempt;
  22  use quiz_attempts_report;
  23  use quiz_overview_options;
  24  use quiz_overview_report;
  25  use quiz_overview_table;
  26  use testable_quiz_attempts_report;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  32  require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  33  require_once($CFG->dirroot . '/mod/quiz/report/default.php');
  34  require_once($CFG->dirroot . '/mod/quiz/report/overview/report.php');
  35  require_once($CFG->dirroot . '/mod/quiz/report/overview/tests/helpers.php');
  36  
  37  /**
  38   * Tests for the quiz overview report.
  39   *
  40   * @package    quiz_overview
  41   * @copyright  2014 The Open University
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class report_test extends \advanced_testcase {
  45  
  46      /**
  47       * Data provider for test_report_sql.
  48       *
  49       * @return array the data for the test sub-cases.
  50       */
  51      public function report_sql_cases() {
  52          return [[null], ['csv']]; // Only need to test on or off, not all download types.
  53      }
  54  
  55      /**
  56       * Test how the report queries the database.
  57       *
  58       * @param bool $isdownloading a download type, or null.
  59       * @dataProvider report_sql_cases
  60       */
  61      public function test_report_sql($isdownloading) {
  62          global $DB;
  63          $this->resetAfterTest(true);
  64  
  65          // Create a course and a quiz.
  66          $generator = $this->getDataGenerator();
  67          $course = $generator->create_course();
  68          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
  69          $quiz = $quizgenerator->create_instance(array('course' => $course->id,
  70                  'grademethod' => QUIZ_GRADEHIGHEST, 'grade' => 100.0, 'sumgrades' => 10.0,
  71                  'attempts' => 10));
  72  
  73          // Add one question.
  74          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  75          $cat = $questiongenerator->create_question_category();
  76          $q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
  77          quiz_add_quiz_question($q->id, $quiz, 0 , 10);
  78  
  79          // Create some students and enrol them in the course.
  80          $student1 = $generator->create_user();
  81          $student2 = $generator->create_user();
  82          $student3 = $generator->create_user();
  83          $generator->enrol_user($student1->id, $course->id);
  84          $generator->enrol_user($student2->id, $course->id);
  85          $generator->enrol_user($student3->id, $course->id);
  86          // This line is not really necessary for the test asserts below,
  87          // but what it does is add an extra user row returned by
  88          // get_enrolled_with_capabilities_join because of a second enrolment.
  89          // The extra row returned used to make $table->query_db complain
  90          // about duplicate records. So this is really a test that an extra
  91          // student enrolment does not cause duplicate records in this query.
  92          $generator->enrol_user($student2->id, $course->id, null, 'self');
  93  
  94          // Also create a user who should not appear in the reports,
  95          // because they have a role with neither 'mod/quiz:attempt'
  96          // nor 'mod/quiz:reviewmyattempts'.
  97          $tutor = $generator->create_user();
  98          $generator->enrol_user($tutor->id, $course->id, 'teacher');
  99  
 100          // The test data.
 101          $timestamp = 1234567890;
 102          $attempts = array(
 103              array($quiz, $student1, 1, 0.0,  quiz_attempt::FINISHED),
 104              array($quiz, $student1, 2, 5.0,  quiz_attempt::FINISHED),
 105              array($quiz, $student1, 3, 8.0,  quiz_attempt::FINISHED),
 106              array($quiz, $student1, 4, null, quiz_attempt::ABANDONED),
 107              array($quiz, $student1, 5, null, quiz_attempt::IN_PROGRESS),
 108              array($quiz, $student2, 1, null, quiz_attempt::ABANDONED),
 109              array($quiz, $student2, 2, null, quiz_attempt::ABANDONED),
 110              array($quiz, $student2, 3, 7.0,  quiz_attempt::FINISHED),
 111              array($quiz, $student2, 4, null, quiz_attempt::ABANDONED),
 112              array($quiz, $student2, 5, null, quiz_attempt::ABANDONED),
 113          );
 114  
 115          // Load it in to quiz attempts table.
 116          foreach ($attempts as $attemptdata) {
 117              list($quiz, $student, $attemptnumber, $sumgrades, $state) = $attemptdata;
 118              $timestart = $timestamp + $attemptnumber * 3600;
 119  
 120              $quizobj = quiz::create($quiz->id, $student->id);
 121              $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 122              $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 123  
 124              // Create the new attempt and initialize the question sessions.
 125              $attempt = quiz_create_attempt($quizobj, $attemptnumber, null, $timestart, false, $student->id);
 126  
 127              $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timestamp);
 128              $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
 129  
 130              // Process some responses from the student.
 131              $attemptobj = quiz_attempt::create($attempt->id);
 132              switch ($state) {
 133                  case quiz_attempt::ABANDONED:
 134                      $attemptobj->process_abandon($timestart + 300, false);
 135                      break;
 136  
 137                  case quiz_attempt::IN_PROGRESS:
 138                      // Do nothing.
 139                      break;
 140  
 141                  case quiz_attempt::FINISHED:
 142                      // Save answer and finish attempt.
 143                      $attemptobj->process_submitted_actions($timestart + 300, false, [
 144                              1 => ['answer' => 'My essay by ' . $student->firstname, 'answerformat' => FORMAT_PLAIN]]);
 145                      $attemptobj->process_finish($timestart + 600, false);
 146  
 147                      // Manually grade it.
 148                      $quba = $attemptobj->get_question_usage();
 149                      $quba->get_question_attempt(1)->manual_grade(
 150                              'Comment', $sumgrades, FORMAT_HTML, $timestart + 1200);
 151                      question_engine::save_questions_usage_by_activity($quba);
 152                      $update = new \stdClass();
 153                      $update->id = $attemptobj->get_attemptid();
 154                      $update->timemodified = $timestart + 1200;
 155                      $update->sumgrades = $quba->get_total_mark();
 156                      $DB->update_record('quiz_attempts', $update);
 157                      quiz_save_best_grade($attemptobj->get_quiz(), $student->id);
 158                      break;
 159              }
 160          }
 161  
 162          // Actually getting the SQL to run is quite hard. Do a minimal set up of
 163          // some objects.
 164          $context = \context_module::instance($quiz->cmid);
 165          $cm = get_coursemodule_from_id('quiz', $quiz->cmid);
 166          $qmsubselect = quiz_report_qm_filter_select($quiz);
 167          $studentsjoins = get_enrolled_with_capabilities_join($context, '',
 168                  array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'));
 169          $empty = new \core\dml\sql_join();
 170  
 171          // Set the options.
 172          $reportoptions = new quiz_overview_options('overview', $quiz, $cm, null);
 173          $reportoptions->attempts = quiz_attempts_report::ENROLLED_ALL;
 174          $reportoptions->onlygraded = true;
 175          $reportoptions->states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE, quiz_attempt::FINISHED);
 176  
 177          // Now do a minimal set-up of the table class.
 178          $q->slot = 1;
 179          $q->maxmark = 10;
 180          $table = new quiz_overview_table($quiz, $context, $qmsubselect, $reportoptions,
 181                  $empty, $studentsjoins, array(1 => $q), null);
 182          $table->download = $isdownloading; // Cannot call the is_downloading API, because it gives errors.
 183          $table->define_columns(array('fullname'));
 184          $table->sortable(true, 'uniqueid');
 185          $table->define_baseurl(new \moodle_url('/mod/quiz/report.php'));
 186          $table->setup();
 187  
 188          // Run the query.
 189          $table->setup_sql_queries($studentsjoins);
 190          $table->query_db(30, false);
 191  
 192          // Should be 4 rows, matching count($table->rawdata) tested below.
 193          // The count is only done if not downloading.
 194          if (!$isdownloading) {
 195              $this->assertEquals(4, $table->totalrows);
 196          }
 197  
 198          // Verify what was returned: Student 1's best and in progress attempts.
 199          // Student 2's finshed attempt, and Student 3 with no attempt.
 200          // The array key is {student id}#{attempt number}.
 201          $this->assertEquals(4, count($table->rawdata));
 202          $this->assertArrayHasKey($student1->id . '#3', $table->rawdata);
 203          $this->assertEquals(1, $table->rawdata[$student1->id . '#3']->gradedattempt);
 204          $this->assertArrayHasKey($student1->id . '#3', $table->rawdata);
 205          $this->assertEquals(0, $table->rawdata[$student1->id . '#5']->gradedattempt);
 206          $this->assertArrayHasKey($student2->id . '#3', $table->rawdata);
 207          $this->assertEquals(1, $table->rawdata[$student2->id . '#3']->gradedattempt);
 208          $this->assertArrayHasKey($student3->id . '#0', $table->rawdata);
 209          $this->assertEquals(0, $table->rawdata[$student3->id . '#0']->gradedattempt);
 210  
 211          // Check the calculation of averages.
 212          $averagerow = $table->compute_average_row('overallaverage', $studentsjoins);
 213          $this->assertStringContainsString('75.00', $averagerow['sumgrades']);
 214          $this->assertStringContainsString('75.00', $averagerow['qsgrade1']);
 215          if (!$isdownloading) {
 216              $this->assertStringContainsString('(2)', $averagerow['sumgrades']);
 217              $this->assertStringContainsString('(2)', $averagerow['qsgrade1']);
 218          }
 219  
 220          // Ensure that filtering by initial does not break it.
 221          // This involves setting a private properly of the base class, which is
 222          // only really possible using reflection :-(.
 223          $reflectionobject = new \ReflectionObject($table);
 224          while ($parent = $reflectionobject->getParentClass()) {
 225              $reflectionobject = $parent;
 226          }
 227          $prefsproperty = $reflectionobject->getProperty('prefs');
 228          $prefsproperty->setAccessible(true);
 229          $prefs = $prefsproperty->getValue($table);
 230          $prefs['i_first'] = 'A';
 231          $prefsproperty->setValue($table, $prefs);
 232  
 233          list($fields, $from, $where, $params) = $table->base_sql($studentsjoins);
 234          $table->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params);
 235          $table->set_sql($fields, $from, $where, $params);
 236          $table->query_db(30, false);
 237          // Just verify that this does not cause a fatal error.
 238      }
 239  
 240      /**
 241       * Bands provider.
 242       * @return array
 243       */
 244      public function get_bands_count_and_width_provider() {
 245          return [
 246              [10, [20, .5]],
 247              [20, [20, 1]],
 248              [30, [15, 2]],
 249              // TODO MDL-55068 Handle bands better when grade is 50.
 250              // [50, [10, 5]],
 251              [100, [20, 5]],
 252              [200, [20, 10]],
 253          ];
 254      }
 255  
 256      /**
 257       * Test bands.
 258       *
 259       * @dataProvider get_bands_count_and_width_provider
 260       * @param int $grade grade
 261       * @param array $expected
 262       */
 263      public function test_get_bands_count_and_width($grade, $expected) {
 264          $this->resetAfterTest(true);
 265          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 266          $quiz = $quizgenerator->create_instance(['course' => SITEID, 'grade' => $grade]);
 267          $this->assertEquals($expected, quiz_overview_report::get_bands_count_and_width($quiz));
 268      }
 269  
 270      /**
 271       * Test delete_selected_attempts function.
 272       */
 273      public function test_delete_selected_attempts() {
 274          $this->resetAfterTest(true);
 275  
 276          $timestamp = 1234567890;
 277          $timestart = $timestamp + 3600;
 278  
 279          // Create a course and a quiz.
 280          $generator = $this->getDataGenerator();
 281          $course = $generator->create_course();
 282          $quizgenerator = $generator->get_plugin_generator('mod_quiz');
 283          $quiz = $quizgenerator->create_instance([
 284                  'course' => $course->id,
 285                  'grademethod' => QUIZ_GRADEHIGHEST,
 286                  'grade' => 100.0,
 287                  'sumgrades' => 10.0,
 288                  'attempts' => 10
 289          ]);
 290  
 291          // Add one question.
 292          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 293          $cat = $questiongenerator->create_question_category();
 294          $q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
 295          quiz_add_quiz_question($q->id, $quiz, 0 , 10);
 296  
 297          // Create student and enrol them in the course.
 298          // Note: we create two enrolments, to test the problem reported in MDL-67942.
 299          $student = $generator->create_user();
 300          $generator->enrol_user($student->id, $course->id);
 301          $generator->enrol_user($student->id, $course->id, null, 'self');
 302  
 303          $context = \context_module::instance($quiz->cmid);
 304          $cm = get_coursemodule_from_id('quiz', $quiz->cmid);
 305          $allowedjoins = get_enrolled_with_capabilities_join($context, '', ['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']);
 306          $quizattemptsreport = new testable_quiz_attempts_report();
 307  
 308          // Create the new attempt and initialize the question sessions.
 309          $quizobj = quiz::create($quiz->id, $student->id);
 310          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 311          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 312          $attempt = quiz_create_attempt($quizobj, 1, null, $timestart, false, $student->id);
 313          $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timestamp);
 314          $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
 315  
 316          // Delete the student's attempt.
 317          $quizattemptsreport->delete_selected_attempts($quiz, $cm, [$attempt->id], $allowedjoins);
 318      }
 319  
 320  }