Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   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   * Update Overdue Attempts Task
  19   *
  20   * @package    mod_quiz
  21   * @copyright  2017 Michael Hughes
  22   * @author Michael Hughes
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  namespace mod_quiz\task;
  26  
  27  use mod_quiz\quiz_attempt;
  28  use moodle_exception;
  29  use moodle_recordset;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  global $CFG;
  34  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  35  
  36  /**
  37   * Update Overdue Attempts Task
  38   *
  39   * @package    mod_quiz
  40   * @copyright  2017 Michael Hughes
  41   * @author Michael Hughes
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   *
  44   */
  45  class update_overdue_attempts extends \core\task\scheduled_task {
  46  
  47      public function get_name(): string {
  48          return get_string('updateoverdueattemptstask', 'mod_quiz');
  49      }
  50  
  51      /**
  52       * Close off any overdue attempts.
  53       */
  54      public function execute() {
  55          $timenow = time();
  56          $processto = $timenow - get_config('quiz', 'graceperiodmin');
  57  
  58          mtrace('  Looking for quiz overdue quiz attempts...');
  59  
  60          list($count, $quizcount) = $this->update_all_overdue_attempts($timenow, $processto);
  61  
  62          mtrace('  Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
  63      }
  64  
  65      /**
  66       * Do the processing required.
  67       *
  68       * @param int $timenow the time to consider as 'now' during the processing.
  69       * @param int $processto only process attempt with timecheckstate longer ago than this.
  70       * @return array with two elements, the number of attempt considered, and how many different quizzes that was.
  71       */
  72      public function update_all_overdue_attempts(int $timenow, int $processto): array {
  73          global $DB;
  74  
  75          $attemptstoprocess = $this->get_list_of_overdue_attempts($processto);
  76  
  77          $course = null;
  78          $quiz = null;
  79          $cm = null;
  80  
  81          $count = 0;
  82          $quizcount = 0;
  83          foreach ($attemptstoprocess as $attempt) {
  84              try {
  85  
  86                  // If we have moved on to a different quiz, fetch the new data.
  87                  if (!$quiz || $attempt->quiz != $quiz->id) {
  88                      $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz], '*', MUST_EXIST);
  89                      $cm = get_coursemodule_from_instance('quiz', $attempt->quiz);
  90                      $quizcount += 1;
  91                  }
  92  
  93                  // If we have moved on to a different course, fetch the new data.
  94                  if (!$course || $course->id != $quiz->course) {
  95                      $course = get_course($quiz->course);
  96                  }
  97  
  98                  // Make a specialised version of the quiz settings, with the relevant overrides.
  99                  $quizforuser = clone($quiz);
 100                  $quizforuser->timeclose = $attempt->usertimeclose;
 101                  $quizforuser->timelimit = $attempt->usertimelimit;
 102  
 103                  // Trigger any transitions that are required.
 104                  $attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course);
 105                  $attemptobj->handle_if_time_expired($timenow, false);
 106                  $count += 1;
 107  
 108              } catch (moodle_exception $e) {
 109                  // If an error occurs while processing one attempt, don't let that kill cron.
 110                  mtrace("Error while processing attempt $attempt->id at $attempt->quiz quiz:");
 111                  mtrace($e->getMessage());
 112                  mtrace($e->getTraceAsString());
 113                  // Close down any currently open transactions, otherwise one error
 114                  // will stop following DB changes from being committed.
 115                  $DB->force_transaction_rollback();
 116              }
 117          }
 118  
 119          $attemptstoprocess->close();
 120          return [$count, $quizcount];
 121      }
 122  
 123      /**
 124       * Get a recordset of all the attempts that need to be processed now.
 125       *
 126       * (Only public to allow unit testing. Do not use!)
 127       *
 128       * @param int $processto timestamp to process up to.
 129       * @return moodle_recordset of quiz_attempts that need to be processed because time has
 130       *     passed, sorted by courseid then quizid.
 131       */
 132      public function get_list_of_overdue_attempts(int $processto): moodle_recordset {
 133          global $DB;
 134  
 135          // SQL to compute timeclose and timelimit for each attempt.
 136          $quizausersql = quiz_get_attempt_usertime_sql(
 137                  "iquiza.state IN ('inprogress', 'overdue') AND iquiza.timecheckstate <= :iprocessto");
 138  
 139          // This query should have all the quiz_attempts columns.
 140          return $DB->get_recordset_sql("
 141           SELECT quiza.*,
 142                  quizauser.usertimeclose,
 143                  quizauser.usertimelimit
 144  
 145             FROM {quiz_attempts} quiza
 146             JOIN {quiz} quiz ON quiz.id = quiza.quiz
 147             JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
 148  
 149            WHERE quiza.state IN ('inprogress', 'overdue')
 150              AND quiza.timecheckstate <= :processto
 151         ORDER BY quiz.course, quiza.quiz",
 152  
 153                  ['processto' => $processto, 'iprocessto' => $processto]);
 154      }
 155  }