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 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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  declare(strict_types=1);
  18  
  19  namespace mod_quiz;
  20  
  21  use advanced_testcase;
  22  use cm_info;
  23  use grade_item;
  24  use mod_quiz\completion\custom_completion;
  25  use question_engine;
  26  use quiz;
  27  use quiz_attempt;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  global $CFG;
  32  require_once($CFG->libdir . '/completionlib.php');
  33  
  34  /**
  35   * Class for unit testing mod_quiz/custom_completion.
  36   *
  37   * @package   mod_quiz
  38   * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
  39   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   * @coversDefaultClass \mod_quiz\completion\custom_completion
  41   */
  42  class custom_completion_test extends advanced_testcase {
  43  
  44      /**
  45       * Setup function for all tests.
  46       *
  47       * @param array $completionoptions ['nbstudents'] => int, ['qtype'] => string, ['quizoptions'] => array
  48       * @return array [$students, $quiz, $cm]
  49       */
  50      private function setup_quiz_for_testing_completion(array $completionoptions): array {
  51          global $CFG, $DB;
  52  
  53          $this->resetAfterTest(true);
  54  
  55          // Enable completion before creating modules, otherwise the completion data is not written in DB.
  56          $CFG->enablecompletion = true;
  57  
  58          // Create a course and students.
  59          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
  60          $course = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
  61          $students = [];
  62          for ($i = 0; $i < $completionoptions['nbstudents']; $i++) {
  63              $students[$i] = $this->getDataGenerator()->create_user();
  64              $this->assertTrue($this->getDataGenerator()->enrol_user($students[$i]->id, $course->id, $studentrole->id));
  65          }
  66  
  67          // Make a quiz.
  68          $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
  69          $data = array_merge([
  70              'course' => $course->id,
  71              'grade' => 100.0,
  72              'questionsperpage' => 0,
  73              'sumgrades' => 1,
  74              'completion' => COMPLETION_TRACKING_AUTOMATIC
  75          ], $completionoptions['quizoptions']);
  76          $quiz = $quizgenerator->create_instance($data);
  77          $cm = get_coursemodule_from_id('quiz', $quiz->cmid);
  78          $cm = cm_info::create($cm);
  79  
  80          // Create a question.
  81          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  82  
  83          $cat = $questiongenerator->create_question_category();
  84          $question = $questiongenerator->create_question($completionoptions['qtype'], null, ['category' => $cat->id]);
  85          quiz_add_quiz_question($question->id, $quiz);
  86  
  87          // Set grade to pass.
  88          $item = grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz',
  89              'iteminstance' => $quiz->id, 'outcomeid' => null]);
  90          $item->gradepass = 80;
  91          $item->update();
  92  
  93          return [
  94              $students,
  95              $quiz,
  96              $cm
  97          ];
  98      }
  99  
 100      /**
 101       * Helper function for tests.
 102       * Starts an attempt, processes responses and finishes the attempt.
 103       *
 104       * @param array $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int
 105       */
 106      private function do_attempt_quiz(array $attemptoptions) {
 107          $quizobj = quiz::create($attemptoptions['quiz']->id);
 108  
 109          // Start the passing attempt.
 110          $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
 111          $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 112  
 113          $timenow = time();
 114          $attempt = quiz_create_attempt($quizobj, $attemptoptions['attemptnumber'], false, $timenow, false,
 115              $attemptoptions['student']->id);
 116          quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptoptions['attemptnumber'], $timenow);
 117          quiz_attempt_save_started($quizobj, $quba, $attempt);
 118  
 119          // Process responses from the student.
 120          $attemptobj = quiz_attempt::create($attempt->id);
 121          $attemptobj->process_submitted_actions($timenow, false, $attemptoptions['tosubmit']);
 122  
 123          // Finish the attempt.
 124          $attemptobj = quiz_attempt::create($attempt->id);
 125          $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
 126          $attemptobj->process_finish($timenow, false);
 127      }
 128  
 129      /**
 130       * Test checking the completion state of a quiz.
 131       * The quiz requires a passing grade to be completed.
 132       *
 133       * @covers ::get_state
 134       * @covers ::get_custom_rule_descriptions
 135       */
 136      public function test_completionpass() {
 137          list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 138              'nbstudents' => 2,
 139              'qtype' => 'numerical',
 140              'quizoptions' => [
 141                  'completionusegrade' => 1,
 142                  'completionpass' => 1
 143              ]
 144          ]);
 145  
 146          list($passstudent, $failstudent) = $students;
 147  
 148          // Do a passing attempt.
 149          $this->do_attempt_quiz([
 150              'quiz' => $quiz,
 151              'student' => $passstudent,
 152              'attemptnumber' => 1,
 153              'tosubmit' => [1 => ['answer' => '3.14']]
 154          ]);
 155  
 156          // Check the results.
 157          $customcompletion = new custom_completion($cm, (int) $passstudent->id);
 158          $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
 159          $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
 160          $this->assertEquals(
 161              'Receive a pass grade',
 162              $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
 163          );
 164  
 165          // Do a failing attempt.
 166          $this->do_attempt_quiz([
 167              'quiz' => $quiz,
 168              'student' => $failstudent,
 169              'attemptnumber' => 1,
 170              'tosubmit' => [1 => ['answer' => '0']]
 171          ]);
 172  
 173          // Check the results.
 174          $customcompletion = new custom_completion($cm, (int) $failstudent->id);
 175          $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
 176          $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
 177          $this->assertEquals(
 178              'Receive a pass grade',
 179              $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
 180          );
 181      }
 182  
 183      /**
 184       * Test checking the completion state of a quiz.
 185       * To be completed, this quiz requires either a passing grade or for all attempts to be used up.
 186       *
 187       * @covers ::get_state
 188       * @covers ::get_custom_rule_descriptions
 189       */
 190      public function test_completionexhausted() {
 191          list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 192              'nbstudents' => 2,
 193              'qtype' => 'numerical',
 194              'quizoptions' => [
 195                  'attempts' => 2,
 196                  'completionusegrade' => 1,
 197                  'completionpass' => 1,
 198                  'completionattemptsexhausted' => 1
 199              ]
 200          ]);
 201  
 202          list($passstudent, $exhauststudent) = $students;
 203  
 204          // Start a passing attempt.
 205          $this->do_attempt_quiz([
 206              'quiz' => $quiz,
 207              'student' => $passstudent,
 208              'attemptnumber' => 1,
 209              'tosubmit' => [1 => ['answer' => '3.14']]
 210          ]);
 211  
 212          // Check the results. Quiz is completed by $passstudent because of passing grade.
 213          $customcompletion = new custom_completion($cm, (int) $passstudent->id);
 214          $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
 215          $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
 216          $this->assertEquals(
 217              'Receive a pass grade or complete all available attempts',
 218              $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
 219          );
 220  
 221          // Do a failing attempt.
 222          $this->do_attempt_quiz([
 223              'quiz' => $quiz,
 224              'student' => $exhauststudent,
 225              'attemptnumber' => 1,
 226              'tosubmit' => [1 => ['answer' => '0']]
 227          ]);
 228  
 229          // Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
 230          $customcompletion = new custom_completion($cm, (int) $exhauststudent->id);
 231          $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
 232          $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
 233          $this->assertEquals(
 234              'Receive a pass grade or complete all available attempts',
 235              $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
 236          );
 237  
 238          // Do a second failing attempt.
 239          $this->do_attempt_quiz([
 240              'quiz' => $quiz,
 241              'student' => $exhauststudent,
 242              'attemptnumber' => 2,
 243              'tosubmit' => [1 => ['answer' => '0']]
 244          ]);
 245  
 246          // Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
 247          $customcompletion = new custom_completion($cm, (int) $exhauststudent->id);
 248          $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
 249          $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
 250          $this->assertEquals(
 251              'Receive a pass grade or complete all available attempts',
 252              $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
 253          );
 254  
 255      }
 256  
 257      /**
 258       * Test checking the completion state of a quiz.
 259       * To be completed, this quiz requires a minimum number of attempts.
 260       *
 261       * @covers ::get_state
 262       * @covers ::get_custom_rule_descriptions
 263       */
 264      public function test_completionminattempts() {
 265          list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
 266              'nbstudents' => 1,
 267              'qtype' => 'essay',
 268              'quizoptions' => [
 269                  'completionminattemptsenabled' => 1,
 270                  'completionminattempts' => 2
 271              ]
 272          ]);
 273  
 274          list($student) = $students;
 275  
 276          // Do a first attempt.
 277          $this->do_attempt_quiz([
 278              'quiz' => $quiz,
 279              'student' => $student,
 280              'attemptnumber' => 1,
 281              'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
 282          ]);
 283  
 284          // Check the results. Quiz is not completed yet because only one attempt was done.
 285          $customcompletion = new custom_completion($cm, (int) $student->id);
 286          $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
 287          $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionminattempts'));
 288          $this->assertEquals(
 289              'Make attempts: 2',
 290              $customcompletion->get_custom_rule_descriptions()['completionminattempts']
 291          );
 292  
 293          // Do a second attempt.
 294          $this->do_attempt_quiz([
 295              'quiz' => $quiz,
 296              'student' => $student,
 297              'attemptnumber' => 2,
 298              'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
 299          ]);
 300  
 301          // Check the results. Quiz is completed by $student because two attempts were done.
 302          $customcompletion = new custom_completion($cm, (int) $student->id);
 303          $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
 304          $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionminattempts'));
 305          $this->assertEquals(
 306              'Make attempts: 2',
 307              $customcompletion->get_custom_rule_descriptions()['completionminattempts']
 308          );
 309      }
 310  
 311      /**
 312       * Test for get_defined_custom_rules().
 313       *
 314       * @covers ::get_defined_custom_rules
 315       */
 316      public function test_get_defined_custom_rules() {
 317          $rules = custom_completion::get_defined_custom_rules();
 318          $this->assertCount(2, $rules);
 319          $this->assertEquals(
 320              ['completionpassorattemptsexhausted', 'completionminattempts'],
 321              $rules
 322          );
 323      }
 324  }