mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
5682c8c70e
I thought about renaming the class to just plain attempt, but I acutally think quiz_attempt makes it clearer what this is. Also not changing the name massively reduces the pain for everyone updating their code (including me right now!)
557 lines
19 KiB
PHP
557 lines
19 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Back-end code for handling data about quizzes and the current user's attempt.
|
|
*
|
|
* There are classes for loading all the information about a quiz and attempts,
|
|
* and for displaying the navigation panel.
|
|
*
|
|
* @package mod_quiz
|
|
* @copyright 2008 onwards Tim Hunt
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
use mod_quiz\access_manager;
|
|
use mod_quiz\output\links_to_other_attempts;
|
|
use mod_quiz\output\navigation_panel_base;
|
|
use mod_quiz\output\renderer;
|
|
use mod_quiz\question\bank\qbank_helper;
|
|
use mod_quiz\question\display_options;
|
|
use mod_quiz\quiz_attempt;
|
|
|
|
/**
|
|
* A class encapsulating a quiz and the questions it contains, and making the
|
|
* information available to scripts like view.php.
|
|
*
|
|
* Initially, it only loads a minimal amout of information about each question - loading
|
|
* extra information only when necessary or when asked. The class tracks which questions
|
|
* are loaded.
|
|
*
|
|
* @copyright 2008 Tim Hunt
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
* @since Moodle 2.0
|
|
*/
|
|
class quiz {
|
|
/** @var stdClass the course settings from the database. */
|
|
protected $course;
|
|
/** @var stdClass the course_module settings from the database. */
|
|
protected $cm;
|
|
/** @var stdClass the quiz settings from the database. */
|
|
protected $quiz;
|
|
/** @var context the quiz context. */
|
|
protected $context;
|
|
|
|
/**
|
|
* @var stdClass[] of questions augmented with slot information. For non-random
|
|
* questions, the array key is question id. For random quesions it is 's' . $slotid.
|
|
* probalby best to use ->questionid field of the object instead.
|
|
*/
|
|
protected $questions = null;
|
|
/** @var stdClass[] of quiz_section rows. */
|
|
protected $sections = null;
|
|
/** @var access_manager the access manager for this quiz. */
|
|
protected $accessmanager = null;
|
|
/** @var bool whether the current user has capability mod/quiz:preview. */
|
|
protected $ispreviewuser = null;
|
|
|
|
// Constructor =============================================================
|
|
/**
|
|
* Constructor, assuming we already have the necessary data loaded.
|
|
*
|
|
* @param object $quiz the row from the quiz table.
|
|
* @param object $cm the course_module object for this quiz.
|
|
* @param object $course the row from the course table for the course we belong to.
|
|
* @param bool $getcontext intended for testing - stops the constructor getting the context.
|
|
*/
|
|
public function __construct($quiz, $cm, $course, $getcontext = true) {
|
|
$this->quiz = $quiz;
|
|
$this->cm = $cm;
|
|
$this->quiz->cmid = $this->cm->id;
|
|
$this->course = $course;
|
|
if ($getcontext && !empty($cm->id)) {
|
|
$this->context = context_module::instance($cm->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Static function to create a new quiz object for a specific user.
|
|
*
|
|
* @param int $quizid the the quiz id.
|
|
* @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
|
|
* @return quiz the new quiz object.
|
|
*/
|
|
public static function create($quizid, $userid = null) {
|
|
global $DB;
|
|
|
|
$quiz = access_manager::load_quiz_and_settings($quizid);
|
|
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
|
|
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
|
|
|
|
// Update quiz with override information.
|
|
if ($userid) {
|
|
$quiz = quiz_update_effective_access($quiz, $userid);
|
|
}
|
|
|
|
return new quiz($quiz, $cm, $course);
|
|
}
|
|
|
|
/**
|
|
* Create a {@see quiz_attempt} for an attempt at this quiz.
|
|
*
|
|
* @param object $attemptdata row from the quiz_attempts table.
|
|
* @return quiz_attempt the new quiz_attempt object.
|
|
*/
|
|
public function create_attempt_object($attemptdata) {
|
|
return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course);
|
|
}
|
|
|
|
// Functions for loading more data =========================================
|
|
|
|
/**
|
|
* Load just basic information about all the questions in this quiz.
|
|
*/
|
|
public function preload_questions() {
|
|
$slots = qbank_helper::get_question_structure($this->quiz->id, $this->context);
|
|
$this->questions = [];
|
|
foreach ($slots as $slot) {
|
|
$this->questions[$slot->questionid] = $slot;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fully load some or all of the questions for this quiz. You must call
|
|
* {@link preload_questions()} first.
|
|
*
|
|
* @param array|null $deprecated no longer supported (it was not used).
|
|
*/
|
|
public function load_questions($deprecated = null) {
|
|
if ($deprecated !== null) {
|
|
debugging('The argument to quiz::load_questions is no longer supported. ' .
|
|
'All questions are always loaded.', DEBUG_DEVELOPER);
|
|
}
|
|
if ($this->questions === null) {
|
|
throw new coding_exception('You must call preload_questions before calling load_questions.');
|
|
}
|
|
|
|
$questionstoprocess = [];
|
|
foreach ($this->questions as $question) {
|
|
if (is_number($question->questionid)) {
|
|
$question->id = $question->questionid;
|
|
$questionstoprocess[$question->questionid] = $question;
|
|
}
|
|
}
|
|
get_question_options($questionstoprocess);
|
|
}
|
|
|
|
/**
|
|
* Get an instance of the {@link \mod_quiz\structure} class for this quiz.
|
|
* @return \mod_quiz\structure describes the questions in the quiz.
|
|
*/
|
|
public function get_structure() {
|
|
return \mod_quiz\structure::create_for_quiz($this);
|
|
}
|
|
|
|
// Simple getters ==========================================================
|
|
/**
|
|
* Get the id of the course this quiz belongs to.
|
|
*
|
|
* @return int the course id.
|
|
*/
|
|
public function get_courseid() {
|
|
return $this->course->id;
|
|
}
|
|
|
|
/**
|
|
* Get the course settings object that this quiz belongs to.
|
|
*
|
|
* @return object the row of the course table.
|
|
*/
|
|
public function get_course() {
|
|
return $this->course;
|
|
}
|
|
|
|
/**
|
|
* Get this quiz's id (in the quiz table).
|
|
*
|
|
* @return int the quiz id.
|
|
*/
|
|
public function get_quizid() {
|
|
return $this->quiz->id;
|
|
}
|
|
|
|
/**
|
|
* Get the quiz settings object.
|
|
*
|
|
* @return stdClass the row of the quiz table.
|
|
*/
|
|
public function get_quiz() {
|
|
return $this->quiz;
|
|
}
|
|
|
|
/**
|
|
* Get the quiz name.
|
|
*
|
|
* @return string the name of this quiz.
|
|
*/
|
|
public function get_quiz_name() {
|
|
return $this->quiz->name;
|
|
}
|
|
|
|
/**
|
|
* Get the navigation method in use.
|
|
*
|
|
* @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ.
|
|
*/
|
|
public function get_navigation_method() {
|
|
return $this->quiz->navmethod;
|
|
}
|
|
|
|
/** @return int the number of attempts allowed at this quiz (0 = infinite). */
|
|
public function get_num_attempts_allowed() {
|
|
return $this->quiz->attempts;
|
|
}
|
|
|
|
/**
|
|
* Get the course-module id for this quiz.
|
|
*
|
|
* @return int the course_module id.
|
|
*/
|
|
public function get_cmid() {
|
|
return $this->cm->id;
|
|
}
|
|
|
|
/**
|
|
* Get the course-module object for this quiz.
|
|
*
|
|
* @return object the course_module object.
|
|
*/
|
|
public function get_cm() {
|
|
return $this->cm;
|
|
}
|
|
|
|
/**
|
|
* Get the quiz context.
|
|
*
|
|
* @return context_module the module context for this quiz.
|
|
*/
|
|
public function get_context() {
|
|
return $this->context;
|
|
}
|
|
|
|
/**
|
|
* @return bool whether the current user is someone who previews the quiz,
|
|
* rather than attempting it.
|
|
*/
|
|
public function is_preview_user() {
|
|
if (is_null($this->ispreviewuser)) {
|
|
$this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
|
|
}
|
|
return $this->ispreviewuser;
|
|
}
|
|
|
|
/**
|
|
* Checks user enrollment in the current course.
|
|
*
|
|
* @param int $userid the id of the user to check.
|
|
* @return bool whether the user is enrolled.
|
|
*/
|
|
public function is_participant($userid) {
|
|
return is_enrolled($this->get_context(), $userid, 'mod/quiz:attempt', $this->show_only_active_users());
|
|
}
|
|
|
|
/**
|
|
* Check is only active users in course should be shown.
|
|
*
|
|
* @return bool true if only active users should be shown.
|
|
*/
|
|
public function show_only_active_users() {
|
|
return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
|
|
}
|
|
|
|
/**
|
|
* @return bool whether any questions have been added to this quiz.
|
|
*/
|
|
public function has_questions() {
|
|
if ($this->questions === null) {
|
|
$this->preload_questions();
|
|
}
|
|
return !empty($this->questions);
|
|
}
|
|
|
|
/**
|
|
* @param int $id the question id.
|
|
* @return stdClass the question object with that id.
|
|
*/
|
|
public function get_question($id) {
|
|
return $this->questions[$id];
|
|
}
|
|
|
|
/**
|
|
* @param array|null $questionids question ids of the questions to load. null for all.
|
|
* @return stdClass[] the question data objects.
|
|
*/
|
|
public function get_questions($questionids = null) {
|
|
if (is_null($questionids)) {
|
|
$questionids = array_keys($this->questions);
|
|
}
|
|
$questions = array();
|
|
foreach ($questionids as $id) {
|
|
if (!array_key_exists($id, $this->questions)) {
|
|
throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
|
|
}
|
|
$questions[$id] = $this->questions[$id];
|
|
$this->ensure_question_loaded($id);
|
|
}
|
|
return $questions;
|
|
}
|
|
|
|
/**
|
|
* Get all the sections in this quiz.
|
|
*
|
|
* @return array 0, 1, 2, ... => quiz_sections row from the database.
|
|
*/
|
|
public function get_sections() {
|
|
global $DB;
|
|
if ($this->sections === null) {
|
|
$this->sections = array_values($DB->get_records('quiz_sections',
|
|
array('quizid' => $this->get_quizid()), 'firstslot'));
|
|
}
|
|
return $this->sections;
|
|
}
|
|
|
|
/**
|
|
* Return access_manager and instance of the access_manager class
|
|
* for this quiz at this time.
|
|
*
|
|
* @param int $timenow the current time as a unix timestamp.
|
|
* @return access_manager and instance of the access_manager class
|
|
* for this quiz at this time.
|
|
*/
|
|
public function get_access_manager($timenow) {
|
|
if (is_null($this->accessmanager)) {
|
|
$this->accessmanager = new access_manager($this, $timenow,
|
|
has_capability('mod/quiz:ignoretimelimits', $this->context, null, false));
|
|
}
|
|
return $this->accessmanager;
|
|
}
|
|
|
|
/**
|
|
* Wrapper round the has_capability funciton that automatically passes in the quiz context.
|
|
*
|
|
* @param string $capability the name of the capability to check. For example mod/quiz:view.
|
|
* @param int|null $userid A user id. By default (null) checks the permissions of the current user.
|
|
* @param bool $doanything If false, ignore effect of admin role assignment.
|
|
* @return boolean true if the user has this capability. Otherwise false.
|
|
*/
|
|
public function has_capability($capability, $userid = null, $doanything = true) {
|
|
return has_capability($capability, $this->context, $userid, $doanything);
|
|
}
|
|
|
|
/**
|
|
* Wrapper round the require_capability function that automatically passes in the quiz context.
|
|
*
|
|
* @param string $capability the name of the capability to check. For example mod/quiz:view.
|
|
* @param int|null $userid A user id. By default (null) checks the permissions of the current user.
|
|
* @param bool $doanything If false, ignore effect of admin role assignment.
|
|
*/
|
|
public function require_capability($capability, $userid = null, $doanything = true) {
|
|
require_capability($capability, $this->context, $userid, $doanything);
|
|
}
|
|
|
|
// URLs related to this attempt ============================================
|
|
/**
|
|
* @return string the URL of this quiz's view page.
|
|
*/
|
|
public function view_url() {
|
|
global $CFG;
|
|
return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
|
|
}
|
|
|
|
/**
|
|
* @return string the URL of this quiz's edit page.
|
|
*/
|
|
public function edit_url() {
|
|
global $CFG;
|
|
return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
|
|
}
|
|
|
|
/**
|
|
* @param int $attemptid the id of an attempt.
|
|
* @param int $page optional page number to go to in the attempt.
|
|
* @return string the URL of that attempt.
|
|
*/
|
|
public function attempt_url($attemptid, $page = 0) {
|
|
global $CFG;
|
|
$url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
|
|
if ($page) {
|
|
$url .= '&page=' . $page;
|
|
}
|
|
$url .= '&cmid=' . $this->get_cmid();
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get the URL to start/continue an attempt.
|
|
*
|
|
* @param int $page page in the attempt to start on (optional).
|
|
* @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
|
|
*/
|
|
public function start_attempt_url($page = 0) {
|
|
$params = array('cmid' => $this->cm->id, 'sesskey' => sesskey());
|
|
if ($page) {
|
|
$params['page'] = $page;
|
|
}
|
|
return new moodle_url('/mod/quiz/startattempt.php', $params);
|
|
}
|
|
|
|
/**
|
|
* @param int $attemptid the id of an attempt.
|
|
* @return string the URL of the review of that attempt.
|
|
*/
|
|
public function review_url($attemptid) {
|
|
return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid()));
|
|
}
|
|
|
|
/**
|
|
* @param int $attemptid the id of an attempt.
|
|
* @return string the URL of the review of that attempt.
|
|
*/
|
|
public function summary_url($attemptid) {
|
|
return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid()));
|
|
}
|
|
|
|
// Bits of content =========================================================
|
|
|
|
/**
|
|
* @param bool $notused not used.
|
|
* @return string an empty string.
|
|
* @deprecated since 3.1. This sort of functionality is now entirely handled by quiz access rules.
|
|
*/
|
|
public function confirm_start_attempt_message($notused) {
|
|
debugging('confirm_start_attempt_message is deprecated. ' .
|
|
'This sort of functionality is now entirely handled by quiz access rules.');
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* If $reviewoptions->attempt is false, meaning that students can't review this
|
|
* attempt at the moment, return an appropriate string explaining why.
|
|
*
|
|
* @param int $when One of the display_options::DURING,
|
|
* IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
|
|
* @param bool $short if true, return a shorter string.
|
|
* @return string an appropraite message.
|
|
*/
|
|
public function cannot_review_message($when, $short = false) {
|
|
|
|
if ($short) {
|
|
$langstrsuffix = 'short';
|
|
$dateformat = get_string('strftimedatetimeshort', 'langconfig');
|
|
} else {
|
|
$langstrsuffix = '';
|
|
$dateformat = '';
|
|
}
|
|
|
|
if ($when == display_options::DURING ||
|
|
$when == display_options::IMMEDIATELY_AFTER) {
|
|
return '';
|
|
} else if ($when == display_options::LATER_WHILE_OPEN && $this->quiz->timeclose &&
|
|
$this->quiz->reviewattempt & display_options::AFTER_CLOSE) {
|
|
return get_string('noreviewuntil' . $langstrsuffix, 'quiz',
|
|
userdate($this->quiz->timeclose, $dateformat));
|
|
} else {
|
|
return get_string('noreview' . $langstrsuffix, 'quiz');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Probably not used any more, but left for backwards compatibility.
|
|
*
|
|
* @param string $title the name of this particular quiz page.
|
|
* @return string always returns ''.
|
|
*/
|
|
public function navigation($title) {
|
|
global $PAGE;
|
|
$PAGE->navbar->add($title);
|
|
return '';
|
|
}
|
|
|
|
// Private methods =========================================================
|
|
/**
|
|
* Check that the definition of a particular question is loaded, and if not throw an exception.
|
|
*
|
|
* @param int $id a question id.
|
|
*/
|
|
protected function ensure_question_loaded($id) {
|
|
if (isset($this->questions[$id]->_partiallyloaded)) {
|
|
throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return all the question types used in this quiz.
|
|
*
|
|
* @param boolean $includepotential if the quiz include random questions,
|
|
* setting this flag to true will make the function to return all the
|
|
* possible question types in the random questions category.
|
|
* @return array a sorted array including the different question types.
|
|
* @since Moodle 3.1
|
|
*/
|
|
public function get_all_question_types_used($includepotential = false) {
|
|
$questiontypes = array();
|
|
|
|
// To control if we need to look in categories for questions.
|
|
$qcategories = array();
|
|
|
|
foreach ($this->get_questions() as $questiondata) {
|
|
if ($questiondata->qtype === 'random' && $includepotential) {
|
|
if (!isset($qcategories[$questiondata->category])) {
|
|
$qcategories[$questiondata->category] = false;
|
|
}
|
|
if (!empty($questiondata->filtercondition)) {
|
|
$filtercondition = json_decode($questiondata->filtercondition);
|
|
$qcategories[$questiondata->category] = !empty($filtercondition->includingsubcategories);
|
|
}
|
|
} else {
|
|
if (!in_array($questiondata->qtype, $questiontypes)) {
|
|
$questiontypes[] = $questiondata->qtype;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($qcategories)) {
|
|
// We have to look for all the question types in these categories.
|
|
$categoriestolook = array();
|
|
foreach ($qcategories as $cat => $includesubcats) {
|
|
if ($includesubcats) {
|
|
$categoriestolook = array_merge($categoriestolook, question_categorylist($cat));
|
|
} else {
|
|
$categoriestolook[] = $cat;
|
|
}
|
|
}
|
|
$questiontypesincategories = question_bank::get_all_question_types_in_categories($categoriestolook);
|
|
$questiontypes = array_merge($questiontypes, $questiontypesincategories);
|
|
}
|
|
$questiontypes = array_unique($questiontypes);
|
|
sort($questiontypes);
|
|
|
|
return $questiontypes;
|
|
}
|
|
}
|