[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/mod/quiz/ -> attemptlib.php (source)

   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   * Back-end code for handling data about quizzes and the current user's attempt.
  19   *
  20   * There are classes for loading all the information about a quiz and attempts,
  21   * and for displaying the navigation panel.
  22   *
  23   * @package   mod_quiz
  24   * @copyright 2008 onwards Tim Hunt
  25   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  
  32  /**
  33   * Class for quiz exceptions. Just saves a couple of arguments on the
  34   * constructor for a moodle_exception.
  35   *
  36   * @copyright 2008 Tim Hunt
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @since     Moodle 2.0
  39   */
  40  class moodle_quiz_exception extends moodle_exception {
  41      public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) {
  42          if (!$link) {
  43              $link = $quizobj->view_url();
  44          }
  45          parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
  46      }
  47  }
  48  
  49  
  50  /**
  51   * A class encapsulating a quiz and the questions it contains, and making the
  52   * information available to scripts like view.php.
  53   *
  54   * Initially, it only loads a minimal amout of information about each question - loading
  55   * extra information only when necessary or when asked. The class tracks which questions
  56   * are loaded.
  57   *
  58   * @copyright  2008 Tim Hunt
  59   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  60   * @since      Moodle 2.0
  61   */
  62  class quiz {
  63      // Fields initialised in the constructor.
  64      protected $course;
  65      protected $cm;
  66      protected $quiz;
  67      protected $context;
  68  
  69      // Fields set later if that data is needed.
  70      protected $questions = null;
  71      protected $accessmanager = null;
  72      protected $ispreviewuser = null;
  73  
  74      // Constructor =============================================================
  75      /**
  76       * Constructor, assuming we already have the necessary data loaded.
  77       *
  78       * @param object $quiz the row from the quiz table.
  79       * @param object $cm the course_module object for this quiz.
  80       * @param object $course the row from the course table for the course we belong to.
  81       * @param bool $getcontext intended for testing - stops the constructor getting the context.
  82       */
  83      public function __construct($quiz, $cm, $course, $getcontext = true) {
  84          $this->quiz = $quiz;
  85          $this->cm = $cm;
  86          $this->quiz->cmid = $this->cm->id;
  87          $this->course = $course;
  88          if ($getcontext && !empty($cm->id)) {
  89              $this->context = context_module::instance($cm->id);
  90          }
  91      }
  92  
  93      /**
  94       * Static function to create a new quiz object for a specific user.
  95       *
  96       * @param int $quizid the the quiz id.
  97       * @param int $userid the the userid.
  98       * @return quiz the new quiz object
  99       */
 100      public static function create($quizid, $userid = null) {
 101          global $DB;
 102  
 103          $quiz = quiz_access_manager::load_quiz_and_settings($quizid);
 104          $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
 105          $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
 106  
 107          // Update quiz with override information.
 108          if ($userid) {
 109              $quiz = quiz_update_effective_access($quiz, $userid);
 110          }
 111  
 112          return new quiz($quiz, $cm, $course);
 113      }
 114  
 115      /**
 116       * Create a {@link quiz_attempt} for an attempt at this quiz.
 117       * @param object $attemptdata row from the quiz_attempts table.
 118       * @return quiz_attempt the new quiz_attempt object.
 119       */
 120      public function create_attempt_object($attemptdata) {
 121          return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course);
 122      }
 123  
 124      // Functions for loading more data =========================================
 125  
 126      /**
 127       * Load just basic information about all the questions in this quiz.
 128       */
 129      public function preload_questions() {
 130          $this->questions = question_preload_questions(null,
 131                  'slot.maxmark, slot.id AS slotid, slot.slot, slot.page',
 132                  '{quiz_slots} slot ON slot.quizid = :quizid AND q.id = slot.questionid',
 133                  array('quizid' => $this->quiz->id), 'slot.slot');
 134      }
 135  
 136      /**
 137       * Fully load some or all of the questions for this quiz. You must call
 138       * {@link preload_questions()} first.
 139       *
 140       * @param array $questionids question ids of the questions to load. null for all.
 141       */
 142      public function load_questions($questionids = null) {
 143          if ($this->questions === null) {
 144              throw new coding_exception('You must call preload_questions before calling load_questions.');
 145          }
 146          if (is_null($questionids)) {
 147              $questionids = array_keys($this->questions);
 148          }
 149          $questionstoprocess = array();
 150          foreach ($questionids as $id) {
 151              if (array_key_exists($id, $this->questions)) {
 152                  $questionstoprocess[$id] = $this->questions[$id];
 153              }
 154          }
 155          get_question_options($questionstoprocess);
 156      }
 157  
 158      /**
 159       * Get an instance of the {@link \mod_quiz\structure} class for this quiz.
 160       * @return \mod_quiz\structure describes the questions in the quiz.
 161       */
 162      public function get_structure() {
 163          return \mod_quiz\structure::create_for_quiz($this);
 164      }
 165  
 166      // Simple getters ==========================================================
 167      /** @return int the course id. */
 168      public function get_courseid() {
 169          return $this->course->id;
 170      }
 171  
 172      /** @return object the row of the course table. */
 173      public function get_course() {
 174          return $this->course;
 175      }
 176  
 177      /** @return int the quiz id. */
 178      public function get_quizid() {
 179          return $this->quiz->id;
 180      }
 181  
 182      /** @return object the row of the quiz table. */
 183      public function get_quiz() {
 184          return $this->quiz;
 185      }
 186  
 187      /** @return string the name of this quiz. */
 188      public function get_quiz_name() {
 189          return $this->quiz->name;
 190      }
 191  
 192      /** @return int the quiz navigation method. */
 193      public function get_navigation_method() {
 194          return $this->quiz->navmethod;
 195      }
 196  
 197      /** @return int the number of attempts allowed at this quiz (0 = infinite). */
 198      public function get_num_attempts_allowed() {
 199          return $this->quiz->attempts;
 200      }
 201  
 202      /** @return int the course_module id. */
 203      public function get_cmid() {
 204          return $this->cm->id;
 205      }
 206  
 207      /** @return object the course_module object. */
 208      public function get_cm() {
 209          return $this->cm;
 210      }
 211  
 212      /** @return object the module context for this quiz. */
 213      public function get_context() {
 214          return $this->context;
 215      }
 216  
 217      /**
 218       * @return bool wether the current user is someone who previews the quiz,
 219       * rather than attempting it.
 220       */
 221      public function is_preview_user() {
 222          if (is_null($this->ispreviewuser)) {
 223              $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
 224          }
 225          return $this->ispreviewuser;
 226      }
 227  
 228      /**
 229       * @return whether any questions have been added to this quiz.
 230       */
 231      public function has_questions() {
 232          if ($this->questions === null) {
 233              $this->preload_questions();
 234          }
 235          return !empty($this->questions);
 236      }
 237  
 238      /**
 239       * @param int $id the question id.
 240       * @return object the question object with that id.
 241       */
 242      public function get_question($id) {
 243          return $this->questions[$id];
 244      }
 245  
 246      /**
 247       * @param array $questionids question ids of the questions to load. null for all.
 248       */
 249      public function get_questions($questionids = null) {
 250          if (is_null($questionids)) {
 251              $questionids = array_keys($this->questions);
 252          }
 253          $questions = array();
 254          foreach ($questionids as $id) {
 255              if (!array_key_exists($id, $this->questions)) {
 256                  throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
 257              }
 258              $questions[$id] = $this->questions[$id];
 259              $this->ensure_question_loaded($id);
 260          }
 261          return $questions;
 262      }
 263  
 264      /**
 265       * @param int $timenow the current time as a unix timestamp.
 266       * @return quiz_access_manager and instance of the quiz_access_manager class
 267       *      for this quiz at this time.
 268       */
 269      public function get_access_manager($timenow) {
 270          if (is_null($this->accessmanager)) {
 271              $this->accessmanager = new quiz_access_manager($this, $timenow,
 272                      has_capability('mod/quiz:ignoretimelimits', $this->context, null, false));
 273          }
 274          return $this->accessmanager;
 275      }
 276  
 277      /**
 278       * Wrapper round the has_capability funciton that automatically passes in the quiz context.
 279       */
 280      public function has_capability($capability, $userid = null, $doanything = true) {
 281          return has_capability($capability, $this->context, $userid, $doanything);
 282      }
 283  
 284      /**
 285       * Wrapper round the require_capability funciton that automatically passes in the quiz context.
 286       */
 287      public function require_capability($capability, $userid = null, $doanything = true) {
 288          return require_capability($capability, $this->context, $userid, $doanything);
 289      }
 290  
 291      // URLs related to this attempt ============================================
 292      /**
 293       * @return string the URL of this quiz's view page.
 294       */
 295      public function view_url() {
 296          global $CFG;
 297          return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
 298      }
 299  
 300      /**
 301       * @return string the URL of this quiz's edit page.
 302       */
 303      public function edit_url() {
 304          global $CFG;
 305          return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
 306      }
 307  
 308      /**
 309       * @param int $attemptid the id of an attempt.
 310       * @param int $page optional page number to go to in the attempt.
 311       * @return string the URL of that attempt.
 312       */
 313      public function attempt_url($attemptid, $page = 0) {
 314          global $CFG;
 315          $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
 316          if ($page) {
 317              $url .= '&page=' . $page;
 318          }
 319          return $url;
 320      }
 321  
 322      /**
 323       * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
 324       */
 325      public function start_attempt_url($page = 0) {
 326          $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey());
 327          if ($page) {
 328              $params['page'] = $page;
 329          }
 330          return new moodle_url('/mod/quiz/startattempt.php', $params);
 331      }
 332  
 333      /**
 334       * @param int $attemptid the id of an attempt.
 335       * @return string the URL of the review of that attempt.
 336       */
 337      public function review_url($attemptid) {
 338          return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
 339      }
 340  
 341      /**
 342       * @param int $attemptid the id of an attempt.
 343       * @return string the URL of the review of that attempt.
 344       */
 345      public function summary_url($attemptid) {
 346          return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid));
 347      }
 348  
 349      // Bits of content =========================================================
 350  
 351      /**
 352       * @param bool $unfinished whether there is currently an unfinished attempt active.
 353       * @return string if the quiz policies merit it, return a warning string to
 354       *      be displayed in a javascript alert on the start attempt button.
 355       */
 356      public function confirm_start_attempt_message($unfinished) {
 357          if ($unfinished) {
 358              return '';
 359          }
 360  
 361          if ($this->quiz->timelimit && $this->quiz->attempts) {
 362              return get_string('confirmstartattempttimelimit', 'quiz', $this->quiz->attempts);
 363          } else if ($this->quiz->timelimit) {
 364              return get_string('confirmstarttimelimit', 'quiz');
 365          } else if ($this->quiz->attempts) {
 366              return get_string('confirmstartattemptlimit', 'quiz', $this->quiz->attempts);
 367          }
 368  
 369          return '';
 370      }
 371  
 372      /**
 373       * If $reviewoptions->attempt is false, meaning that students can't review this
 374       * attempt at the moment, return an appropriate string explaining why.
 375       *
 376       * @param int $when One of the mod_quiz_display_options::DURING,
 377       *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
 378       * @param bool $short if true, return a shorter string.
 379       * @return string an appropraite message.
 380       */
 381      public function cannot_review_message($when, $short = false) {
 382  
 383          if ($short) {
 384              $langstrsuffix = 'short';
 385              $dateformat = get_string('strftimedatetimeshort', 'langconfig');
 386          } else {
 387              $langstrsuffix = '';
 388              $dateformat = '';
 389          }
 390  
 391          if ($when == mod_quiz_display_options::DURING ||
 392                  $when == mod_quiz_display_options::IMMEDIATELY_AFTER) {
 393              return '';
 394          } else if ($when == mod_quiz_display_options::LATER_WHILE_OPEN && $this->quiz->timeclose &&
 395                  $this->quiz->reviewattempt & mod_quiz_display_options::AFTER_CLOSE) {
 396              return get_string('noreviewuntil' . $langstrsuffix, 'quiz',
 397                      userdate($this->quiz->timeclose, $dateformat));
 398          } else {
 399              return get_string('noreview' . $langstrsuffix, 'quiz');
 400          }
 401      }
 402  
 403      /**
 404       * @param string $title the name of this particular quiz page.
 405       * @return array the data that needs to be sent to print_header_simple as the $navigation
 406       * parameter.
 407       */
 408      public function navigation($title) {
 409          global $PAGE;
 410          $PAGE->navbar->add($title);
 411          return '';
 412      }
 413  
 414      // Private methods =========================================================
 415      /**
 416       * Check that the definition of a particular question is loaded, and if not throw an exception.
 417       * @param $id a questionid.
 418       */
 419      protected function ensure_question_loaded($id) {
 420          if (isset($this->questions[$id]->_partiallyloaded)) {
 421              throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
 422          }
 423      }
 424  }
 425  
 426  
 427  /**
 428   * This class extends the quiz class to hold data about the state of a particular attempt,
 429   * in addition to the data about the quiz.
 430   *
 431   * @copyright  2008 Tim Hunt
 432   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 433   * @since      Moodle 2.0
 434   */
 435  class quiz_attempt {
 436  
 437      /** @var string to identify the in progress state. */
 438      const IN_PROGRESS = 'inprogress';
 439      /** @var string to identify the overdue state. */
 440      const OVERDUE     = 'overdue';
 441      /** @var string to identify the finished state. */
 442      const FINISHED    = 'finished';
 443      /** @var string to identify the abandoned state. */
 444      const ABANDONED   = 'abandoned';
 445  
 446      /** @var int maximum number of slots in the quiz for the review page to default to show all. */
 447      const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50;
 448  
 449      // Basic data.
 450      protected $quizobj;
 451      protected $attempt;
 452  
 453      /** @var question_usage_by_activity the question usage for this quiz attempt. */
 454      protected $quba;
 455  
 456      /** @var array page no => array of slot numbers on the page in order. */
 457      protected $pagelayout;
 458  
 459      /** @var array slot => displayed question number for this slot. (E.g. 1, 2, 3 or 'i'.) */
 460      protected $questionnumbers;
 461  
 462      /** @var array slot => page number for this slot. */
 463      protected $questionpages;
 464  
 465      /** @var mod_quiz_display_options cache for the appropriate review options. */
 466      protected $reviewoptions = null;
 467  
 468      // Constructor =============================================================
 469      /**
 470       * Constructor assuming we already have the necessary data loaded.
 471       *
 472       * @param object $attempt the row of the quiz_attempts table.
 473       * @param object $quiz the quiz object for this attempt and user.
 474       * @param object $cm the course_module object for this quiz.
 475       * @param object $course the row from the course table for the course we belong to.
 476       * @param bool $loadquestions (optional) if true, the default, load all the details
 477       *      of the state of each question. Else just set up the basic details of the attempt.
 478       */
 479      public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) {
 480          $this->attempt = $attempt;
 481          $this->quizobj = new quiz($quiz, $cm, $course);
 482  
 483          if (!$loadquestions) {
 484              return;
 485          }
 486  
 487          $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
 488          $this->determine_layout();
 489          $this->number_questions();
 490      }
 491  
 492      /**
 493       * Used by {create()} and {create_from_usage_id()}.
 494       * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions).
 495       */
 496      protected static function create_helper($conditions) {
 497          global $DB;
 498  
 499          $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST);
 500          $quiz = quiz_access_manager::load_quiz_and_settings($attempt->quiz);
 501          $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
 502          $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
 503  
 504          // Update quiz with override information.
 505          $quiz = quiz_update_effective_access($quiz, $attempt->userid);
 506  
 507          return new quiz_attempt($attempt, $quiz, $cm, $course);
 508      }
 509  
 510      /**
 511       * Static function to create a new quiz_attempt object given an attemptid.
 512       *
 513       * @param int $attemptid the attempt id.
 514       * @return quiz_attempt the new quiz_attempt object
 515       */
 516      public static function create($attemptid) {
 517          return self::create_helper(array('id' => $attemptid));
 518      }
 519  
 520      /**
 521       * Static function to create a new quiz_attempt object given a usage id.
 522       *
 523       * @param int $usageid the attempt usage id.
 524       * @return quiz_attempt the new quiz_attempt object
 525       */
 526      public static function create_from_usage_id($usageid) {
 527          return self::create_helper(array('uniqueid' => $usageid));
 528      }
 529  
 530      /**
 531       * @param string $state one of the state constants like IN_PROGRESS.
 532       * @return string the human-readable state name.
 533       */
 534      public static function state_name($state) {
 535          return quiz_attempt_state_name($state);
 536      }
 537  
 538      /**
 539       * Parse attempt->layout to populate the other arrays the represent the layout.
 540       */
 541      protected function determine_layout() {
 542          $this->pagelayout = array();
 543  
 544          // Break up the layout string into pages.
 545          $pagelayouts = explode(',0', $this->attempt->layout);
 546  
 547          // Strip off any empty last page (normally there is one).
 548          if (end($pagelayouts) == '') {
 549              array_pop($pagelayouts);
 550          }
 551  
 552          // File the ids into the arrays.
 553          $this->pagelayout = array();
 554          foreach ($pagelayouts as $page => $pagelayout) {
 555              $pagelayout = trim($pagelayout, ',');
 556              if ($pagelayout == '') {
 557                  continue;
 558              }
 559              $this->pagelayout[$page] = explode(',', $pagelayout);
 560          }
 561      }
 562  
 563      /**
 564       * Work out the number to display for each question/slot.
 565       */
 566      protected function number_questions() {
 567          $number = 1;
 568          foreach ($this->pagelayout as $page => $slots) {
 569              foreach ($slots as $slot) {
 570                  if ($length = $this->is_real_question($slot)) {
 571                      $this->questionnumbers[$slot] = $number;
 572                      $number += $length;
 573                  } else {
 574                      $this->questionnumbers[$slot] = get_string('infoshort', 'quiz');
 575                  }
 576                  $this->questionpages[$slot] = $page;
 577              }
 578          }
 579      }
 580  
 581      /**
 582       * If the given page number is out of range (before the first page, or after
 583       * the last page, chnage it to be within range).
 584       * @param int $page the requested page number.
 585       * @return int a safe page number to use.
 586       */
 587      public function force_page_number_into_range($page) {
 588          return min(max($page, 0), count($this->pagelayout) - 1);
 589      }
 590  
 591      // Simple getters ==========================================================
 592      public function get_quiz() {
 593          return $this->quizobj->get_quiz();
 594      }
 595  
 596      public function get_quizobj() {
 597          return $this->quizobj;
 598      }
 599  
 600      /** @return int the course id. */
 601      public function get_courseid() {
 602          return $this->quizobj->get_courseid();
 603      }
 604  
 605      /** @return int the course id. */
 606      public function get_course() {
 607          return $this->quizobj->get_course();
 608      }
 609  
 610      /** @return int the quiz id. */
 611      public function get_quizid() {
 612          return $this->quizobj->get_quizid();
 613      }
 614  
 615      /** @return string the name of this quiz. */
 616      public function get_quiz_name() {
 617          return $this->quizobj->get_quiz_name();
 618      }
 619  
 620      /** @return int the quiz navigation method. */
 621      public function get_navigation_method() {
 622          return $this->quizobj->get_navigation_method();
 623      }
 624  
 625      /** @return object the course_module object. */
 626      public function get_cm() {
 627          return $this->quizobj->get_cm();
 628      }
 629  
 630      /** @return object the course_module object. */
 631      public function get_cmid() {
 632          return $this->quizobj->get_cmid();
 633      }
 634  
 635      /**
 636       * @return bool wether the current user is someone who previews the quiz,
 637       * rather than attempting it.
 638       */
 639      public function is_preview_user() {
 640          return $this->quizobj->is_preview_user();
 641      }
 642  
 643      /** @return int the number of attempts allowed at this quiz (0 = infinite). */
 644      public function get_num_attempts_allowed() {
 645          return $this->quizobj->get_num_attempts_allowed();
 646      }
 647  
 648      /** @return int number fo pages in this quiz. */
 649      public function get_num_pages() {
 650          return count($this->pagelayout);
 651      }
 652  
 653      /**
 654       * @param int $timenow the current time as a unix timestamp.
 655       * @return quiz_access_manager and instance of the quiz_access_manager class
 656       *      for this quiz at this time.
 657       */
 658      public function get_access_manager($timenow) {
 659          return $this->quizobj->get_access_manager($timenow);
 660      }
 661  
 662      /** @return int the attempt id. */
 663      public function get_attemptid() {
 664          return $this->attempt->id;
 665      }
 666  
 667      /** @return int the attempt unique id. */
 668      public function get_uniqueid() {
 669          return $this->attempt->uniqueid;
 670      }
 671  
 672      /** @return object the row from the quiz_attempts table. */
 673      public function get_attempt() {
 674          return $this->attempt;
 675      }
 676  
 677      /** @return int the number of this attemp (is it this user's first, second, ... attempt). */
 678      public function get_attempt_number() {
 679          return $this->attempt->attempt;
 680      }
 681  
 682      /** @return string one of the quiz_attempt::IN_PROGRESS, FINISHED, OVERDUE or ABANDONED constants. */
 683      public function get_state() {
 684          return $this->attempt->state;
 685      }
 686  
 687      /** @return int the id of the user this attempt belongs to. */
 688      public function get_userid() {
 689          return $this->attempt->userid;
 690      }
 691  
 692      /** @return int the current page of the attempt. */
 693      public function get_currentpage() {
 694          return $this->attempt->currentpage;
 695      }
 696  
 697      public function get_sum_marks() {
 698          return $this->attempt->sumgrades;
 699      }
 700  
 701      /**
 702       * @return bool whether this attempt has been finished (true) or is still
 703       *     in progress (false). Be warned that this is not just state == self::FINISHED,
 704       *     it also includes self::ABANDONED.
 705       */
 706      public function is_finished() {
 707          return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED;
 708      }
 709  
 710      /** @return bool whether this attempt is a preview attempt. */
 711      public function is_preview() {
 712          return $this->attempt->preview;
 713      }
 714  
 715      /**
 716       * Is this a student dealing with their own attempt/teacher previewing,
 717       * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
 718       *
 719       * @return bool whether this situation should be treated as someone looking at their own
 720       * attempt. The distinction normally only matters when an attempt is being reviewed.
 721       */
 722      public function is_own_attempt() {
 723          global $USER;
 724          return $this->attempt->userid == $USER->id &&
 725                  (!$this->is_preview_user() || $this->attempt->preview);
 726      }
 727  
 728      /**
 729       * @return bool whether this attempt is a preview belonging to the current user.
 730       */
 731      public function is_own_preview() {
 732          global $USER;
 733          return $this->attempt->userid == $USER->id &&
 734                  $this->is_preview_user() && $this->attempt->preview;
 735      }
 736  
 737      /**
 738       * Is the current user allowed to review this attempt. This applies when
 739       * {@link is_own_attempt()} returns false.
 740       * @return bool whether the review should be allowed.
 741       */
 742      public function is_review_allowed() {
 743          if (!$this->has_capability('mod/quiz:viewreports')) {
 744              return false;
 745          }
 746  
 747          $cm = $this->get_cm();
 748          if ($this->has_capability('moodle/site:accessallgroups') ||
 749                  groups_get_activity_groupmode($cm) != SEPARATEGROUPS) {
 750              return true;
 751          }
 752  
 753          // Check the users have at least one group in common.
 754          $teachersgroups = groups_get_activity_allowed_groups($cm);
 755          $studentsgroups = groups_get_all_groups(
 756                  $cm->course, $this->attempt->userid, $cm->groupingid);
 757          return $teachersgroups && $studentsgroups &&
 758                  array_intersect(array_keys($teachersgroups), array_keys($studentsgroups));
 759      }
 760  
 761      /**
 762       * Has the student, in this attempt, engaged with the quiz in a non-trivial way?
 763       * That is, is there any question worth a non-zero number of marks, where
 764       * the student has made some response that we have saved?
 765       * @return bool true if we have saved a response for at least one graded question.
 766       */
 767      public function has_response_to_at_least_one_graded_question() {
 768          foreach ($this->quba->get_attempt_iterator() as $qa) {
 769              if ($qa->get_max_mark() == 0) {
 770                  continue;
 771              }
 772              if ($qa->get_num_steps() > 1) {
 773                  return true;
 774              }
 775          }
 776          return false;
 777      }
 778  
 779      /**
 780       * Get extra summary information about this attempt.
 781       *
 782       * Some behaviours may be able to provide interesting summary information
 783       * about the attempt as a whole, and this method provides access to that data.
 784       * To see how this works, try setting a quiz to one of the CBM behaviours,
 785       * and then look at the extra information displayed at the top of the quiz
 786       * review page once you have sumitted an attempt.
 787       *
 788       * In the return value, the array keys are identifiers of the form
 789       * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary.
 790       * The values are arrays with two items, title and content. Each of these
 791       * will be either a string, or a renderable.
 792       *
 793       * @param question_display_options $options the display options for this quiz attempt at this time.
 794       * @return array as described above.
 795       */
 796      public function get_additional_summary_data(question_display_options $options) {
 797          return $this->quba->get_summary_information($options);
 798      }
 799  
 800      /**
 801       * Get the overall feedback corresponding to a particular mark.
 802       * @param $grade a particular grade.
 803       */
 804      public function get_overall_feedback($grade) {
 805          return quiz_feedback_for_grade($grade, $this->get_quiz(),
 806                  $this->quizobj->get_context());
 807      }
 808  
 809      /**
 810       * Wrapper round the has_capability funciton that automatically passes in the quiz context.
 811       */
 812      public function has_capability($capability, $userid = null, $doanything = true) {
 813          return $this->quizobj->has_capability($capability, $userid, $doanything);
 814      }
 815  
 816      /**
 817       * Wrapper round the require_capability funciton that automatically passes in the quiz context.
 818       */
 819      public function require_capability($capability, $userid = null, $doanything = true) {
 820          return $this->quizobj->require_capability($capability, $userid, $doanything);
 821      }
 822  
 823      /**
 824       * Check the appropriate capability to see whether this user may review their own attempt.
 825       * If not, prints an error.
 826       */
 827      public function check_review_capability() {
 828          if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) {
 829              $capability = 'mod/quiz:attempt';
 830          } else {
 831              $capability = 'mod/quiz:reviewmyattempts';
 832          }
 833  
 834          // These next tests are in a slighly funny order. The point is that the
 835          // common and most performance-critical case is students attempting a quiz
 836          // so we want to check that permisison first.
 837  
 838          if ($this->has_capability($capability)) {
 839              // User has the permission that lets you do the quiz as a student. Fine.
 840              return;
 841          }
 842  
 843          if ($this->has_capability('mod/quiz:viewreports') ||
 844                  $this->has_capability('mod/quiz:preview')) {
 845              // User has the permission that lets teachers review. Fine.
 846              return;
 847          }
 848  
 849          // They should not be here. Trigger the standard no-permission error
 850          // but using the name of the student capability.
 851          // We know this will fail. We just want the stadard exception thown.
 852          $this->require_capability($capability);
 853      }
 854  
 855      /**
 856       * Checks whether a user may navigate to a particular slot
 857       */
 858      public function can_navigate_to($slot) {
 859          switch ($this->get_navigation_method()) {
 860              case QUIZ_NAVMETHOD_FREE:
 861                  return true;
 862                  break;
 863              case QUIZ_NAVMETHOD_SEQ:
 864                  return false;
 865                  break;
 866          }
 867          return true;
 868      }
 869  
 870      /**
 871       * @return int one of the mod_quiz_display_options::DURING,
 872       *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
 873       */
 874      public function get_attempt_state() {
 875          return quiz_attempt_state($this->get_quiz(), $this->attempt);
 876      }
 877  
 878      /**
 879       * Wrapper that the correct mod_quiz_display_options for this quiz at the
 880       * moment.
 881       *
 882       * @return question_display_options the render options for this user on this attempt.
 883       */
 884      public function get_display_options($reviewing) {
 885          if ($reviewing) {
 886              if (is_null($this->reviewoptions)) {
 887                  $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
 888                          $this->attempt, $this->quizobj->get_context());
 889              }
 890              return $this->reviewoptions;
 891  
 892          } else {
 893              $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(),
 894                      mod_quiz_display_options::DURING);
 895              $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context());
 896              return $options;
 897          }
 898      }
 899  
 900      /**
 901       * Wrapper that the correct mod_quiz_display_options for this quiz at the
 902       * moment.
 903       *
 904       * @param bool $reviewing true for review page, else attempt page.
 905       * @param int $slot which question is being displayed.
 906       * @param moodle_url $thispageurl to return to after the editing form is
 907       *      submitted or cancelled. If null, no edit link will be generated.
 908       *
 909       * @return question_display_options the render options for this user on this
 910       *      attempt, with extra info to generate an edit link, if applicable.
 911       */
 912      public function get_display_options_with_edit_link($reviewing, $slot, $thispageurl) {
 913          $options = clone($this->get_display_options($reviewing));
 914  
 915          if (!$thispageurl) {
 916              return $options;
 917          }
 918  
 919          if (!($reviewing || $this->is_preview())) {
 920              return $options;
 921          }
 922  
 923          $question = $this->quba->get_question($slot);
 924          if (!question_has_capability_on($question, 'edit', $question->category)) {
 925              return $options;
 926          }
 927  
 928          $options->editquestionparams['cmid'] = $this->get_cmid();
 929          $options->editquestionparams['returnurl'] = $thispageurl;
 930  
 931          return $options;
 932      }
 933  
 934      /**
 935       * @param int $page page number
 936       * @return bool true if this is the last page of the quiz.
 937       */
 938      public function is_last_page($page) {
 939          return $page == count($this->pagelayout) - 1;
 940      }
 941  
 942      /**
 943       * Return the list of question ids for either a given page of the quiz, or for the
 944       * whole quiz.
 945       *
 946       * @param mixed $page string 'all' or integer page number.
 947       * @return array the reqested list of question ids.
 948       */
 949      public function get_slots($page = 'all') {
 950          if ($page === 'all') {
 951              $numbers = array();
 952              foreach ($this->pagelayout as $numbersonpage) {
 953                  $numbers = array_merge($numbers, $numbersonpage);
 954              }
 955              return $numbers;
 956          } else {
 957              return $this->pagelayout[$page];
 958          }
 959      }
 960  
 961      /**
 962       * Get the question_attempt object for a particular question in this attempt.
 963       * @param int $slot the number used to identify this question within this attempt.
 964       * @return question_attempt
 965       */
 966      public function get_question_attempt($slot) {
 967          return $this->quba->get_question_attempt($slot);
 968      }
 969  
 970      /**
 971       * Is a particular question in this attempt a real question, or something like a description.
 972       * @param int $slot the number used to identify this question within this attempt.
 973       * @return int whether that question is a real question. Actually returns the
 974       *     question length, which could theoretically be greater than one.
 975       */
 976      public function is_real_question($slot) {
 977          return $this->quba->get_question($slot)->length;
 978      }
 979  
 980      /**
 981       * Is a particular question in this attempt a real question, or something like a description.
 982       * @param int $slot the number used to identify this question within this attempt.
 983       * @return bool whether that question is a real question.
 984       */
 985      public function is_question_flagged($slot) {
 986          return $this->quba->get_question_attempt($slot)->is_flagged();
 987      }
 988  
 989      /**
 990       * @param int $slot the number used to identify this question within this attempt.
 991       * @return string the displayed question number for the question in this slot.
 992       *      For example '1', '2', '3' or 'i'.
 993       */
 994      public function get_question_number($slot) {
 995          return $this->questionnumbers[$slot];
 996      }
 997  
 998      /**
 999       * @param int $slot the number used to identify this question within this attempt.
1000       * @return int the page of the quiz this question appears on.
1001       */
1002      public function get_question_page($slot) {
1003          return $this->questionpages[$slot];
1004      }
1005  
1006      /**
1007       * Return the grade obtained on a particular question, if the user is permitted
1008       * to see it. You must previously have called load_question_states to load the
1009       * state data about this question.
1010       *
1011       * @param int $slot the number used to identify this question within this attempt.
1012       * @return string the formatted grade, to the number of decimal places specified
1013       *      by the quiz.
1014       */
1015      public function get_question_name($slot) {
1016          return $this->quba->get_question($slot)->name;
1017      }
1018  
1019      /**
1020       * Return the grade obtained on a particular question, if the user is permitted
1021       * to see it. You must previously have called load_question_states to load the
1022       * state data about this question.
1023       *
1024       * @param int $slot the number used to identify this question within this attempt.
1025       * @param bool $showcorrectness Whether right/partial/wrong states should
1026       * be distinguised.
1027       * @return string the formatted grade, to the number of decimal places specified
1028       *      by the quiz.
1029       */
1030      public function get_question_status($slot, $showcorrectness) {
1031          return $this->quba->get_question_state_string($slot, $showcorrectness);
1032      }
1033  
1034      /**
1035       * Return the grade obtained on a particular question, if the user is permitted
1036       * to see it. You must previously have called load_question_states to load the
1037       * state data about this question.
1038       *
1039       * @param int $slot the number used to identify this question within this attempt.
1040       * @param bool $showcorrectness Whether right/partial/wrong states should
1041       * be distinguised.
1042       * @return string class name for this state.
1043       */
1044      public function get_question_state_class($slot, $showcorrectness) {
1045          return $this->quba->get_question_state_class($slot, $showcorrectness);
1046      }
1047  
1048      /**
1049       * Return the grade obtained on a particular question.
1050       * You must previously have called load_question_states to load the state
1051       * data about this question.
1052       *
1053       * @param int $slot the number used to identify this question within this attempt.
1054       * @return string the formatted grade, to the number of decimal places specified by the quiz.
1055       */
1056      public function get_question_mark($slot) {
1057          return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot));
1058      }
1059  
1060      /**
1061       * Get the time of the most recent action performed on a question.
1062       * @param int $slot the number used to identify this question within this usage.
1063       * @return int timestamp.
1064       */
1065      public function get_question_action_time($slot) {
1066          return $this->quba->get_question_action_time($slot);
1067      }
1068  
1069      /**
1070       * Get the time remaining for an in-progress attempt, if the time is short
1071       * enought that it would be worth showing a timer.
1072       * @param int $timenow the time to consider as 'now'.
1073       * @return int|false the number of seconds remaining for this attempt.
1074       *      False if there is no limit.
1075       */
1076      public function get_time_left_display($timenow) {
1077          if ($this->attempt->state != self::IN_PROGRESS) {
1078              return false;
1079          }
1080          return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow);
1081      }
1082  
1083  
1084      /**
1085       * @return int the time when this attempt was submitted. 0 if it has not been
1086       * submitted yet.
1087       */
1088      public function get_submitted_date() {
1089          return $this->attempt->timefinish;
1090      }
1091  
1092      /**
1093       * If the attempt is in an applicable state, work out the time by which the
1094       * student should next do something.
1095       * @return int timestamp by which the student needs to do something.
1096       */
1097      public function get_due_date() {
1098          $deadlines = array();
1099          if ($this->quizobj->get_quiz()->timelimit) {
1100              $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit;
1101          }
1102          if ($this->quizobj->get_quiz()->timeclose) {
1103              $deadlines[] = $this->quizobj->get_quiz()->timeclose;
1104          }
1105          if ($deadlines) {
1106              $duedate = min($deadlines);
1107          } else {
1108              return false;
1109          }
1110  
1111          switch ($this->attempt->state) {
1112              case self::IN_PROGRESS:
1113                  return $duedate;
1114  
1115              case self::OVERDUE:
1116                  return $duedate + $this->quizobj->get_quiz()->graceperiod;
1117  
1118              default:
1119                  throw new coding_exception('Unexpected state: ' . $this->attempt->state);
1120          }
1121      }
1122  
1123      // URLs related to this attempt ============================================
1124      /**
1125       * @return string quiz view url.
1126       */
1127      public function view_url() {
1128          return $this->quizobj->view_url();
1129      }
1130  
1131      /**
1132       * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
1133       */
1134      public function start_attempt_url($slot = null, $page = -1) {
1135          if ($page == -1 && !is_null($slot)) {
1136              $page = $this->get_question_page($slot);
1137          } else {
1138              $page = 0;
1139          }
1140          return $this->quizobj->start_attempt_url($page);
1141      }
1142  
1143      /**
1144       * @param int $slot if speified, the slot number of a specific question to link to.
1145       * @param int $page if specified, a particular page to link to. If not givem deduced
1146       *      from $slot, or goes to the first page.
1147       * @param int $questionid a question id. If set, will add a fragment to the URL
1148       * to jump to a particuar question on the page.
1149       * @param int $thispage if not -1, the current page. Will cause links to other things on
1150       * this page to be output as only a fragment.
1151       * @return string the URL to continue this attempt.
1152       */
1153      public function attempt_url($slot = null, $page = -1, $thispage = -1) {
1154          return $this->page_and_question_url('attempt', $slot, $page, false, $thispage);
1155      }
1156  
1157      /**
1158       * @return string the URL of this quiz's summary page.
1159       */
1160      public function summary_url() {
1161          return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
1162      }
1163  
1164      /**
1165       * @return string the URL of this quiz's summary page.
1166       */
1167      public function processattempt_url() {
1168          return new moodle_url('/mod/quiz/processattempt.php');
1169      }
1170  
1171      /**
1172       * @param int $slot indicates which question to link to.
1173       * @param int $page if specified, the URL of this particular page of the attempt, otherwise
1174       * the URL will go to the first page.  If -1, deduce $page from $slot.
1175       * @param bool|null $showall if true, the URL will be to review the entire attempt on one page,
1176       * and $page will be ignored. If null, a sensible default will be chosen.
1177       * @param int $thispage if not -1, the current page. Will cause links to other things on
1178       * this page to be output as only a fragment.
1179       * @return string the URL to review this attempt.
1180       */
1181      public function review_url($slot = null, $page = -1, $showall = null, $thispage = -1) {
1182          return $this->page_and_question_url('review', $slot, $page, $showall, $thispage);
1183      }
1184  
1185      /**
1186       * By default, should this script show all questions on one page for this attempt?
1187       * @param string $script the script name, e.g. 'attempt', 'summary', 'review'.
1188       * @return whether show all on one page should be on by default.
1189       */
1190      public function get_default_show_all($script) {
1191          return $script == 'review' && count($this->questionpages) < self::MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL;
1192      }
1193  
1194      // Bits of content =========================================================
1195  
1196      /**
1197       * If $reviewoptions->attempt is false, meaning that students can't review this
1198       * attempt at the moment, return an appropriate string explaining why.
1199       *
1200       * @param bool $short if true, return a shorter string.
1201       * @return string an appropraite message.
1202       */
1203      public function cannot_review_message($short = false) {
1204          return $this->quizobj->cannot_review_message(
1205                  $this->get_attempt_state(), $short);
1206      }
1207  
1208      /**
1209       * Initialise the JS etc. required all the questions on a page.
1210       * @param mixed $page a page number, or 'all'.
1211       */
1212      public function get_html_head_contributions($page = 'all', $showall = false) {
1213          if ($showall) {
1214              $page = 'all';
1215          }
1216          $result = '';
1217          foreach ($this->get_slots($page) as $slot) {
1218              $result .= $this->quba->render_question_head_html($slot);
1219          }
1220          $result .= question_engine::initialise_js();
1221          return $result;
1222      }
1223  
1224      /**
1225       * Initialise the JS etc. required by one question.
1226       * @param int $questionid the question id.
1227       */
1228      public function get_question_html_head_contributions($slot) {
1229          return $this->quba->render_question_head_html($slot) .
1230                  question_engine::initialise_js();
1231      }
1232  
1233      /**
1234       * Print the HTML for the start new preview button, if the current user
1235       * is allowed to see one.
1236       */
1237      public function restart_preview_button() {
1238          global $OUTPUT;
1239          if ($this->is_preview() && $this->is_preview_user()) {
1240              return $OUTPUT->single_button(new moodle_url(
1241                      $this->start_attempt_url(), array('forcenew' => true)),
1242                      get_string('startnewpreview', 'quiz'));
1243          } else {
1244              return '';
1245          }
1246      }
1247  
1248      /**
1249       * Generate the HTML that displayes the question in its current state, with
1250       * the appropriate display options.
1251       *
1252       * @param int $id the id of a question in this quiz attempt.
1253       * @param bool $reviewing is the being printed on an attempt or a review page.
1254       * @param moodle_url $thispageurl the URL of the page this question is being printed on.
1255       * @return string HTML for the question in its current state.
1256       */
1257      public function render_question($slot, $reviewing, $thispageurl = null) {
1258          return $this->quba->render_question($slot,
1259                  $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
1260                  $this->get_question_number($slot));
1261      }
1262  
1263      /**
1264       * Like {@link render_question()} but displays the question at the past step
1265       * indicated by $seq, rather than showing the latest step.
1266       *
1267       * @param int $id the id of a question in this quiz attempt.
1268       * @param int $seq the seq number of the past state to display.
1269       * @param bool $reviewing is the being printed on an attempt or a review page.
1270       * @param string $thispageurl the URL of the page this question is being printed on.
1271       * @return string HTML for the question in its current state.
1272       */
1273      public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
1274          return $this->quba->render_question_at_step($slot, $seq,
1275                  $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
1276                  $this->get_question_number($slot));
1277      }
1278  
1279      /**
1280       * Wrapper round print_question from lib/questionlib.php.
1281       *
1282       * @param int $id the id of a question in this quiz attempt.
1283       */
1284      public function render_question_for_commenting($slot) {
1285          $options = $this->get_display_options(true);
1286          $options->hide_all_feedback();
1287          $options->manualcomment = question_display_options::EDITABLE;
1288          return $this->quba->render_question($slot, $options,
1289                  $this->get_question_number($slot));
1290      }
1291  
1292      /**
1293       * Check wheter access should be allowed to a particular file.
1294       *
1295       * @param int $id the id of a question in this quiz attempt.
1296       * @param bool $reviewing is the being printed on an attempt or a review page.
1297       * @param string $thispageurl the URL of the page this question is being printed on.
1298       * @return string HTML for the question in its current state.
1299       */
1300      public function check_file_access($slot, $reviewing, $contextid, $component,
1301              $filearea, $args, $forcedownload) {
1302          return $this->quba->check_file_access($slot, $this->get_display_options($reviewing),
1303                  $component, $filearea, $args, $forcedownload);
1304      }
1305  
1306      /**
1307       * Get the navigation panel object for this attempt.
1308       *
1309       * @param $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel
1310       * @param $page the current page number.
1311       * @param $showall whether we are showing the whole quiz on one page. (Used by review.php)
1312       * @return quiz_nav_panel_base the requested object.
1313       */
1314      public function get_navigation_panel(mod_quiz_renderer $output,
1315               $panelclass, $page, $showall = false) {
1316          $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall);
1317  
1318          $bc = new block_contents();
1319          $bc->attributes['id'] = 'mod_quiz_navblock';
1320          $bc->title = get_string('quiznavigation', 'quiz');
1321          $bc->content = $output->navigation_panel($panel);
1322          return $bc;
1323      }
1324  
1325      /**
1326       * Given a URL containing attempt={this attempt id}, return an array of variant URLs
1327       * @param moodle_url $url a URL.
1328       * @return string HTML fragment. Comma-separated list of links to the other
1329       * attempts with the attempt number as the link text. The curent attempt is
1330       * included but is not a link.
1331       */
1332      public function links_to_other_attempts(moodle_url $url) {
1333          $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all');
1334          if (count($attempts) <= 1) {
1335              return false;
1336          }
1337  
1338          $links = new mod_quiz_links_to_other_attempts();
1339          foreach ($attempts as $at) {
1340              if ($at->id == $this->attempt->id) {
1341                  $links->links[$at->attempt] = null;
1342              } else {
1343                  $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id));
1344              }
1345          }
1346          return $links;
1347      }
1348  
1349      // Methods for processing ==================================================
1350  
1351      /**
1352       * Check this attempt, to see if there are any state transitions that should
1353       * happen automatically.  This function will update the attempt checkstatetime.
1354       * @param int $timestamp the timestamp that should be stored as the modifed
1355       * @param bool $studentisonline is the student currently interacting with Moodle?
1356       */
1357      public function handle_if_time_expired($timestamp, $studentisonline) {
1358          global $DB;
1359  
1360          $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
1361  
1362          if ($timeclose === false || $this->is_preview()) {
1363              $this->update_timecheckstate(null);
1364              return; // No time limit
1365          }
1366          if ($timestamp < $timeclose) {
1367              $this->update_timecheckstate($timeclose);
1368              return; // Time has not yet expired.
1369          }
1370  
1371          // If the attempt is already overdue, look to see if it should be abandoned ...
1372          if ($this->attempt->state == self::OVERDUE) {
1373              $timeoverdue = $timestamp - $timeclose;
1374              $graceperiod = $this->quizobj->get_quiz()->graceperiod;
1375              if ($timeoverdue >= $graceperiod) {
1376                  $this->process_abandon($timestamp, $studentisonline);
1377              } else {
1378                  // Overdue time has not yet expired
1379                  $this->update_timecheckstate($timeclose + $graceperiod);
1380              }
1381              return; // ... and we are done.
1382          }
1383  
1384          if ($this->attempt->state != self::IN_PROGRESS) {
1385              $this->update_timecheckstate(null);
1386              return; // Attempt is already in a final state.
1387          }
1388  
1389          // Otherwise, we were in quiz_attempt::IN_PROGRESS, and time has now expired.
1390          // Transition to the appropriate state.
1391          switch ($this->quizobj->get_quiz()->overduehandling) {
1392              case 'autosubmit':
1393                  $this->process_finish($timestamp, false);
1394                  return;
1395  
1396              case 'graceperiod':
1397                  $this->process_going_overdue($timestamp, $studentisonline);
1398                  return;
1399  
1400              case 'autoabandon':
1401                  $this->process_abandon($timestamp, $studentisonline);
1402                  return;
1403          }
1404  
1405          // This is an overdue attempt with no overdue handling defined, so just abandon.
1406          $this->process_abandon($timestamp, $studentisonline);
1407          return;
1408      }
1409  
1410      /**
1411       * Process all the actions that were submitted as part of the current request.
1412       *
1413       * @param int  $timestamp  the timestamp that should be stored as the modifed
1414       *                         time in the database for these actions. If null, will use the current time.
1415       * @param bool $becomingoverdue
1416       * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data, keys are slot
1417       *                                          nos and values are arrays representing student responses which will be passed to
1418       *                                          question_definition::prepare_simulated_post_data method and then have the
1419       *                                          appropriate prefix added.
1420       */
1421      public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) {
1422          global $DB;
1423  
1424          $transaction = $DB->start_delegated_transaction();
1425  
1426          if ($simulatedresponses !== null) {
1427              $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
1428          } else {
1429              $simulatedpostdata = null;
1430          }
1431  
1432          $this->quba->process_all_actions($timestamp, $simulatedpostdata);
1433          question_engine::save_questions_usage_by_activity($this->quba);
1434  
1435          $this->attempt->timemodified = $timestamp;
1436          if ($this->attempt->state == self::FINISHED) {
1437              $this->attempt->sumgrades = $this->quba->get_total_mark();
1438          }
1439          if ($becomingoverdue) {
1440              $this->process_going_overdue($timestamp, true);
1441          } else {
1442              $DB->update_record('quiz_attempts', $this->attempt);
1443          }
1444  
1445          if (!$this->is_preview() && $this->attempt->state == self::FINISHED) {
1446              quiz_save_best_grade($this->get_quiz(), $this->get_userid());
1447          }
1448  
1449          $transaction->allow_commit();
1450      }
1451  
1452      /**
1453       * Process all the autosaved data that was part of the current request.
1454       *
1455       * @param int $timestamp the timestamp that should be stored as the modifed
1456       * time in the database for these actions. If null, will use the current time.
1457       */
1458      public function process_auto_save($timestamp) {
1459          global $DB;
1460  
1461          $transaction = $DB->start_delegated_transaction();
1462  
1463          $this->quba->process_all_autosaves($timestamp);
1464          question_engine::save_questions_usage_by_activity($this->quba);
1465  
1466          $transaction->allow_commit();
1467      }
1468  
1469      /**
1470       * Update the flagged state for all question_attempts in this usage, if their
1471       * flagged state was changed in the request.
1472       */
1473      public function save_question_flags() {
1474          global $DB;
1475  
1476          $transaction = $DB->start_delegated_transaction();
1477          $this->quba->update_question_flags();
1478          question_engine::save_questions_usage_by_activity($this->quba);
1479          $transaction->allow_commit();
1480      }
1481  
1482      public function process_finish($timestamp, $processsubmitted) {
1483          global $DB;
1484  
1485          $transaction = $DB->start_delegated_transaction();
1486  
1487          if ($processsubmitted) {
1488              $this->quba->process_all_actions($timestamp);
1489          }
1490          $this->quba->finish_all_questions($timestamp);
1491  
1492          question_engine::save_questions_usage_by_activity($this->quba);
1493  
1494          $this->attempt->timemodified = $timestamp;
1495          $this->attempt->timefinish = $timestamp;
1496          $this->attempt->sumgrades = $this->quba->get_total_mark();
1497          $this->attempt->state = self::FINISHED;
1498          $this->attempt->timecheckstate = null;
1499          $DB->update_record('quiz_attempts', $this->attempt);
1500  
1501          if (!$this->is_preview()) {
1502              quiz_save_best_grade($this->get_quiz(), $this->attempt->userid);
1503  
1504              // Trigger event.
1505              $this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp);
1506  
1507              // Tell any access rules that care that the attempt is over.
1508              $this->get_access_manager($timestamp)->current_attempt_finished();
1509          }
1510  
1511          $transaction->allow_commit();
1512      }
1513  
1514      /**
1515       * Update this attempt timecheckstate if necessary.
1516       * @param int|null the timecheckstate
1517       */
1518      public function update_timecheckstate($time) {
1519          global $DB;
1520          if ($this->attempt->timecheckstate !== $time) {
1521              $this->attempt->timecheckstate = $time;
1522              $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id'=>$this->attempt->id));
1523          }
1524      }
1525  
1526      /**
1527       * Mark this attempt as now overdue.
1528       * @param int $timestamp the time to deem as now.
1529       * @param bool $studentisonline is the student currently interacting with Moodle?
1530       */
1531      public function process_going_overdue($timestamp, $studentisonline) {
1532          global $DB;
1533  
1534          $transaction = $DB->start_delegated_transaction();
1535          $this->attempt->timemodified = $timestamp;
1536          $this->attempt->state = self::OVERDUE;
1537          // If we knew the attempt close time, we could compute when the graceperiod ends.
1538          // Instead we'll just fix it up through cron.
1539          $this->attempt->timecheckstate = $timestamp;
1540          $DB->update_record('quiz_attempts', $this->attempt);
1541  
1542          $this->fire_state_transition_event('\mod_quiz\event\attempt_becameoverdue', $timestamp);
1543  
1544          $transaction->allow_commit();
1545  
1546          quiz_send_overdue_message($this);
1547      }
1548  
1549      /**
1550       * Mark this attempt as abandoned.
1551       * @param int $timestamp the time to deem as now.
1552       * @param bool $studentisonline is the student currently interacting with Moodle?
1553       */
1554      public function process_abandon($timestamp, $studentisonline) {
1555          global $DB;
1556  
1557          $transaction = $DB->start_delegated_transaction();
1558          $this->attempt->timemodified = $timestamp;
1559          $this->attempt->state = self::ABANDONED;
1560          $this->attempt->timecheckstate = null;
1561          $DB->update_record('quiz_attempts', $this->attempt);
1562  
1563          $this->fire_state_transition_event('\mod_quiz\event\attempt_abandoned', $timestamp);
1564  
1565          $transaction->allow_commit();
1566      }
1567  
1568      /**
1569       * Fire a state transition event.
1570       * the same event information.
1571       * @param string $eventclass the event class name.
1572       * @param int $timestamp the timestamp to include in the event.
1573       * @return void
1574       */
1575      protected function fire_state_transition_event($eventclass, $timestamp) {
1576          global $USER;
1577          $quizrecord = $this->get_quiz();
1578          $params = array(
1579              'context' => $this->get_quizobj()->get_context(),
1580              'courseid' => $this->get_courseid(),
1581              'objectid' => $this->attempt->id,
1582              'relateduserid' => $this->attempt->userid,
1583              'other' => array(
1584                  'submitterid' => CLI_SCRIPT ? null : $USER->id,
1585                  'quizid' => $quizrecord->id
1586              )
1587          );
1588  
1589          $event = $eventclass::create($params);
1590          $event->add_record_snapshot('quiz', $this->get_quiz());
1591          $event->add_record_snapshot('quiz_attempts', $this->get_attempt());
1592          $event->trigger();
1593      }
1594  
1595      /**
1596       * Print the fields of the comment form for questions in this attempt.
1597       * @param $slot which question to output the fields for.
1598       * @param $prefix Prefix to add to all field names.
1599       */
1600      public function question_print_comment_fields($slot, $prefix) {
1601          // Work out a nice title.
1602          $student = get_record('user', 'id', $this->get_userid());
1603          $a = new object();
1604          $a->fullname = fullname($student, true);
1605          $a->attempt = $this->get_attempt_number();
1606  
1607          question_print_comment_fields($this->quba->get_question_attempt($slot),
1608                  $prefix, $this->get_display_options(true)->markdp,
1609                  get_string('gradingattempt', 'quiz_grading', $a));
1610      }
1611  
1612      // Private methods =========================================================
1613  
1614      /**
1615       * Get a URL for a particular question on a particular page of the quiz.
1616       * Used by {@link attempt_url()} and {@link review_url()}.
1617       *
1618       * @param string $script. Used in the URL like /mod/quiz/$script.php
1619       * @param int $slot identifies the specific question on the page to jump to.
1620       *      0 to just use the $page parameter.
1621       * @param int $page -1 to look up the page number from the slot, otherwise
1622       *      the page number to go to.
1623       * @param bool|null $showall if true, return a URL with showall=1, and not page number.
1624       *      if null, then an intelligent default will be chosen.
1625       * @param int $thispage the page we are currently on. Links to questions on this
1626       *      page will just be a fragment #q123. -1 to disable this.
1627       * @return The requested URL.
1628       */
1629      protected function page_and_question_url($script, $slot, $page, $showall, $thispage) {
1630  
1631          $defaultshowall = $this->get_default_show_all($script);
1632          if ($showall === null && ($page == 0 || $page == -1)) {
1633              $showall = $defaultshowall;
1634          }
1635  
1636          // Fix up $page.
1637          if ($page == -1) {
1638              if ($slot !== null && !$showall) {
1639                  $page = $this->get_question_page($slot);
1640              } else {
1641                  $page = 0;
1642              }
1643          }
1644  
1645          if ($showall) {
1646              $page = 0;
1647          }
1648  
1649          // Add a fragment to scroll down to the question.
1650          $fragment = '';
1651          if ($slot !== null) {
1652              if ($slot == reset($this->pagelayout[$page])) {
1653                  // First question on page, go to top.
1654                  $fragment = '#';
1655              } else {
1656                  $fragment = '#q' . $slot;
1657              }
1658          }
1659  
1660          // Work out the correct start to the URL.
1661          if ($thispage == $page) {
1662              return new moodle_url($fragment);
1663  
1664          } else {
1665              $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
1666                      array('attempt' => $this->attempt->id));
1667              if ($page == 0 && $showall != $defaultshowall) {
1668                  $url->param('showall', (int) $showall);
1669              } else if ($page > 0) {
1670                  $url->param('page', $page);
1671              }
1672              return $url;
1673          }
1674      }
1675  }
1676  
1677  
1678  /**
1679   * Represents a single link in the navigation panel.
1680   *
1681   * @copyright  2011 The Open University
1682   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1683   * @since      Moodle 2.1
1684   */
1685  class quiz_nav_question_button implements renderable {
1686      public $id;
1687      public $number;
1688      public $stateclass;
1689      public $statestring;
1690      public $currentpage;
1691      public $flagged;
1692      public $url;
1693  }
1694  
1695  
1696  /**
1697   * Represents the navigation panel, and builds a {@link block_contents} to allow
1698   * it to be output.
1699   *
1700   * @copyright  2008 Tim Hunt
1701   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1702   * @since      Moodle 2.0
1703   */
1704  abstract class quiz_nav_panel_base {
1705      /** @var quiz_attempt */
1706      protected $attemptobj;
1707      /** @var question_display_options */
1708      protected $options;
1709      /** @var integer */
1710      protected $page;
1711      /** @var boolean */
1712      protected $showall;
1713  
1714      public function __construct(quiz_attempt $attemptobj,
1715              question_display_options $options, $page, $showall) {
1716          $this->attemptobj = $attemptobj;
1717          $this->options = $options;
1718          $this->page = $page;
1719          $this->showall = $showall;
1720      }
1721  
1722      public function get_question_buttons() {
1723          $buttons = array();
1724          foreach ($this->attemptobj->get_slots() as $slot) {
1725              $qa = $this->attemptobj->get_question_attempt($slot);
1726              $showcorrectness = $this->options->correctness && $qa->has_marks();
1727  
1728              $button = new quiz_nav_question_button();
1729              $button->id          = 'quiznavbutton' . $slot;
1730              $button->number      = $this->attemptobj->get_question_number($slot);
1731              $button->stateclass  = $qa->get_state_class($showcorrectness);
1732              $button->navmethod   = $this->attemptobj->get_navigation_method();
1733              if (!$showcorrectness && $button->stateclass == 'notanswered') {
1734                  $button->stateclass = 'complete';
1735              }
1736              $button->statestring = $this->get_state_string($qa, $showcorrectness);
1737              $button->currentpage = $this->showall || $this->attemptobj->get_question_page($slot) == $this->page;
1738              $button->flagged     = $qa->is_flagged();
1739              $button->url         = $this->get_question_url($slot);
1740              $buttons[] = $button;
1741          }
1742  
1743          return $buttons;
1744      }
1745  
1746      protected function get_state_string(question_attempt $qa, $showcorrectness) {
1747          if ($qa->get_question()->length > 0) {
1748              return $qa->get_state_string($showcorrectness);
1749          }
1750  
1751          // Special case handling for 'information' items.
1752          if ($qa->get_state() == question_state::$todo) {
1753              return get_string('notyetviewed', 'quiz');
1754          } else {
1755              return get_string('viewed', 'quiz');
1756          }
1757      }
1758  
1759      public function render_before_button_bits(mod_quiz_renderer $output) {
1760          return '';
1761      }
1762  
1763      abstract public function render_end_bits(mod_quiz_renderer $output);
1764  
1765      protected function render_restart_preview_link($output) {
1766          if (!$this->attemptobj->is_own_preview()) {
1767              return '';
1768          }
1769          return $output->restart_preview_button(new moodle_url(
1770                  $this->attemptobj->start_attempt_url(), array('forcenew' => true)));
1771      }
1772  
1773      protected abstract function get_question_url($slot);
1774  
1775      public function user_picture() {
1776          global $DB;
1777          if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_NONE) {
1778              return null;
1779          }
1780          $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid()));
1781          $userpicture = new user_picture($user);
1782          $userpicture->courseid = $this->attemptobj->get_courseid();
1783          if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_LARGE) {
1784              $userpicture->size = true;
1785          }
1786          return $userpicture;
1787      }
1788  
1789      /**
1790       * Return 'allquestionsononepage' as CSS class name when $showall is set,
1791       * otherwise, return 'multipages' as CSS class name.
1792       * @return string, CSS class name
1793       */
1794      public function get_button_container_class() {
1795          // Quiz navigation is set on 'Show all questions on one page'.
1796          if ($this->showall) {
1797              return 'allquestionsononepage';
1798          }
1799          // Quiz navigation is set on 'Show one page at a time'.
1800          return 'multipages';
1801      }
1802  }
1803  
1804  
1805  /**
1806   * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page.
1807   *
1808   * @copyright  2008 Tim Hunt
1809   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1810   * @since      Moodle 2.0
1811   */
1812  class quiz_attempt_nav_panel extends quiz_nav_panel_base {
1813      public function get_question_url($slot) {
1814          if ($this->attemptobj->can_navigate_to($slot)) {
1815              return $this->attemptobj->attempt_url($slot, -1, $this->page);
1816          } else {
1817              return null;
1818          }
1819      }
1820  
1821      public function render_before_button_bits(mod_quiz_renderer $output) {
1822          return html_writer::tag('div', get_string('navnojswarning', 'quiz'),
1823                  array('id' => 'quiznojswarning'));
1824      }
1825  
1826      public function render_end_bits(mod_quiz_renderer $output) {
1827          return html_writer::link($this->attemptobj->summary_url(),
1828                  get_string('endtest', 'quiz'), array('class' => 'endtestlink')) .
1829                  $output->countdown_timer($this->attemptobj, time()) .
1830                  $this->render_restart_preview_link($output);
1831      }
1832  }
1833  
1834  
1835  /**
1836   * Specialisation of {@link quiz_nav_panel_base} for the review quiz page.
1837   *
1838   * @copyright  2008 Tim Hunt
1839   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1840   * @since      Moodle 2.0
1841   */
1842  class quiz_review_nav_panel extends quiz_nav_panel_base {
1843      public function get_question_url($slot) {
1844          return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page);
1845      }
1846  
1847      public function render_end_bits(mod_quiz_renderer $output) {
1848          $html = '';
1849          if ($this->attemptobj->get_num_pages() > 1) {
1850              if ($this->showall) {
1851                  $html .= html_writer::link($this->attemptobj->review_url(null, 0, false),
1852                          get_string('showeachpage', 'quiz'));
1853              } else {
1854                  $html .= html_writer::link($this->attemptobj->review_url(null, 0, true),
1855                          get_string('showall', 'quiz'));
1856              }
1857          }
1858          $html .= $output->finish_review_link($this->attemptobj);
1859          $html .= $this->render_restart_preview_link($output);
1860          return $html;
1861      }
1862  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1