Merge branch 'MDL-3030' of git://github.com/timhunt/moodle

This commit is contained in:
Dan Poltawski
2012-04-30 11:05:19 +08:00
46 changed files with 1485 additions and 434 deletions
+7 -14
View File
@@ -392,15 +392,14 @@ class quiz_access_manager {
}
/**
* Will cause the attempt time to start counting down after the page has loaded,
* if that is necessary.
* Compute how much time is left before this attempt must be submitted.
*
* @param object $attempt the data from the relevant quiz_attempts row.
* @param int $timenow the time to consider as 'now'.
* @param mod_quiz_renderer $output the quiz renderer.
* @return int|false the number of seconds remaining for this attempt.
* False if there is no limit.
*/
public function show_attempt_timer_if_needed($attempt, $timenow, $output) {
public function get_time_left($attempt, $timenow) {
$timeleft = false;
foreach ($this->rules as $rule) {
$ruletimeleft = $rule->time_left($attempt, $timenow);
@@ -408,13 +407,7 @@ class quiz_access_manager {
$timeleft = $ruletimeleft;
}
}
if ($timeleft !== false) {
// Make sure the timer starts just above zero. If $timeleft was <= 0, then
// this will just have the effect of causing the quiz to be submitted immediately.
$timerstartvalue = max($timeleft, 1);
$output->initialise_timer($timerstartvalue);
}
return $timeleft;
}
/**
@@ -469,8 +462,8 @@ class quiz_access_manager {
*/
public function make_review_link($attempt, $reviewoptions, $output) {
// If review of responses is not allowed, or the attempt is still open, don't link.
if (!$attempt->timefinish) {
// If the attempt is still open, don't link.
if (in_array($attempt->state, array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE))) {
return $output->no_review_message('');
}
+22 -4
View File
@@ -64,11 +64,29 @@ class quizaccess_openclosedate extends quiz_access_rule_base {
}
public function prevent_access() {
if ($this->timenow < $this->quiz->timeopen ||
($this->quiz->timeclose && $this->timenow > $this->quiz->timeclose)) {
return get_string('notavailable', 'quizaccess_openclosedate');
$message = get_string('notavailable', 'quizaccess_openclosedate');
if ($this->timenow < $this->quiz->timeopen) {
return $message;
}
return false;
if (!$this->quiz->timeclose) {
return false;
}
if ($this->timenow <= $this->quiz->timeclose) {
return false;
}
if ($this->quiz->overduehandling != 'graceperiod') {
return $message;
}
if ($this->timenow <= $this->quiz->timeclose + $this->quiz->graceperiod) {
return false;
}
return $message;
}
public function is_finished($numprevattempts, $lastattempt) {
@@ -42,6 +42,7 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
$quiz = new stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 0;
$quiz->overduehandling = 'autosubmit';
$quiz->questions = '';
$cm = new stdClass();
$cm->id = 0;
@@ -69,6 +70,7 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
$quiz = new stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 0;
$quiz->overduehandling = 'autosubmit';
$quiz->questions = '';
$cm = new stdClass();
$cm->id = 0;
@@ -98,6 +100,7 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
$quiz = new stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'autosubmit';
$quiz->questions = '';
$cm = new stdClass();
$cm->id = 0;
@@ -133,6 +136,7 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
$quiz = new stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'autosubmit';
$quiz->questions = '';
$cm = new stdClass();
$cm->id = 0;
@@ -177,4 +181,31 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
$this->assertEquals($rule->time_left($attempt, 20000), 0);
$this->assertEquals($rule->time_left($attempt, 20100), -100);
}
public function test_close_date_with_overdue() {
$quiz = new stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'graceperiod';
$quiz->graceperiod = 1000;
$quiz->questions = '';
$cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
$attempt = new stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 20000);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 20001);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 21000);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 21001);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
}
}
+5 -5
View File
@@ -17,10 +17,9 @@
/**
* This script displays a particular page of a quiz attempt that is in progress.
*
* @package mod
* @subpackage quiz
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
@@ -70,6 +69,8 @@ if (!$attemptobj->is_preview_user()) {
// If the attempt is already closed, send them to the review page.
if ($attemptobj->is_finished()) {
redirect($attemptobj->review_url(null, $page));
} else if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
redirect($attemptobj->summary_url());
}
// Check the access rules.
@@ -126,5 +127,4 @@ if ($attemptobj->is_last_page($page)) {
$nextpage = $page + 1;
}
$accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time(), $output);
echo $output->attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, $nextpage);
+254 -30
View File
@@ -20,10 +20,9 @@
* There are classes for loading all the information about a quiz and attempts,
* and for displaying the navigation panel.
*
* @package mod
* @subpackage quiz
* @copyright 2008 onwards Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package mod_quiz
* @copyright 2008 onwards Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -34,9 +33,9 @@ defined('MOODLE_INTERNAL') || die();
* Class for quiz exceptions. Just saves a couple of arguments on the
* constructor for a moodle_exception.
*
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.0
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.0
*/
class moodle_quiz_exception extends moodle_exception {
public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) {
@@ -118,6 +117,15 @@ class quiz {
return new quiz($quiz, $cm, $course);
}
/**
* Create a {@link 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 =====================================================
/**
@@ -324,6 +332,14 @@ class quiz {
return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
}
/**
* @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));
}
// Bits of content =====================================================================
/**
@@ -411,12 +427,22 @@ class quiz {
* @since Moodle 2.0
*/
class quiz_attempt {
// Fields initialised in the constructor.
/** @var string to identify the in progress state. */
const IN_PROGRESS = 'inprogress';
/** @var string to identify the overdue state. */
const OVERDUE = 'overdue';
/** @var string to identify the finished state. */
const FINISHED = 'finished';
/** @var string to identify the abandoned state. */
const ABANDONED = 'abandoned';
// Basic data
protected $quizobj;
protected $attempt;
protected $quba;
// Fields set later if that data is needed.
// More details of what happened for each question.
protected $quba;
protected $pagelayout; // array page no => array of numbers on the page in order.
protected $reviewoptions = null;
@@ -482,6 +508,14 @@ class quiz_attempt {
return self::create_helper(array('uniqueid' => $usageid));
}
/**
* @param string $state one of the state constants like IN_PROGRESS.
* @return string the human-readable state name.
*/
public static function state_name($state) {
return quiz_attempt_state_name($state);
}
private function determine_layout() {
$this->pagelayout = array();
@@ -612,6 +646,10 @@ class quiz_attempt {
return $this->attempt->attempt;
}
public function get_state() {
return $this->attempt->state;
}
/** @return int the id of the user this attempt belongs to. */
public function get_userid() {
return $this->attempt->userid;
@@ -622,12 +660,17 @@ class quiz_attempt {
return $this->attempt->currentpage;
}
public function get_sum_marks() {
return $this->attempt->sumgrades;
}
/**
* @return bool whether this attempt has been finished (true) or is still
* in progress (false).
* in progress (false). Be warned that this is not just state == self::FINISHED,
* it also includes self::ABANDONED.
*/
public function is_finished() {
return $this->attempt->timefinish != 0;
return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED;
}
/** @return bool whether this attempt is a preview attempt. */
@@ -927,6 +970,59 @@ class quiz_attempt {
return $this->quba->get_question_action_time($slot);
}
/**
* Get the time remaining for an in-progress attempt, if the time is short
* enought that it would be worth showing a timer.
* @param int $timenow the time to consider as 'now'.
* @return int|false the number of seconds remaining for this attempt.
* False if there is no limit.
*/
public function get_time_left($timenow) {
if ($this->attempt->state != self::IN_PROGRESS) {
return false;
}
return $this->get_access_manager($timenow)->get_time_left($this->attempt, $timenow);
}
/**
* @return int the time when this attempt was submitted. 0 if it has not been
* submitted yet.
*/
public function get_submitted_date() {
return $this->attempt->timefinish;
}
/**
* If the attempt is in an applicable state, work out the time by which the
* student should next do something.
* @return int timestamp by which the student needs to do something.
*/
function get_due_date() {
$deadlines = array();
if ($this->quizobj->get_quiz()->timelimit) {
$deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit;
}
if ($this->quizobj->get_quiz()->timeclose) {
$deadlines[] = $this->quizobj->get_quiz()->timeclose;
}
if ($deadlines) {
$duedate = min($deadlines);
} else {
return false;
}
switch ($this->attempt->state) {
case self::IN_PROGRESS:
return $duedate;
case self::OVERDUE:
return $duedate + $this->quizobj->get_quiz()->graceperiod;
default:
throw new coding_exception('Unexpected state: ' . $this->attempt->state);
}
}
// URLs related to this attempt ========================================================
/**
* @return string quiz view url.
@@ -1146,26 +1242,81 @@ class quiz_attempt {
// Methods for processing ==================================================
/**
* Check this attempt, to see if there are any state transitions that should
* happen automatically.
* @param int $timestamp the timestamp that should be stored as the modifed
* @param bool $studentisonline is the student currently interacting with Moodle?
*/
public function handle_if_time_expired($timestamp, $studentisonline) {
global $DB;
$timeleft = $this->get_access_manager($timestamp)->get_time_left($this->attempt, $timestamp);
if ($timeleft === false || $timeleft > 0) {
return; // Time has not yet expired.
}
// If the attempt is already overdue, look to see if it should be abandoned ...
if ($this->attempt->state == quiz_attempt::OVERDUE) {
$timeoverdue = -$timeleft;
if ($timeoverdue > $this->quizobj->get_quiz()->graceperiod) {
$this->process_abandon($timestamp, $studentisonline);
}
return; // ... and we are done.
}
if ($this->attempt->state != quiz_attempt::IN_PROGRESS) {
return; // Attempt is already in a final state.
}
// Otherwise, we were in quiz_attempt::IN_PROGRESS, and time has now expired.
// Transition to the appropriate state.
switch ($this->quizobj->get_quiz()->overduehandling) {
case 'autosubmit':
$this->process_finish($timestamp, false);
return;
case 'graceperiod':
$this->process_going_overdue($timestamp, $studentisonline);
return;
case 'autoabandon':
$this->process_abandon($timestamp, $studentisonline);
return;
}
}
/**
* Process all the actions that were submitted as part of the current request.
*
* @param int $timestamp the timestamp that should be stored as the modifed
* time in the database for these actions. If null, will use the current time.
*/
public function process_all_actions($timestamp) {
public function process_submitted_actions($timestamp, $becomingoverdue = false) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$this->quba->process_all_actions($timestamp);
question_engine::save_questions_usage_by_activity($this->quba);
$this->attempt->timemodified = $timestamp;
if ($this->attempt->timefinish) {
if ($this->attempt->state == self::FINISHED) {
$this->attempt->sumgrades = $this->quba->get_total_mark();
}
$DB->update_record('quiz_attempts', $this->attempt);
if ($becomingoverdue) {
$this->process_going_overdue($timestamp, true);
} else {
$DB->update_record('quiz_attempts', $this->attempt);
}
if (!$this->is_preview() && $this->attempt->timefinish) {
if (!$this->is_preview() && $this->attempt->state == quiz_attempt::FINISHED) {
quiz_save_best_grade($this->get_quiz(), $this->get_userid());
}
$transaction->allow_commit();
}
/**
@@ -1173,13 +1324,20 @@ class quiz_attempt {
* flagged state was changed in the request.
*/
public function save_question_flags() {
$transaction = $DB->start_delegated_transaction();
$this->quba->update_question_flags();
question_engine::save_questions_usage_by_activity($this->quba);
$transaction->allow_commit();
}
public function finish_attempt($timestamp) {
global $DB, $USER;
$this->quba->process_all_actions($timestamp);
public function process_finish($timestamp, $processsubmitted) {
global $DB;
$transaction = $DB->start_delegated_transaction();
if ($processsubmitted) {
$this->quba->process_all_actions($timestamp);
}
$this->quba->finish_all_questions($timestamp);
question_engine::save_questions_usage_by_activity($this->quba);
@@ -1187,26 +1345,92 @@ class quiz_attempt {
$this->attempt->timemodified = $timestamp;
$this->attempt->timefinish = $timestamp;
$this->attempt->sumgrades = $this->quba->get_total_mark();
$this->attempt->state = self::FINISHED;
$DB->update_record('quiz_attempts', $this->attempt);
if (!$this->is_preview()) {
quiz_save_best_grade($this->get_quiz(), $this->attempt->userid);
// Trigger event
$eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->attemptid = $this->attempt->id;
$eventdata->timefinish = $this->attempt->timefinish;
$eventdata->userid = $this->attempt->userid;
$eventdata->submitterid = $USER->id;
$eventdata->quizid = $this->get_quizid();
$eventdata->cmid = $this->get_cmid();
$eventdata->courseid = $this->get_courseid();
events_trigger('quiz_attempt_submitted', $eventdata);
$this->fire_state_transition_event('quiz_attempt_submitted', $timestamp);
// Tell any access rules that care that the attempt is over.
$this->get_access_manager($timestamp)->current_attempt_finished();
}
$transaction->allow_commit();
}
/**
* Mark this attempt as now overdue.
* @param int $timestamp the time to deem as now.
* @param bool $studentisonline is the student currently interacting with Moodle?
*/
public function process_going_overdue($timestamp, $studentisonline) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::OVERDUE;
$DB->update_record('quiz_attempts', $this->attempt);
$this->fire_state_transition_event('quiz_attempt_overdue', $timestamp);
$transaction->allow_commit();
}
/**
* Mark this attempt as abandoned.
* @param int $timestamp the time to deem as now.
* @param bool $studentisonline is the student currently interacting with Moodle?
*/
public function process_abandon($timestamp, $studentisonline) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::ABANDONED;
$DB->update_record('quiz_attempts', $this->attempt);
$this->fire_state_transition_event('quiz_attempt_abandoned', $timestamp);
$transaction->allow_commit();
}
/**
* Fire a state transition event.
* @param string $event the type of event. Should be listed in db/events.php.
* @param int $timestamp the timestamp to include in the event.
*/
protected function fire_state_transition_event($event, $timestamp) {
global $USER;
// Trigger event
$eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->attemptid = $this->attempt->id;
$eventdata->timestamp = $timestamp;
$eventdata->userid = $this->attempt->userid;
$eventdata->quizid = $this->get_quizid();
$eventdata->cmid = $this->get_cmid();
$eventdata->courseid = $this->get_courseid();
// I don't think if (CLI_SCRIPT) is really the right logic here. The
// question is really 'is $USER currently set to a real user', but I cannot
// see standard Moodle function to answer that question. For example,
// cron fakes $USER.
if (CLI_SCRIPT) {
$eventdata->submitterid = null;
} else {
$eventdata->submitterid = $USER->id;
}
if ($event == 'quiz_attempt_submitted') {
// Backwards compatibility for this event type. $eventdata->timestamp is now preferred.
$eventdata->timefinish = $timestamp;
}
events_trigger($event, $eventdata);
}
/**
@@ -1421,7 +1645,7 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base {
public function render_end_bits(mod_quiz_renderer $output) {
return html_writer::link($this->attemptobj->summary_url(),
get_string('endtest', 'quiz'), array('class' => 'endtestlink')) .
$output->countdown_timer() .
$output->countdown_timer($this->attemptobj, time()) .
$this->render_restart_preview_link($output);
}
}
@@ -40,17 +40,16 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
// Define each element separated
$quiz = new backup_nested_element('quiz', array('id'), array(
'name', 'intro', 'introformat', 'timeopen',
'timeclose', 'preferredbehaviour', 'attempts_number',
'attemptonlast', 'grademethod', 'navmethod', 'decimalpoints', 'questiondecimalpoints',
'name', 'intro', 'introformat', 'timeopen', 'timeclose', 'timelimit',
'overduehandling', 'graceperiod', 'preferredbehaviour', 'attempts_number',
'attemptonlast', 'grademethod', 'decimalpoints', 'questiondecimalpoints',
'reviewattempt', 'reviewcorrectness', 'reviewmarks',
'reviewspecificfeedback', 'reviewgeneralfeedback',
'reviewrightanswer', 'reviewoverallfeedback',
'questionsperpage', 'shufflequestions', 'shuffleanswers',
'questionsperpage', 'navmethod', 'shufflequestions', 'shuffleanswers',
'questions', 'sumgrades', 'grade', 'timecreated',
'timemodified', 'timelimit', 'password', 'subnet',
'browsersecurity', 'delay1', 'delay2', 'showuserpicture',
'showblocks'));
'timemodified', 'password', 'subnet', 'browsersecurity',
'delay1', 'delay2', 'showuserpicture', 'showblocks'));
// Define elements for access rule subplugin settings.
$this->add_subplugin_structure('quizaccess', $quiz, true);
@@ -79,9 +78,8 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
$attempts = new backup_nested_element('attempts');
$attempt = new backup_nested_element('attempt', array('id'), array(
'uniqueid', 'userid', 'attemptnum', 'sumgrades',
'timestart', 'timefinish', 'timemodified', 'layout',
'preview', 'currentpage'));
'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview',
'state', 'timestart', 'timefinish', 'timemodified', 'sumgrades'));
// This module is using questions, so produce the related question states and sessions
// attaching them to the $attempt element based in 'uniqueid' matching
+1 -1
View File
@@ -57,7 +57,7 @@ echo $OUTPUT->heading(format_string($attemptobj->get_question_name($slot)));
if (data_submitted() && confirm_sesskey()) {
if (optional_param('submit', false, PARAM_BOOL)) {
$transaction = $DB->start_delegated_transaction();
$attemptobj->process_all_actions(time());
$attemptobj->process_submitted_actions(time());
$transaction->allow_commit();
echo $OUTPUT->notification(get_string('changessaved'), 'notifysuccess');
close_window(2, true);
+149
View File
@@ -0,0 +1,149 @@
<?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/>.
/**
* Library code used by quiz cron.
*
* @package mod_quiz
* @copyright 2012 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* This class holds all the code for automatically updating all attempts that have
* gone over their time limit.
*
* @copyright 2012 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_overdue_attempt_updater {
/**
* Do the processing required.
* @param int $timenow the time to consider as 'now' during the processing.
* @param int $processfrom the value of $processupto the last time update_overdue_attempts was
* called called and completed successfully.
* @param int $processto only process attempt modifed longer ago than this.
*/
public function update_overdue_attempts($timenow, $processfrom, $processto) {
global $DB;
$attemptstoprocess = $this->get_list_of_overdue_attempts($processfrom, $processto);
$course = null;
$quiz = null;
$cm = null;
foreach ($attemptstoprocess as $attempt) {
// If we have moved on to a different quiz, fetch the new data.
if (!$quiz || $attempt->quiz != $quiz->id) {
$quiz = $DB->get_record('quiz', array('id' => $attempt->quiz), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('quiz', $attempt->quiz);
}
// If we have moved on to a different course, fetch the new data.
if (!$course || $course->id != $quiz->course) {
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
}
// Make a specialised version of the quiz settings, with the relevant overrides.
$quizforuser = clone($quiz);
$quizforuser->timeclose = $attempt->usertimeclose;
$quizforuser->timelimit = $attempt->usertimelimit;
// Trigger any transitions that are required.
$attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course);
$attemptobj->handle_if_time_expired($timenow, false);
}
$attemptstoprocess->close();
}
/**
* @return moodle_recordset of quiz_attempts that need to be processed because time has
* passed. The array is sorted by courseid then quizid.
*/
protected function get_list_of_overdue_attempts($processfrom, $processto) {
global $DB;
// This query should have all the quiz_attempts columns.
return $DB->get_recordset_sql("
SELECT quiza.id,
quiza.quiz,
quiza.userid,
quiza.attempt,
quiza.uniqueid,
quiza.layout,
quiza.currentpage,
quiza.preview,
quiza.state,
quiza.timestart,
quiza.timefinish,
quiza.timemodified,
quiza.sumgrades,
COALESCE(quo.timeclose, MAX(qgo.timeclose), quiz.timeclose) AS usertimeclose,
COALESCE(quo.timelimit, MAX(qgo.timelimit), quiz.timelimit) AS usertimelimit
FROM {quiz_attempts} quiza
JOIN {quiz} quiz ON quiz.id = quiza.quiz
LEFT JOIN {quiz_overrides} quo ON quo.quiz = quiz.id AND quo.userid = quiza.userid
LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id
LEFT JOIN {groups_members} gm ON gm.userid = quiza.userid AND gm.groupid = qgo.groupid
WHERE quiza.state IN ('inprogress', 'overdue')
AND quiza.timemodified >= :processfrom
AND quiza.timemodified < :processto
GROUP BY quiza.id,
quiza.quiz,
quiza.userid,
quiza.attempt,
quiza.uniqueid,
quiza.layout,
quiza.currentpage,
quiza.preview,
quiza.state,
quiza.timestart,
quiza.timefinish,
quiza.timemodified,
quiza.sumgrades,
quiz.course,
quiz.timeclose,
quiz.timelimit,
quiz.graceperiod,
quo.timeclose,
quo.timelimit
HAVING (quiza.state = 'inprogress' AND (
:timenow1 > COALESCE(quo.timeclose, MAX(qgo.timeclose), quiz.timeclose)
OR :timenow2 > timestart + COALESCE(quo.timelimit, MAX(qgo.timelimit), quiz.timelimit)))
OR (quiza.state = 'overdue' AND (
:timenow3 > graceperiod + COALESCE(quo.timeclose, MAX(qgo.timeclose), quiz.timeclose)
OR :timenow4 > timestart + graceperiod + COALESCE(quo.timelimit, MAX(qgo.timelimit), quiz.timelimit)))
ORDER BY quiz.course, quiza.quiz",
array('processfrom' => $processfrom, 'processto' => $processto,
'timenow1' => $processto, 'timenow2' => $processto,
'timenow3' => $processto, 'timenow4' => $processto));
}
}
+8 -1
View File
@@ -173,6 +173,13 @@ $capabilities = array(
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array()
)
),
// Receive a notification message when a quiz attempt becomes overdue.
'mod/quiz:emailwarnoverdue' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array()
),
);
+30
View File
@@ -35,6 +35,14 @@ $handlers = array(
'handlerfunction' => 'quiz_attempt_submitted_handler',
'schedule' => 'cron',
),
// Handle our own quiz_attempt_overdue event, to email the student to let them
// know they forgot to submit, and that they have another chance.
'quiz_attempt_overdue' => array (
'handlerfile' => '/mod/quiz/locallib.php',
'handlerfunction' => 'quiz_attempt_overdue_handler',
'schedule' => 'cron',
),
);
/* List of events generated by the quiz module, with the fields on the event object.
@@ -43,6 +51,7 @@ quiz_attempt_started
->component = 'mod_quiz';
->attemptid = // The id of the new quiz attempt.
->timestart = // The timestamp of when the attempt was started.
->timestamp = // The timestamp of when the attempt was started.
->userid = // The user id that the attempt belongs to.
->quizid = // The quiz id of the quiz the attempt belongs to.
->cmid = // The course_module id of the quiz the attempt belongs to.
@@ -52,10 +61,31 @@ quiz_attempt_submitted
->component = 'mod_quiz';
->attemptid = // The id of the quiz attempt that was submitted.
->timefinish = // The timestamp of when the attempt was submitted.
->timestamp = // The timestamp of when the attempt was submitted.
->userid = // The user id that the attempt belongs to.
->submitterid = // The user id of the user who sumitted the attempt.
->quizid = // The quiz id of the quiz the attempt belongs to.
->cmid = // The course_module id of the quiz the attempt belongs to.
->courseid = // The course id of the course the quiz belongs to.
quiz_attempt_overdue
->component = 'mod_quiz';
->attemptid = // The id of the quiz attempt that has become overdue.
->timestamp = // The timestamp of when the attempt become overdue.
->userid = // The user id that the attempt belongs to.
->submitterid = // The user id of the user who triggered this transition (may be null, e.g. on cron.).
->quizid = // The quiz id of the quiz the attempt belongs to.
->cmid = // The course_module id of the quiz the attempt belongs to.
->courseid = // The course id of the course the quiz belongs to.
quiz_attempt_abandoned
->component = 'mod_quiz';
->attemptid = // The id of the quiz attempt that was submitted.
->timestamp = // The timestamp of when the attempt was submitted.
->userid = // The user id that the attempt belongs to.
->submitterid = // The user id of the user who triggered this transition (may be null, e.g. on cron.).
->quizid = // The quiz id of the quiz the attempt belongs to.
->cmid = // The course_module id of the quiz the attempt belongs to.
->courseid = // The course id of the course the quiz belongs to.
*/
+51 -47
View File
@@ -4,21 +4,24 @@
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="quiz" COMMENT="Main information about each quiz" NEXT="quiz_attempts">
<TABLE NAME="quiz" COMMENT="The settings for each quiz." NEXT="quiz_attempts">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="course"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="name"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="course" NEXT="intro"/>
<FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false" PREVIOUS="name" NEXT="introformat"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="intro" NEXT="timeopen"/>
<FIELD NAME="timeopen" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="introformat" NEXT="timeclose"/>
<FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timeopen" NEXT="preferredbehaviour"/>
<FIELD NAME="preferredbehaviour" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" PREVIOUS="timeclose" NEXT="attempts"/>
<FIELD NAME="attempts" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="preferredbehaviour" NEXT="attemptonlast"/>
<FIELD NAME="attemptonlast" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="attempts" NEXT="grademethod"/>
<FIELD NAME="grademethod" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="attemptonlast" NEXT="decimalpoints"/>
<FIELD NAME="decimalpoints" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="2" SEQUENCE="false" PREVIOUS="grademethod" NEXT="questiondecimalpoints"/>
<FIELD NAME="questiondecimalpoints" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="-2" SEQUENCE="false" PREVIOUS="decimalpoints" NEXT="reviewattempt"/>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Standard Moodle primary key." NEXT="course"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the course this quiz is part of." PREVIOUS="id" NEXT="name"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz name." PREVIOUS="course" NEXT="intro"/>
<FIELD NAME="intro" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz introduction text." PREVIOUS="name" NEXT="introformat"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Quiz intro text format." PREVIOUS="intro" NEXT="timeopen"/>
<FIELD NAME="timeopen" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz opens. (0 = no restriction.)" PREVIOUS="introformat" NEXT="timeclose"/>
<FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz closes. (0 = no restriction.)" PREVIOUS="timeopen" NEXT="timelimit"/>
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time limit for quiz attempts, in seconds." PREVIOUS="timeclose" NEXT="overduehandling"/>
<FIELD NAME="overduehandling" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="autoabandon" SEQUENCE="false" COMMENT="The method used to handle overdue attempts. 'autosubmit', 'graceperiod' or 'autoabandon'." PREVIOUS="timelimit" NEXT="graceperiod"/>
<FIELD NAME="graceperiod" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The amount of time (in seconds) after the time limit runs out during which attempts can still be submitted, if overduehandling is set to allow it." PREVIOUS="overduehandling" NEXT="preferredbehaviour"/>
<FIELD NAME="preferredbehaviour" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" COMMENT="The behaviour to ask questions to use." PREVIOUS="graceperiod" NEXT="attempts"/>
<FIELD NAME="attempts" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The maximum number of attempts a student is allowed." PREVIOUS="preferredbehaviour" NEXT="attemptonlast"/>
<FIELD NAME="attemptonlast" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether subsequent attempts start from teh answer to the previous attempt (1) or start blank (0)." PREVIOUS="attempts" NEXT="grademethod"/>
<FIELD NAME="grademethod" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST." PREVIOUS="attemptonlast" NEXT="decimalpoints"/>
<FIELD NAME="decimalpoints" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="2" SEQUENCE="false" COMMENT="Number of decimal points to use when displaying grades." PREVIOUS="grademethod" NEXT="questiondecimalpoints"/>
<FIELD NAME="questiondecimalpoints" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="-2" SEQUENCE="false" COMMENT="Number of decimal points to use when displaying question grades. (-2 means use decimalpoints.)" PREVIOUS="decimalpoints" NEXT="reviewattempt"/>
<FIELD NAME="reviewattempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. This is a bit field, decoded by the mod_quiz_display_options class. It is formed by ORing together the constants defined there." PREVIOUS="questiondecimalpoints" NEXT="reviewcorrectness"/>
<FIELD NAME="reviewcorrectness" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. A bit field, like reviewattempt." PREVIOUS="reviewattempt" NEXT="reviewmarks"/>
<FIELD NAME="reviewmarks" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. A bit field, like reviewattempt." PREVIOUS="reviewcorrectness" NEXT="reviewspecificfeedback"/>
@@ -26,21 +29,20 @@
<FIELD NAME="reviewgeneralfeedback" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. A bit field, like reviewattempt." PREVIOUS="reviewspecificfeedback" NEXT="reviewrightanswer"/>
<FIELD NAME="reviewrightanswer" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. A bit field, like reviewattempt." PREVIOUS="reviewgeneralfeedback" NEXT="reviewoverallfeedback"/>
<FIELD NAME="reviewoverallfeedback" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether users are allowed to review their quiz attempts at various times. A bit field, like reviewattempt." PREVIOUS="reviewrightanswer" NEXT="questionsperpage"/>
<FIELD NAME="questionsperpage" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="reviewoverallfeedback" NEXT="navmethod"/>
<FIELD NAME="navmethod" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="free" SEQUENCE="false" PREVIOUS="questionsperpage" NEXT="shufflequestions"/>
<FIELD NAME="shufflequestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="navmethod" NEXT="shuffleanswers"/>
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="shufflequestions" NEXT="questions"/>
<FIELD NAME="questions" TYPE="text" NOTNULL="true" SEQUENCE="false" PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" PREVIOUS="questions" NEXT="grade"/>
<FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" PREVIOUS="sumgrades" NEXT="timecreated"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="grade" NEXT="timemodified"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timecreated" NEXT="timelimit"/>
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timemodified" NEXT="password"/>
<FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="timelimit" NEXT="subnet"/>
<FIELD NAME="subnet" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="password" NEXT="browsersecurity"/>
<FIELD NAME="browsersecurity" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" PREVIOUS="subnet" NEXT="delay1"/>
<FIELD NAME="delay1" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="browsersecurity" NEXT="delay2"/>
<FIELD NAME="delay2" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="delay1" NEXT="showuserpicture"/>
<FIELD NAME="questionsperpage" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="How often to insert a page break when editing the quiz, or when shuffling the question order." PREVIOUS="reviewoverallfeedback" NEXT="navmethod"/>
<FIELD NAME="navmethod" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="free" SEQUENCE="false" COMMENT="Any constraints on how the user is allowed to navigate around the quiz. Currently recognised values are 'free' and 'seq'." PREVIOUS="questionsperpage" NEXT="shufflequestions"/>
<FIELD NAME="shufflequestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the question order should be shuffled for each attempt." PREVIOUS="navmethod" NEXT="shuffleanswers"/>
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the parts of the question should be shuffled, in those question types that support it." PREVIOUS="shufflequestions" NEXT="questions"/>
<FIELD NAME="questions" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table." PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total of all the question instance maxmarks." PREVIOUS="questions" NEXT="grade"/>
<FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total that the quiz overall grade is scaled to be out of." PREVIOUS="sumgrades" NEXT="timecreated"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when the quiz was added to the course." PREVIOUS="grade" NEXT="timemodified"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timecreated" NEXT="password"/>
<FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="A password that the student must enter before starting or continuing a quiz attempt." PREVIOUS="timemodified" NEXT="subnet"/>
<FIELD NAME="subnet" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Used to restrict the IP addresses from which this quiz can be attempted. The format is as requried by the address_in_subnet function." PREVIOUS="password" NEXT="browsersecurity"/>
<FIELD NAME="browsersecurity" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" COMMENT="Restriciton on the browser the student must use. E.g. 'securewindow'." PREVIOUS="subnet" NEXT="delay1"/>
<FIELD NAME="delay1" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Delay that must be left between the first and second attempt, in seconds." PREVIOUS="browsersecurity" NEXT="delay2"/>
<FIELD NAME="delay2" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Delay that must be left between the second and subsequent attempt, in seconds." PREVIOUS="delay1" NEXT="showuserpicture"/>
<FIELD NAME="showuserpicture" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Option to show the user's picture during the attempt and on the review page." PREVIOUS="delay2" NEXT="showblocks"/>
<FIELD NAME="showblocks" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether blocks should be shown on the attempt.php and review.php pages." PREVIOUS="showuserpicture"/>
</FIELDS>
@@ -51,32 +53,34 @@
<INDEX NAME="course" UNIQUE="false" FIELDS="course"/>
</INDEXES>
</TABLE>
<TABLE NAME="quiz_attempts" COMMENT="Stores various attempts on a quiz" PREVIOUS="quiz" NEXT="quiz_grades">
<TABLE NAME="quiz_attempts" COMMENT="Stores users attempts at quizzes." PREVIOUS="quiz" NEXT="quiz_grades">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="uniqueid"/>
<FIELD NAME="uniqueid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="quiz"/>
<FIELD NAME="quiz" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="userid"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="quiz" NEXT="attempt"/>
<FIELD NAME="attempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="userid" NEXT="sumgrades"/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" PREVIOUS="attempt" NEXT="timestart"/>
<FIELD NAME="timestart" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="sumgrades" NEXT="timefinish"/>
<FIELD NAME="timefinish" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timestart" NEXT="timemodified"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timefinish" NEXT="layout"/>
<FIELD NAME="layout" TYPE="text" NOTNULL="true" SEQUENCE="false" PREVIOUS="timemodified" NEXT="currentpage"/>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Standard Moodle primary key." NEXT="quiz"/>
<FIELD NAME="quiz" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the quiz that was attempted." PREVIOUS="id" NEXT="userid"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the user whose attempt this is." PREVIOUS="quiz" NEXT="attempt"/>
<FIELD NAME="attempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Sequentially numbers this student's attempts at this quiz." PREVIOUS="userid" NEXT="uniqueid"/>
<FIELD NAME="uniqueid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the question_usage that holds the details of the the question_attempts that make up this quiz attempt." PREVIOUS="attempt" NEXT="layout"/>
<FIELD NAME="layout" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="currentpage"/>
<FIELD NAME="currentpage" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="layout" NEXT="preview"/>
<FIELD NAME="preview" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="currentpage" NEXT="needsupgradetonewqe"/>
<FIELD NAME="needsupgradetonewqe" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="preview"/>
<FIELD NAME="preview" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="currentpage" NEXT="state"/>
<FIELD NAME="state" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="inprogress" SEQUENCE="false" COMMENT="The current state of the attempts. 'inprogress', 'overdue', 'finished' or 'abandoned'." PREVIOUS="preview" NEXT="timestart"/>
<FIELD NAME="timestart" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was started." PREVIOUS="state" NEXT="timefinish"/>
<FIELD NAME="timefinish" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was submitted. 0 if the attempt has not been submitted yet." PREVIOUS="timestart" NEXT="timemodified"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timefinish" NEXT="sumgrades"/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt." PREVIOUS="timemodified" NEXT="needsupgradetonewqe"/>
<FIELD NAME="needsupgradetonewqe" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used during the upgrade from Moodle 2.0 to 2.1. This will be removed in the future." PREVIOUS="sumgrades"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="uniqueid"/>
<KEY NAME="uniqueid" TYPE="foreign-unique" FIELDS="uniqueid" REFTABLE="question_attempts" REFFIELDS="id" PREVIOUS="primary" NEXT="quiz"/>
<KEY NAME="quiz" TYPE="foreign" FIELDS="quiz" REFTABLE="quiz" REFFIELDS="id" PREVIOUS="uniqueid"/>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="quiz"/>
<KEY NAME="quiz" TYPE="foreign" FIELDS="quiz" REFTABLE="quiz" REFFIELDS="id" PREVIOUS="primary" NEXT="userid"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id" PREVIOUS="quiz" NEXT="uniqueid"/>
<KEY NAME="uniqueid" TYPE="foreign-unique" FIELDS="uniqueid" REFTABLE="question_attempts" REFFIELDS="id" PREVIOUS="userid"/>
</KEYS>
<INDEXES>
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
<INDEX NAME="quiz-userid-attempt" UNIQUE="true" FIELDS="quiz, userid, attempt"/>
</INDEXES>
</TABLE>
<TABLE NAME="quiz_grades" COMMENT="The overall grade for each user on the quiz, based on their individual and the quiz.grademethod setting." PREVIOUS="quiz_attempts" NEXT="quiz_question_instances">
<TABLE NAME="quiz_grades" COMMENT="Stores the overall grade for each user on the quiz, based on their various attempts and the quiz.grademethod setting." PREVIOUS="quiz_attempts" NEXT="quiz_question_instances">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="quiz"/>
<FIELD NAME="quiz" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references quiz.id." PREVIOUS="id" NEXT="userid"/>
+14 -8
View File
@@ -25,14 +25,20 @@
defined('MOODLE_INTERNAL') || die();
$messageproviders = array (
// Notify teacher that a student has submitted a quiz attempt
'submission' => array (
'capability' => 'mod/quiz:emailnotifysubmission'
$messageproviders = array(
// Notify teacher that a student has submitted a quiz attempt.
'submission' => array(
'capability' => 'mod/quiz:emailnotifysubmission'
),
// Confirm a student's quiz attempt
'confirmation' => array (
'capability' => 'mod/quiz:emailconfirmsubmission'
)
// Confirm a student's quiz attempt.
'confirmation' => array(
'capability' => 'mod/quiz:emailconfirmsubmission'
),
// Warning to the student that their quiz attempt is now overdue, if the quiz
// has a grace period.
'attempt_overdue' => array(
'capability' => 'mod/quiz:emailwarnoverdue'
),
);
+99
View File
@@ -94,6 +94,105 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2012030901, 'quiz');
}
if ($oldversion < 2012040200) {
// Define index quiz-userid-attempt (unique) to be dropped form quiz_attempts
$table = new xmldb_table('quiz_attempts');
$index = new xmldb_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid'));
// Conditionally launch drop index quiz-userid-attempt
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040200, 'quiz');
}
if ($oldversion < 2012040201) {
// Define key userid (foreign) to be added to quiz_attempts
$table = new xmldb_table('quiz_attempts');
$key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
// Launch add key userid
$dbman->add_key($table, $key);
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040201, 'quiz');
}
if ($oldversion < 2012040202) {
// Define index quiz-userid-attempt (unique) to be added to quiz_attempts
$table = new xmldb_table('quiz_attempts');
$index = new xmldb_index('quiz-userid-attempt', XMLDB_INDEX_UNIQUE, array('quiz', 'userid', 'attempt'));
// Conditionally launch add index quiz-userid-attempt
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040202, 'quiz');
}
if ($oldversion < 2012040203) {
// Define field state to be added to quiz_attempts
$table = new xmldb_table('quiz_attempts');
$field = new xmldb_field('state', XMLDB_TYPE_CHAR, '16', null, XMLDB_NOTNULL, null, 'inprogress', 'preview');
// Conditionally launch add field state
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040203, 'quiz');
}
if ($oldversion < 2012040204) {
// Update quiz_attempts.state for finished attempts.
$DB->set_field_select('quiz_attempts', 'state', 'finished', 'timefinish > 0');
// Other, more complex transitions (basically abandoned attempts), will
// be handled by cron later.
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040204, 'quiz');
}
if ($oldversion < 2012040205) {
// Define field overduehandling to be added to quiz
$table = new xmldb_table('quiz');
$field = new xmldb_field('overduehandling', XMLDB_TYPE_CHAR, '16', null, XMLDB_NOTNULL, null, 'autoabandon', 'timelimit');
// Conditionally launch add field overduehandling
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040205, 'quiz');
}
if ($oldversion < 2012040206) {
// Define field graceperiod to be added to quiz
$table = new xmldb_table('quiz');
$field = new xmldb_field('graceperiod', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'overduehandling');
// Conditionally launch add field graceperiod
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// quiz savepoint reached
upgrade_mod_savepoint(true, 2012040206, 'quiz');
}
return true;
}
+48 -5
View File
@@ -94,6 +94,7 @@ $string['attemptsnum'] = 'Attempts: {$a}';
$string['attemptsnumthisgroup'] = 'Attempts: {$a->total} ({$a->group} from this group)';
$string['attemptsnumyourgroups'] = 'Attempts: {$a->total} ({$a->group} from your groups)';
$string['attemptsonly'] = 'Show only students with attempts';
$string['attemptstate'] = 'State';
$string['attemptstillinprogress'] = 'Attempt still in progress';
$string['attemptsunlimited'] = 'Unlimited attempts';
$string['back'] = 'Back to preview question';
@@ -261,16 +262,27 @@ This message confirms that we have safely received your answers.
You can access this quiz at {$a->quizurl}.';
$string['emailconfirmsmall'] = 'Thank you for submitting your answers to \'{$a->quizname}\'';
$string['emailconfirmsubject'] = 'Quiz submission confirmation: {$a->quizname}';
$string['emailconfirmsubject'] = 'Submission confirmation: {$a->quizname}';
$string['emailnotifybody'] = 'Dear {$a->username},
{$a->studentname} has completed the quiz
{$a->studentname} has completed
\'{$a->quizname}\' ({$a->quizurl})
in course \'{$a->coursename}\'
You can review this attempt at {$a->quizreviewurl}.';
$string['emailnotifysmall'] = '{$a->studentname} has completed {$a->quizname}';
$string['emailnotifysubject'] = '{$a->studentname} has completed quiz {$a->quizname}';
$string['emailnotifysmall'] = '{$a->studentname} has completed {$a->quizname}. See {$a->quizreviewurl}';
$string['emailnotifysubject'] = '{$a->studentname} has completed {$a->quizname}';
$string['emailoverduebody'] = 'Dear {$a->studentname},
You started an attempt at \'{$a->quizname}\'
in course \'{$a->coursename}\', but you never submitted it.
If you would would still like to submit this attempt, please go to
{$a->attemptsummaryurl} and click the submit button.
You must do this before {$a->attemptgraceend}
otherwise you attempt will not be counted.';
$string['emailoverduesmall'] = 'You did not submit your attempt at {$a->quizname}. Please go to {$a->attemptsummaryurl} before {$a->attemptgraceend} if you would still like to submit.';
$string['emailoverduesubject'] = 'Attempt now overdue: {$a->quizname}';
$string['empty'] = 'Empty';
$string['enabled'] = 'Enabled';
$string['endtest'] = 'Finish attempt ...';
@@ -317,6 +329,11 @@ $string['fromfile'] = 'from file:';
$string['functiondisabledbysecuremode'] = 'That functionality is currently disabled';
$string['generalfeedback'] = 'General feedback';
$string['generalfeedback_help'] = 'General feedback is text which is shown after a question has been attempted. Unlike feedback for a specific question which depends on the response given, the same general feedback is always shown.';
$string['graceperiod'] = 'Submission grace period';
$string['graceperiod_desc'] = 'If what to do when time expires is set to \'Allow a grace period to submit, but not change any responses\', the default amount of extra time that is allowed, in seconds.';
$string['graceperiod_help'] = 'If what to do when time expires is set to \'Allow a grace period to submit, but not change any responses\', the amount of extra time that is allowed.';
$string['graceperiodmin'] = 'Last submission grace period';
$string['graceperiodmin_desc'] = 'There is a potential problem right at the end of the quiz. On the one hand, we want to let students continue working right up until the last second - with the help of the timer that automatically submits the quiz when time runs out. On the other hand, the server may then be overloaded, and take some time to get to process the responses. Therefore, we will accept responses for up to this many seconds after time expires, so they are not penalised for the server being slow. However, the student could cheat and get this many seconds to answer the quiz. You have to make a trade-off based on how much you trust the performance of your server during quizzes.';
$string['grade'] = 'Grade';
$string['gradeall'] = 'Grade all';
$string['gradeaverage'] = 'Average grade';
@@ -412,6 +429,7 @@ $string['moveselectedonpage'] = 'Move selected questions to page: {$a}';
$string['multichoice'] = 'Multiple choice';
$string['multipleanswers'] = 'Choose at least one answer.';
$string['multiplier'] = 'Multiplier';
$string['mustbesubmittedby'] = 'This attempt must be submitted by {$a}.';
$string['name'] = 'Name';
$string['navmethod'] = 'Navigation method';
$string['navmethod_free'] = 'Free';
@@ -485,6 +503,12 @@ $string['outofshort'] = '{$a->grade}/{$a->maxgrade}';
$string['overallfeedback'] = 'Overall feedback';
$string['overallfeedback_help'] = 'Overall feedback is text that is shown after a quiz has been attempted. By specifying additional grade boundaries (as a percentage or as a number), the text shown can depend on the grade obtained.';
$string['overdue'] = 'Overdue';
$string['overduehandling'] = 'When time expires';
$string['overduehandling_desc'] = 'What should happen by default if a student does not submit the quiz before time expires.';
$string['overduehandlingautosubmit'] = 'the attempt is submitted automatically';
$string['overduehandlinggraceperiod'] = 'there is a grace period in which to submit the attempt, but not answer more questions';
$string['overduehandlingautoabandon'] = 'that is it. The attempt must be submitted before time expires, or it is not counted';
$string['overduemustbesubmittedby'] = 'This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {$a}. If you not submit it by then, no marks from this attempt will be counted.';
$string['override'] = 'Override';
$string['overridedeletegroupsure'] = 'Are you sure you want to delete the override for group {$a}?';
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
@@ -495,7 +519,7 @@ $string['overrideuser'] = 'Override user';
$string['overrideusereventname'] = '{$a->quiz} - Override';
$string['page-mod-quiz-x'] = 'Any quiz module page';
$string['page-mod-quiz-edit'] = 'Edit quiz page';
$string['pagesize'] = 'Attempts shown per page:';
$string['pagesize'] = 'Page size';
$string['parent'] = 'Parent';
$string['parentcategory'] = 'Parent category';
$string['parsingquestions'] = 'Parsing questions from import file.';
@@ -555,6 +579,7 @@ $string['quizcloseson'] = 'This quiz will close at {$a}';
$string['quiz:deleteattempts'] = 'Delete quiz attempts';
$string['quiz:emailconfirmsubmission'] = 'Get a confirmation message when submitting';
$string['quiz:emailnotifysubmission'] = 'Get a notification message when an attempt is submitted';
$string['quiz:emailwarnoverdue'] = 'Get a notification message when an attempt becomes overdue and needs to be submitted.';
$string['quiz:grade'] = 'Grade quizzes manually';
$string['quiz:ignoretimelimits'] = 'Ignores time limit on quizzes';
$string['quizisclosed'] = 'This quiz is closed';
@@ -614,16 +639,26 @@ $string['replace'] = 'Replace';
$string['replacementoptions'] = 'Replacement options';
$string['report'] = 'Reports';
$string['reportanalysis'] = 'Item analysis';
$string['reportattemptsfrom'] = 'Attempts from';
$string['reportattemptsthatare'] = 'Attempts that are';
$string['reportdisplayoptions'] = 'Display options';
$string['reportfullstat'] = 'Detailed statistics';
$string['reportmulti_percent'] = 'Multi-percentages';
$string['reportmulti_q_x_student'] = 'Multi-student choices';
$string['reportmulti_resp'] = 'Individual responses';
$string['reportmustselectstate'] = 'You must select at least one state.';
$string['reportnotfound'] = 'Report not known ({$a})';
$string['reportoverview'] = 'Overview';
$string['reportregrade'] = 'Regrade attempts';
$string['reportresponses'] = 'Detailed responses';
$string['reports'] = 'Reports';
$string['reportshowonly'] = 'Show only attempts';
$string['reportsimplestat'] = 'Simple statistics';
$string['reportusersall'] = 'all users who have attempted the quiz';
$string['reportuserswith'] = 'enrolled users who have attempted the quiz';
$string['reportuserswithorwithout'] = 'enrolled users who have, or have not, attempted the quiz';
$string['reportuserswithout'] = 'enrolled users who have not attempted the quiz';
$string['reportwhattoinclude'] = 'What to include in the report';
$string['requirepassword'] = 'Require password';
$string['requirepassword_help'] = 'If a password is specified, a student must enter it in order to attempt the quiz.';
$string['requiresubnet'] = 'Require network address';
@@ -707,6 +742,7 @@ $string['showfeedback'] = 'After answering, show feedback?';
$string['showinsecurepopup'] = 'Use a \'secure\' popup window for attempts';
$string['shownoattempts'] = 'Show students with no attempts';
$string['shownoattemptsonly'] = 'Show only students with no attempts';
$string['showreport'] = 'Show report';
$string['showteacherattempts'] = 'Show teacher attempts';
$string['showuserpicture'] = 'Show the user\'s picture';
$string['showuserpicture_help'] = 'If enabled, the student\'s name and picture will be shown on-screen during the attempt, and on the review screen, making it easier to check that the student is logged in as themself in an invigilated (proctored) exam.';
@@ -729,7 +765,13 @@ $string['startagain'] = 'Start again';
$string['startattempt'] = 'Start attempt';
$string['startedon'] = 'Started on';
$string['startnewpreview'] = 'Start a new preview';
$string['stateabandoned'] = 'Never submitted';
$string['statefinished'] = 'Finished';
$string['statefinisheddetails'] = 'Submitted {$a}';
$string['stateinprogress'] = 'In progress';
$string['statenotloaded'] = 'The state for question {$a} has not been loaded from the database';
$string['stateoverdue'] = 'Overdue';
$string['stateoverduedetails'] = 'Must be submitted by {$a}';
$string['status'] = 'Status';
$string['stoponerror'] = 'Stop on error';
$string['submitallandfinish'] = 'Submit all and finish';
@@ -757,6 +799,7 @@ $string['timelimitsec'] = 'Time limit (seconds)';
$string['timestr'] = '%H:%M:%S on %d/%m/%y';
$string['timesup'] = 'Time is up!';
$string['timetaken'] = 'Time taken';
$string['timing'] = 'Timing';
$string['tofile'] = 'to file';
$string['tolerance'] = 'Tolerance';
$string['toomanyrandom'] = 'The number of random questions required is more than are still available in the category!';
+50 -11
View File
@@ -402,7 +402,6 @@ function quiz_user_outline($course, $user, $mod, $quiz) {
* Print a detailed representation of what a user has done with
* a given particular instance of this module, for user activity reports.
*
* @global object
* @param object $course
* @param object $user
* @param object $mod
@@ -411,7 +410,8 @@ function quiz_user_outline($course, $user, $mod, $quiz) {
*/
function quiz_user_complete($course, $user, $mod, $quiz) {
global $DB, $CFG, $OUTPUT;
require_once("$CFG->libdir/gradelib.php");
require_once($CFG->libdir . '/gradelib.php');
require_once($CFG->libdir . '/mod/quiz/locallib.php');
$grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
if (!empty($grades->items[0]->grades)) {
@@ -426,8 +426,8 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
array('userid' => $user->id, 'quiz' => $quiz->id), 'attempt')) {
foreach ($attempts as $attempt) {
echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
if ($attempt->timefinish == 0) {
print_string('unfinished');
if ($attempt->state != quiz_attempt::FINISHED) {
echo quiz_attempt_state_name($attempt->state);
} else {
echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
quiz_format_grade($quiz, $quiz->sumgrades);
@@ -445,6 +445,27 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
* Quiz periodic clean-up tasks.
*/
function quiz_cron() {
global $CFG;
// Since the quiz specifies $module->cron = 60, so that the subplugins can
// have frequent cron if they need it, we now need to do our own scheduling.
$quizconfig = get_config('quiz');
if (!isset($quizconfig->overduelastrun)) {
$quizconfig->overduelastrun = 0;
$quizconfig->overduedoneto = 0;
}
$timenow = time();
if ($timenow > $quizconfig->overduelastrun + 3600) {
require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
$overduehander = new mod_quiz_overdue_attempt_updater();
$processto = $timenow - $quizconfig->graceperiodmin;
$overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
set_config('overduelastrun', $timenow, 'quiz');
set_config('overduedoneto', $processto, 'quiz');
}
// Run cron for our sub-plugin types.
cron_execute_plugin_type('quiz', 'quiz reports');
@@ -463,18 +484,36 @@ function quiz_cron() {
*/
function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
global $DB;
$status_condition = array(
'all' => '',
'finished' => ' AND timefinish > 0',
'unfinished' => ' AND timefinish = 0'
);
$params = array();
switch ($status) {
case 'all':
$statuscondition = '';
break;
case 'finished':
$statuscondition = ' AND state IN (:state1, :state2)';
$params['state1'] = quiz_attempt::FINISHED;
$params['state2'] = quiz_attempt::ABANDONED;
break;
case 'unfinished':
$statuscondition = ' AND state IN (:state1, :state2)';
$params['state1'] = quiz_attempt::IN_PROGRESS;
$params['state2'] = quiz_attempt::OVERDUE;
break;
}
$previewclause = '';
if (!$includepreviews) {
$previewclause = ' AND preview = 0';
}
$params['quizid'] = $quizid;
$params['userid'] = $userid;
return $DB->get_records_select('quiz_attempts',
'quiz = ? AND userid = ?' . $previewclause . $status_condition[$status],
array($quizid, $userid), 'attempt ASC');
'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
$params, 'attempt ASC');
}
/**
+169 -16
View File
@@ -48,6 +48,14 @@ require_once($CFG->libdir . '/filelib.php');
*/
define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
/**
* @var int If there are fewer than this many seconds left when the student submits
* a page of the quiz, then do not take them to the next page of the quiz. Instead
* close the quiz immediately.
*/
define('QUIZ_MIN_TIME_TO_CONTINUE', '2');
/// Functions related to attempts /////////////////////////////////////////
/**
@@ -97,6 +105,7 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
$attempt->timestart = $timenow;
$attempt->timefinish = 0;
$attempt->timemodified = $timenow;
$attempt->state = quiz_attempt::IN_PROGRESS;
// If this is a preview, mark it as such.
if ($ispreview) {
@@ -437,8 +446,9 @@ function quiz_update_all_attempt_sumgrades($quiz) {
sumgrades = (
{$dm->sum_usage_marks_subquery('uniqueid')}
)
WHERE quiz = :quizid AND timefinish <> 0";
$DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id));
WHERE quiz = :quizid AND state = :finishedstate";
$DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id,
'finishedstate' => quiz_attempt::FINISHED));
}
/**
@@ -535,6 +545,7 @@ function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
$DB->update_record('quiz_grades', $grade);
} else {
$grade = new stdClass();
$grade->quiz = $quiz->id;
$grade->userid = $userid;
$grade->grade = $bestgrade;
@@ -605,7 +616,7 @@ function quiz_update_all_final_grades($quiz) {
return;
}
$param = array('iquizid' => $quiz->id);
$param = array('iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED);
$firstlastattemptjoin = "JOIN (
SELECT
iquiza.userid,
@@ -615,7 +626,7 @@ function quiz_update_all_final_grades($quiz) {
FROM {quiz_attempts} iquiza
WHERE
iquiza.timefinish <> 0 AND
iquiza.state = :istatefinished
iquiza.preview = 0 AND
iquiza.quiz = :iquizid
@@ -662,13 +673,15 @@ function quiz_update_all_final_grades($quiz) {
$param['quizid2'] = $quiz->id;
$param['quizid3'] = $quiz->id;
$param['quizid4'] = $quiz->id;
$param['statefinished'] = quiz_attempt::FINISHED;
$param['statefinished2'] = quiz_attempt::FINISHED;
$finalgradesubquery = "
SELECT quiza.userid, $finalgrade AS newgrade
FROM {quiz_attempts} quiza
$join
WHERE
$where
quiza.timefinish <> 0 AND
quiza.quiza.state = :statefinished AND
quiza.preview = 0 AND
quiza.quiz = :quizid3
GROUP BY quiza.userid";
@@ -684,7 +697,7 @@ function quiz_update_all_final_grades($quiz) {
SELECT DISTINCT userid
FROM {quiz_attempts} quiza2
WHERE
quiza2.timefinish <> 0 AND
quiza2.quiza.state = :statefinished2 AND
quiza2.preview = 0 AND
quiza2.quiz = :quizid2
) users
@@ -776,7 +789,8 @@ function quiz_calculate_best_attempt($quiz, $attempts) {
}
/**
* @return the options for calculating the quiz grade from the individual attempt grades.
* @return array int => lang string the options for calculating the quiz grade
* from the individual attempt grades.
*/
function quiz_get_grading_options() {
return array(
@@ -797,6 +811,37 @@ function quiz_get_grading_option_name($option) {
return $strings[$option];
}
/**
* @return array string => lang string the options for handling overdue quiz
* attempts.
*/
function quiz_get_overdue_handling_options() {
return array(
'autosubmit' => get_string('overduehandlingautosubmit', 'quiz'),
'graceperiod' => get_string('overduehandlinggraceperiod', 'quiz'),
'autoabandon' => get_string('overduehandlingautoabandon', 'quiz'),
);
}
/**
* @param string $state one of the state constants like IN_PROGRESS.
* @return string the human-readable state name.
*/
function quiz_attempt_state_name($state) {
switch ($state) {
case quiz_attempt::IN_PROGRESS:
return get_string('stateinprogress', 'quiz');
case quiz_attempt::OVERDUE:
return get_string('stateoverdue', 'quiz');
case quiz_attempt::FINISHED:
return get_string('statefinished', 'quiz');
case quiz_attempt::ABANDONED:
return get_string('stateabandoned', 'quiz');
default:
throw new coding_exception('Unknown quiz attempt state.');
}
}
/// Other quiz functions ////////////////////////////////////////////////////
/**
@@ -929,14 +974,15 @@ function quiz_get_flag_option($attempt, $context) {
}
/**
* Work out what state this quiz attempt is in.
* Work out what state this quiz attempt is in - in the sense used by
* quiz_get_review_options, not in the sense of $attempt->state.
* @param object $quiz the quiz settings
* @param object $attempt the quiz_attempt database row.
* @return int one of the mod_quiz_display_options::DURING,
* IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
*/
function quiz_attempt_state($quiz, $attempt) {
if ($attempt->timefinish == 0) {
if ($attempt->state != quiz_attempt::FINISHED) {
return mod_quiz_display_options::DURING;
} else if (time() < $attempt->timefinish + 120) {
return mod_quiz_display_options::IMMEDIATELY_AFTER;
@@ -968,7 +1014,7 @@ function quiz_get_review_options($quiz, $attempt, $context) {
}
// Show a link to the comment box only for closed attempts
if (!empty($attempt->id) && $attempt->timefinish && !$attempt->preview &&
if (!empty($attempt->id) && $attempt->state == quiz_attempt::FINISHED && !$attempt->preview &&
!is_null($context) && has_capability('mod/quiz:grade', $context)) {
$options->manualcomment = question_display_options::VISIBLE;
$options->manualcommentlink = new moodle_url('/mod/quiz/comment.php',
@@ -1234,14 +1280,14 @@ function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm
$a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id;
$a->quizreportlink = '<a href="' . $a->quizreporturl . '">' .
format_string($quiz->name) . ' report</a>';
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
$a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' .
format_string($quiz->name) . ' review</a>';
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
// Attempt info
$a->submissiontime = userdate($attempt->timefinish);
$a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
$a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' .
format_string($quiz->name) . ' review</a>';
// Student who sat the quiz info
$a->studentidnumber = $submitter->idnumber;
$a->studentname = fullname($submitter);
@@ -1267,6 +1313,80 @@ function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm
return $allok;
}
/**
* Send the notification message when a quiz attempt becomes overdue.
*
* @param object $course the course
* @param object $quiz the quiz
* @param object $attempt this attempt just finished
* @param object $context the quiz context
* @param object $cm the coursemodule for this quiz
*/
function quiz_send_overdue_message($course, $quiz, $attempt, $context, $cm) {
global $CFG, $DB;
// Do nothing if required objects not present
if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.');
}
$submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST);
if (!has_capability('mod/quiz:emailwarnoverdue', $context, $submitter, false)) {
return; // Message not required.
}
// Prepare lots of useful information that admins might want to include in
// the email message.
$quizname = format_string($quiz->name);
$deadlines = array();
if ($quiz->timelimit) {
$deadlines[] = $attempt->timestart + $quiz->timelimit;
}
if ($quiz->timeclose) {
$deadlines[] = $quiz->timeclose;
}
$duedate = min($deadlines) + $quiz->graceperiod;
$a = new stdClass();
// Course info.
$a->coursename = $course->fullname;
$a->courseshortname = $course->shortname;
// Quiz info.
$a->quizname = $quizname;
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$a->quizlink = '<a href="' . $a->quizurl . '">' . $quizname . '</a>';
// Attempt info.
$a->attemptgraceend = format_time($duedate);
$a->attemptsummaryurl = $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $attempt->id;
$a->attemptsummarylink = '<a href="' . $a->attemptsummaryurl . '">' . $quizname . ' review</a>';
// Student's info.
$a->studentidnumber = $submitter->idnumber;
$a->studentname = fullname($submitter);
$a->studentusername = $submitter->username;
// Prepare the message.
$eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->name = 'attempt_overdue';
$eventdata->notification = 1;
$eventdata->userfrom = get_admin();
$eventdata->userto = $submitter;
$eventdata->subject = get_string('emailoverduesubject', 'quiz', $a);
$eventdata->fullmessage = get_string('emailoverduebody', 'quiz', $a);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '';
$eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a);
$eventdata->contexturl = $a->quizurl;
$eventdata->contexturlname = $a->quizname;
// Send the message.
return message_send($eventdata);
}
/**
* Handle the quiz_attempt_submitted event.
*
@@ -1292,6 +1412,36 @@ function quiz_attempt_submitted_handler($event) {
get_context_instance(CONTEXT_MODULE, $cm->id), $cm);
}
/**
* Handle the quiz_attempt_overdue event.
*
* For quizzes with applicable settings, this sends a message to the user, reminding
* them that they forgot to submit, and that they have another chance to do so.
*
* @param object $event the event object.
*/
function quiz_attempt_overdue_handler($event) {
global $DB;
$course = $DB->get_record('course', array('id' => $event->courseid));
$quiz = $DB->get_record('quiz', array('id' => $event->quizid));
$cm = get_coursemodule_from_id('quiz', $event->cmid, $event->courseid);
$attempt = $DB->get_record('quiz_attempts', array('id' => $event->attemptid));
if (!($course && $quiz && $cm && $attempt)) {
// Something has been deleted since the event was raised. Therefore, the
// event is no longer relevant.
return true;
}
return quiz_send_overdue_message($course, $quiz, $attempt,
get_context_instance(CONTEXT_MODULE, $cm->id), $cm);
}
/**
* Get the information about the standard quiz JavaScript module.
* @return array a standard jsmodule structure.
*/
function quiz_get_js_module() {
global $PAGE;
@@ -1392,14 +1542,17 @@ class mod_quiz_display_options extends question_display_options {
class qubaids_for_quiz extends qubaid_join {
public function __construct($quizid, $includepreviews = true, $onlyfinished = false) {
$where = 'quiza.quiz = :quizaquiz';
$params = array('quizaquiz' => $quizid);
if (!$includepreviews) {
$where .= ' AND preview = 0';
}
if ($onlyfinished) {
$where .= ' AND timefinish <> 0';
$where .= ' AND state == :statefinished';
$params['statefinished'] = quiz_attempt::FINISHED;
}
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where,
array('quizaquiz' => $quizid));
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
}
}
+28 -8
View File
@@ -73,6 +73,9 @@ class mod_quiz_mod_form extends moodleform_mod {
// Introduction.
$this->add_intro_editor(false, get_string('introduction', 'quiz'));
//-------------------------------------------------------------------------------
$mform->addElement('header', 'timing', get_string('timing', 'quiz'));
// Open and close dates.
$mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'),
array('optional' => true, 'step' => 1));
@@ -88,6 +91,31 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->setAdvanced('timelimit', $quizconfig->timelimit_adv);
$mform->setDefault('timelimit', $quizconfig->timelimit);
// What to do with overdue attempts.
$mform->addElement('select', 'overduehandling', get_string('overduehandling', 'quiz'),
quiz_get_overdue_handling_options());
$mform->setAdvanced('overduehandling', $quizconfig->overduehandling_adv);
$mform->setDefault('overduehandling', $quizconfig->overduehandling);
// TODO Formslib does OR logic on disableif, and we need AND logic here.
// $mform->disabledIf('overduehandling', 'timelimit', 'eq', 0);
// $mform->disabledIf('overduehandling', 'timeclose', 'eq', 0);
// Grace period time.
$mform->addElement('duration', 'graceperiod', get_string('graceperiod', 'quiz'),
array('optional' => true));
$mform->addHelpButton('graceperiod', 'graceperiod', 'quiz');
$mform->setAdvanced('graceperiod', $quizconfig->graceperiod_adv);
$mform->setDefault('graceperiod', $quizconfig->graceperiod);
$mform->disabledIf('graceperiod', 'overduehandling', 'neq', 'graceperiod');
//-------------------------------------------------------------------------------
// Grade settings
$this->standard_grading_coursemodule_elements();
$mform->removeElement('grade');
$mform->addElement('hidden', 'grade', $quizconfig->maximumgrade);
$mform->setType('grade', PARAM_NUMBER);
// Number of attempts.
$attemptoptions = array('0' => get_string('unlimited'));
for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) {
@@ -106,14 +134,6 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->setDefault('grademethod', $quizconfig->grademethod);
$mform->disabledIf('grademethod', 'attempts', 'eq', 1);
//-------------------------------------------------------------------------------
// Grade settings
$this->standard_grading_coursemodule_elements();
$mform->removeElement('grade');
$mform->addElement('hidden', 'grade', $quizconfig->maximumgrade);
$mform->setType('grade', PARAM_NUMBER);
//-------------------------------------------------------------------------------
$mform->addElement('header', 'layouthdr', get_string('layout', 'quiz'));
+5 -1
View File
@@ -96,7 +96,11 @@ M.mod_quiz.timer = {
Y.one('#quiz-time-left').setContent(M.str.quiz.timesup);
var input = Y.one('input[name=timeup]');
input.set('value', 1);
input.ancestor('form').submit();
var form = input.ancestor('form');
if (form.one('input[name=finishattempt]')) {
form.one('input[name=finishattempt]').set('value', 0);
}
form.submit();
return;
}
+46 -19
View File
@@ -41,7 +41,7 @@ $attemptid = required_param('attempt', PARAM_INT);
$next = optional_param('next', false, PARAM_BOOL);
$thispage = optional_param('thispage', 0, PARAM_INT);
$nextpage = optional_param('nextpage', 0, PARAM_INT);
$finishattempt = optional_param('finishattempt', 0, PARAM_BOOL);
$finishattempt = optional_param('finishattempt', false, PARAM_BOOL);
$timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer.
$scrollpos = optional_param('scrollpos', '', PARAM_RAW);
@@ -63,9 +63,16 @@ if ($page == -1) {
}
}
// We treat automatically closed attempts just like normally closed attempts
if ($timeup) {
$finishattempt = 1;
// If there is only a very small amount of time left, there is no point trying
// to show the student another page of the quiz. Just finish now.
$accessmanager = $attemptobj->get_access_manager($timenow);
$timeleft = $accessmanager->get_time_left($attemptobj->get_attempt(), $timenow);
$toolate = false;
if ($timeleft !== false && $timeleft < QUIZ_MIN_TIME_TO_CONTINUE) {
$timeup = true;
if ($timeleft < -get_config('quiz', 'graceperiodmin')) {
$toolate = true;
}
}
// Check login.
@@ -88,30 +95,50 @@ if ($attemptobj->is_finished()) {
'attemptalreadyclosed', null, $attemptobj->review_url());
}
// If time is running out, trigger the appropriate action.
$becomingoverdue = false;
if ($timeup) {
if ($attemptobj->get_quiz()->overduehandling == 'graceperiod') {
$becomingoverdue = true;
} else {
$finishattempt = true;
}
}
// Don't log - we will end with a redirect to a page that is logged.
if (!$finishattempt) {
// Just process the responses for this page and go to the next page.
try {
$attemptobj->process_all_actions($timenow);
if (!$toolate) {
try {
$attemptobj->process_submitted_actions($timenow, $becomingoverdue);
} catch (question_out_of_sequence_exception $e) {
print_error('submissionoutofsequencefriendlymessage', 'question',
$attemptobj->attempt_url(null, $thispage));
} catch (question_out_of_sequence_exception $e) {
print_error('submissionoutofsequencefriendlymessage', 'question',
$attemptobj->attempt_url(null, $thispage));
} catch (Exception $e) {
// This sucks, if we display our own custom error message, there is no way
// to display the original stack trace.
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
} catch (Exception $e) {
// This sucks, if we display our own custom error message, there is no way
// to display the original stack trace.
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
print_error('errorprocessingresponses', 'question',
$attemptobj->attempt_url(null, $thispage), $e->getMessage(), $debuginfo);
}
print_error('errorprocessingresponses', 'question',
$attemptobj->attempt_url(null, $thispage), $e->getMessage(), $debuginfo);
} else {
// The student is too late.
$attemptobj->process_going_overdue($timenow, true);
}
$transaction->allow_commit();
redirect($nexturl);
if ($becomingoverdue) {
redirect($attemptobj->summary_url());
} else {
redirect($nexturl);
}
}
// Otherwise, we have been asked to finish attempt, so do that.
@@ -123,7 +150,7 @@ add_to_log($attemptobj->get_courseid(), 'quiz', 'close attempt',
// Update the quiz attempt record.
try {
$attemptobj->finish_attempt($timenow);
$attemptobj->process_finish($timenow, !$toolate);
} catch (question_out_of_sequence_exception $e) {
print_error('submissionoutofsequencefriendlymessage', 'question',
+83 -58
View File
@@ -112,7 +112,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
* @param int $page the current page number
* @return $summarydata containing filtered row data
*/
protected function filter_summary_table($summarydata, $page) {
protected function filter_review_summary_table($summarydata, $page) {
if ($page == 0) {
return $summarydata;
}
@@ -134,7 +134,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
* @param int $page contains the current page number
*/
public function review_summary_table($summarydata, $page) {
$summarydata = $this->filter_summary_table($summarydata, $page);
$summarydata = $this->filter_review_summary_table($summarydata, $page);
if (empty($summarydata)) {
return '';
}
@@ -261,7 +261,16 @@ class mod_quiz_renderer extends plugin_renderer_base {
* Return the HTML of the quiz timer.
* @return string HTML content.
*/
public function countdown_timer() {
public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
$timeleft = $attemptobj->get_time_left($timenow);
if ($timeleft !== false) {
// Make sure the timer starts just above zero. If $timeleft was <= 0, then
// this will just have the effect of causing the quiz to be submitted immediately.
$timerstartvalue = max($timeleft, 1);
$this->initialise_timer($timerstartvalue);
}
return html_writer::tag('div', get_string('timeleft', 'quiz') . ' ' .
html_writer::tag('span', '', array('id' => 'quiz-time-left')),
array('id' => 'quiz-timer'));
@@ -604,8 +613,6 @@ class mod_quiz_renderer extends plugin_renderer_base {
*/
public function summary_page_controls($attemptobj) {
$output = '';
// countdown timer
$output .= $this->countdown_timer();
// Return to place button
$button = new single_button(
@@ -626,11 +633,23 @@ class mod_quiz_renderer extends plugin_renderer_base {
new moodle_url($attemptobj->processattempt_url(), $options),
get_string('submitallandfinish', 'quiz'));
$button->id = 'responseform';
$button->add_action(new confirm_action(get_string('confirmclose', 'quiz'), null,
get_string('submitallandfinish', 'quiz')));
if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
$button->add_action(new confirm_action(get_string('confirmclose', 'quiz'), null,
get_string('submitallandfinish', 'quiz')));
}
$output .= $this->container($this->container($this->render($button),
'controls'), 'submitbtns mdl-align');
$duedate = $attemptobj->get_due_date();
$message = '';
if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
$message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate));
} else if ($duedate) {
$message = get_string('mustbesubmittedby', 'quiz', userdate($duedate));
}
$output .= $this->countdown_timer($attemptobj, time());
$output .= $this->container($message . $this->container(
$this->render($button), 'controls'), 'submitbtns mdl-align');
return $output;
}
@@ -856,11 +875,9 @@ class mod_quiz_renderer extends plugin_renderer_base {
* @param mod_quiz_view_object $viewobj
*/
public function view_table($quiz, $context, $viewobj) {
$output = '';
if (!$viewobj->attempts) {
return $output;
return '';
}
$output .= $this->view_table_heading();
// Prepare table header
$table = new html_table();
@@ -873,7 +890,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
$table->align[] = 'center';
$table->size[] = '';
}
$table->head[] = get_string('timecompleted', 'quiz');
$table->head[] = get_string('attemptstate', 'quiz');
$table->align[] = 'left';
$table->size[] = '';
if ($viewobj->markcolumn) {
@@ -898,79 +915,59 @@ class mod_quiz_renderer extends plugin_renderer_base {
$table->align[] = 'left';
$table->size[] = '';
}
if (isset($quiz->showtimetaken)) {
$table->head[] = get_string('timetaken', 'quiz');
$table->align[] = 'left';
$table->size[] = '';
}
// One row for each attempt
foreach ($viewobj->attempts as $attempt) {
$attemptoptions = quiz_get_review_options($quiz, $attempt, $context);
foreach ($viewobj->attemptobjs as $attemptobj) {
$attemptoptions = $attemptobj->get_display_options(true);
$row = array();
// Add the attempt number, making it a link, if appropriate.
// Add the attempt number.
if ($viewobj->attemptcolumn) {
if ($attempt->preview) {
if ($attemptobj->is_preview()) {
$row[] = get_string('preview', 'quiz');
} else {
$row[] = $attempt->attempt;
$row[] = $attemptobj->get_attempt_number();
}
}
// prepare strings for time taken and date completed
$timetaken = '';
$datecompleted = '';
if ($attempt->timefinish > 0) {
// attempt has finished
$timetaken = format_time($attempt->timefinish - $attempt->timestart);
$datecompleted = userdate($attempt->timefinish);
} else if (!$quiz->timeclose || $viewobj->timenow < $quiz->timeclose) {
// The attempt is still in progress.
$timetaken = format_time($viewobj->timenow - $attempt->timestart);
$datecompleted = get_string('inprogress', 'quiz');
} else {
$timetaken = format_time($quiz->timeclose - $attempt->timestart);
$datecompleted = userdate($quiz->timeclose);
}
$row[] = $datecompleted;
$row[] = $this->attempt_state($attemptobj);
if ($viewobj->markcolumn) {
if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
$attempt->timefinish > 0) {
$row[] = quiz_format_grade($quiz, $attempt->sumgrades);
$attemptobj->is_finished()) {
$row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks());
} else {
$row[] = '';
}
}
// Ouside the if because we may be showing feedback but not grades.
$attemptgrade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
$attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
if ($viewobj->gradecolumn) {
if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
$attempt->timefinish > 0) {
$formattedgrade = quiz_format_grade($quiz, $attemptgrade);
// highlight the highest grade if appropriate
if ($viewobj->overallstats && !$attempt->preview
$attemptobj->is_finished()) {
// Highlight the highest grade if appropriate
if ($viewobj->overallstats && !$attemptobj->is_preview()
&& $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
&& $attemptgrade == $viewobj->mygrade
&& $quiz->grademethod == QUIZ_GRADEHIGHEST) {
$table->rowclasses[$attempt->attempt] = 'bestrow';
$table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow';
}
$row[] = $formattedgrade;
$row[] = quiz_format_grade($quiz, $attemptgrade);
} else {
$row[] = '';
}
}
if ($viewobj->canreviewmine) {
$row[] = $viewobj->accessmanager->make_review_link($attempt,
$row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(),
$attemptoptions, $this);
}
if ($viewobj->feedbackcolumn && $attempt->timefinish > 0) {
if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) {
if ($attemptoptions->overallfeedback) {
$row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context);
} else {
@@ -978,21 +975,47 @@ class mod_quiz_renderer extends plugin_renderer_base {
}
}
if (isset($quiz->showtimetaken)) {
$row[] = $timetaken;
}
if ($attempt->preview) {
if ($attemptobj->is_preview()) {
$table->data['preview'] = $row;
} else {
$table->data[$attempt->attempt] = $row;
$table->data[$attemptobj->get_attempt_number()] = $row;
}
} // End of loop over attempts.
$output .= html_writer::table($table);
$output = '';
$output .= $this->view_table_heading();
$output .= html_writer::table($table);
return $output;
}
/**
* Generate a brief textual desciption of the current state of an attempt.
* @param quiz_attempt $attemptobj the attempt
* @param int $timenow the time to use as 'now'.
* @return string the appropriate lang string to describe the state.
*/
public function attempt_state($attemptobj) {
switch ($attemptobj->get_state()) {
case quiz_attempt::IN_PROGRESS:
return get_string('stateinprogress', 'quiz');
case quiz_attempt::OVERDUE:
return get_string('stateoverdue', 'quiz') . html_writer::tag('span',
get_string('stateoverduedetails', 'quiz',
userdate($attemptobj->get_due_date())),
array('class' => 'statedetails'));
case quiz_attempt::FINISHED:
return get_string('statefinished', 'quiz') . html_writer::tag('span',
get_string('statefinisheddetails', 'quiz',
userdate($attemptobj->get_submitted_date())),
array('class' => 'statedetails'));
case quiz_attempt::ABANDONED:
return get_string('stateabandoned', 'quiz');
}
}
/**
* Prints the students best score
*
@@ -1134,8 +1157,10 @@ class mod_quiz_links_to_other_attempts implements renderable {
class mod_quiz_view_object {
/** @var array $infomessages of messages with information to display about the quiz. */
public $infomessages;
/** @var array $attempt contains all the user's attempts at this quiz. */
/** @var array $attempts contains all the user's attempts at this quiz. */
public $attempts;
/** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */
public $attemptobjs;
/** @var quiz_access_manager $accessmanager contains various access rules. */
public $accessmanager;
/** @var bool $canreviewmine whether the current user has the capability to
+18 -8
View File
@@ -39,14 +39,14 @@ abstract class quiz_attempts_report extends quiz_default_report {
/** @var int default page size for reports. */
const DEFAULT_PAGE_SIZE = 30;
/** @var int include all attempts. */
const ALL_ATTEMPTS = 0;
/** @var int include just enroled users who have not attempted the quiz. */
const STUDENTS_WITH_NO = 1;
/** @var int include just enroled users who have attempted the quiz. */
const STUDENTS_WITH = 2;
/** @var int include all enroled users. */
const ALL_STUDENTS = 3;
/** @var string constant used for the options, means all users with attempts. */
const ALL_WITH = 'all_with';
/** @var string constant used for the options, means only enrolled users with attempts. */
const ENROLLED_WITH = 'enrolled_with';
/** @var string constant used for the options, means only enrolled users without attempts. */
const ENROLLED_WITHOUT = 'enrolled_without';
/** @var string constant used for the options, means all enrolled users. */
const ENROLLED_ALL = 'enrolled_any';
/** @var string the mode this report is. */
protected $mode;
@@ -199,6 +199,16 @@ abstract class quiz_attempts_report extends quiz_default_report {
$table->column_class('fullname', 'bold');
}
/**
* Add the state column to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_state_column(&$columns, &$headers) {
$columns[] = 'state';
$headers[] = get_string('attemptstate', 'quiz');
}
/**
* Add all the time-related columns to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
+67 -51
View File
@@ -40,65 +40,81 @@ abstract class mod_quiz_attempts_report_form extends moodleform {
$mform = $this->_form;
$mform->addElement('header', 'preferencespage',
get_string('preferencespage', 'quiz_overview'));
get_string('reportwhattoinclude', 'quiz'));
if (!$this->_customdata['currentgroup']) {
$studentsstring = get_string('participants');
} else {
$a = new stdClass();
$a->coursestudent = get_string('participants');
$a->groupname = groups_get_group_name($this->_customdata['currentgroup']);
if (20 < strlen($a->groupname)) {
$studentsstring = get_string('studentingrouplong', 'quiz_overview', $a);
} else {
$studentsstring = get_string('studentingroup', 'quiz_overview', $a);
}
}
$options = array();
if (!$this->_customdata['currentgroup']) {
$options[quiz_attempts_report::ALL_ATTEMPTS] = get_string('optallattempts', 'quiz_overview');
}
if ($this->_customdata['currentgroup'] ||
!is_inside_frontpage($this->_customdata['context'])) {
$options[quiz_attempts_report::ALL_STUDENTS] =
get_string('optallstudents', 'quiz_overview', $studentsstring);
$options[quiz_attempts_report::STUDENTS_WITH] =
get_string('optattemptsonly', 'quiz_overview', $studentsstring);
$options[quiz_attempts_report::STUDENTS_WITH_NO] =
get_string('optnoattemptsonly', 'quiz_overview', $studentsstring);
}
$mform->addElement('select', 'attemptsmode',
get_string('show', 'quiz_overview'), $options);
$this->definition_inner($mform);
$this->standard_attempt_fields($mform);
$this->other_attempt_fields($mform);
$mform->addElement('header', 'preferencesuser',
get_string('preferencesuser', 'quiz_overview'));
get_string('reportdisplayoptions', 'quiz'));
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz_overview'));
$mform->setType('pagesize', PARAM_INT);
$this->standard_preference_fields($mform);
$this->other_preference_fields($mform);
$mform->addElement('submit', 'submitbutton',
get_string('preferencessave', 'quiz_overview'));
get_string('showreport', 'quiz'));
}
/**
* Add any report-specific options to the form.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected abstract function definition_inner(MoodleQuickForm $mform);
protected function standard_attempt_fields(MoodleQuickForm $mform) {
/**
* Create the standard checkbox for the 'include highest graded only' option.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected function create_qmfilter_checkbox(MoodleQuickForm $mform) {
$gm = html_writer::tag('span', quiz_get_grading_option_name(
$this->_customdata['quiz']->grademethod), array('class' => 'highlight'));
return $mform->createElement('advcheckbox', 'qmfilter',
get_string('showattempts', 'quiz_overview'),
get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0, 1));
$mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), array(
quiz_attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'),
quiz_attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'),
quiz_attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'),
quiz_attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'),
));
$stategroup = array(
$mform->createElement('advcheckbox', 'stateinprogress', '',
get_string('stateinprogress', 'quiz')),
$mform->createElement('advcheckbox', 'stateoverdue', '',
get_string('stateoverdue', 'quiz')),
$mform->createElement('advcheckbox', 'statefinished', '',
get_string('statefinished', 'quiz')),
$mform->createElement('advcheckbox', 'stateabandoned', '',
get_string('stateabandoned', 'quiz')),
);
$mform->addGroup($stategroup, 'stateoptions',
get_string('reportattemptsthatare', 'quiz'), array(' '), false);
$mform->setDefault('stateinprogress', 1);
$mform->setDefault('stateoverdue', 1);
$mform->setDefault('statefinished', 1);
$mform->setDefault('stateabandoned', 1);
$mform->disabledIf('stateinprogress', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateoverdue', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('statefinished', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateabandoned', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
if (quiz_report_can_filter_only_graded($this->_customdata['quiz'])) {
$gm = html_writer::tag('span',
quiz_get_grading_option_name($this->_customdata['quiz']->grademethod),
array('class' => 'highlight'));
$mform->addElement('advcheckbox', 'onlygraded', get_string('reportshowonly', 'quiz'),
get_string('optonlygradedattempts', 'quiz_overview', $gm));
$mform->disabledIf('onlygraded', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('onlygraded', 'statefinished', 'notchecked');
}
}
protected function other_attempt_fields(MoodleQuickForm $mform) {
}
protected function standard_preference_fields(MoodleQuickForm $mform) {
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz'));
$mform->setType('pagesize', PARAM_INT);
}
protected function other_preference_fields(MoodleQuickForm $mform) {
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != quiz_attempts_report::ENROLLED_WITHOUT && !(
$data['stateinprogress'] || $data['stateoverdue'] || $data['statefinished'] || $data['stateabandoned'])) {
$errors['stateoptions'] = get_string('reportmustselectstate', 'quiz');
}
return $errors;
}
}
+71 -22
View File
@@ -48,12 +48,32 @@ class mod_quiz_attempts_report_options {
/** @var object the course settings for the course the quiz is in. */
public $course;
/** @var string quiz_attempts_report:: constants. */
public $attempts = quiz_attempts_report::STUDENTS_WITH;
/**
* @var array form field name => corresponding quiz_attempt:: state constant.
*/
protected static $statefields = array(
'stateinprogress' => quiz_attempt::IN_PROGRESS,
'stateoverdue' => quiz_attempt::OVERDUE,
'statefinished' => quiz_attempt::FINISHED,
'stateabandoned' => quiz_attempt::ABANDONED,
);
/**
* @var string quiz_attempts_report::ALL_WITH or quiz_attempts_report::ENROLLED_WITH
* quiz_attempts_report::ENROLLED_WITHOUT or quiz_attempts_report::ENROLLED_ALL
*/
public $attempts = quiz_attempts_report::ENROLLED_WITH;
/** @var int the currently selected group. 0 if no group is selected. */
public $group = 0;
/**
* @var array|null of quiz_attempt::IN_PROGRESS, etc. constants. null means
* no restriction.
*/
public $states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE,
quiz_attempt::FINISHED, quiz_attempt::ABANDONED);
/**
* @var bool whether to show all finished attmepts, or just the one that gave
* the final grade for the user.
@@ -94,10 +114,10 @@ class mod_quiz_attempts_report_options {
*/
protected function get_url_params() {
return array(
'id' => $this->cm->id,
'mode' => $this->mode,
'attemptsmode' => $this->attempts,
'qmfilter' => $this->onlygraded,
'id' => $this->cm->id,
'mode' => $this->mode,
'attempts' => $this->attempts,
'onlygraded' => $this->onlygraded,
);
}
@@ -136,9 +156,9 @@ class mod_quiz_attempts_report_options {
*/
public function get_initial_form_data() {
$toform = new stdClass();
$toform->attemptsmode = $this->attempts;
$toform->qmfilter = $this->onlygraded;
$toform->pagesize = $this->pagesize;
$toform->attempts = $this->attempts;
$toform->onlygraded = $this->onlygraded;
$toform->pagesize = $this->pagesize;
return $toform;
}
@@ -148,21 +168,31 @@ class mod_quiz_attempts_report_options {
* @param object $fromform The data from $mform->get_data() from the settings form.
*/
public function setup_from_form_data($fromform) {
$this->attempts = $fromform->attemptsmode;
$this->attempts = $fromform->attempts;
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = !empty($fromform->qmfilter);
$this->onlygraded = !empty($fromform->onlygraded);
$this->pagesize = $fromform->pagesize;
$this->states = array();
foreach (self::$statefields as $field => $state) {
if (!empty($fromform->$field)) {
$this->states[] = $state;
}
}
}
/**
* Set the fields of this object from the user's preferences.
*/
public function setup_from_params() {
$this->attempts = optional_param('attemptsmode', $this->attempts, PARAM_ALPHAEXT);
$this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT);
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = optional_param('qmfilter', $this->onlygraded, PARAM_BOOL);
$this->onlygraded = optional_param('onlygraded', $this->onlygraded, PARAM_BOOL);
$this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT);
$this->states = explode('-', optional_param('states',
implode('-', $this->states), PARAM_ALPHAEXT));
$this->download = optional_param('download', $this->download, PARAM_ALPHA);
}
@@ -188,30 +218,49 @@ class mod_quiz_attempts_report_options {
public function resolve_dependencies() {
if ($this->group) {
// Default for when a group is selected.
if ($this->attempts === null || $this->attempts == quiz_attempts_report::ALL_ATTEMPTS) {
$this->attempts = quiz_attempts_report::STUDENTS_WITH;
if ($this->attempts === null || $this->attempts == quiz_attempts_report::ALL_WITH) {
$this->attempts = quiz_attempts_report::ENROLLED_WITH;
}
} else if (!$this->group && $this->course->id == SITEID) {
// Force report on front page to show all, unless a group is selected.
$this->attempts = quiz_attempts_report::ALL_ATTEMPTS;
$this->attempts = quiz_attempts_report::ALL_WITH;
} else if ($this->attempts === null) {
$this->attempts = quiz_attempts_report::ALL_ATTEMPTS;
} else if (!in_array($this->attempts, array(quiz_attempts_report::ALL_WITH, quiz_attempts_report::ENROLLED_WITH,
quiz_attempts_report::ENROLLED_WITHOUT, quiz_attempts_report::ENROLLED_ALL))) {
$this->attempts = quiz_attempts_report::ENROLLED_WITH;
}
if ($this->pagesize < 1) {
$this->pagesize = quiz_attempts_report::DEFAULT_PAGE_SIZE;
$cleanstates = array();
foreach (self::$statefields as $state) {
if (in_array($state, $this->states)) {
$cleanstates[] = $state;
}
}
$this->states = $cleanstates;
if (count($this->states) == count(self::$statefields)) {
// If all states have been selected, then there is no constraint
// required in the SQL, so clear the array.
$this->states = null;
}
if (!quiz_report_qm_filter_select($this->quiz)) {
if (!quiz_report_can_filter_only_graded($this->quiz)) {
// A grading mode like 'average' has been selected, so we cannot do
// the show the attempt that gave the final grade thing.
$this->onlygraded = false;
}
if ($this->attempts == quiz_attempts_report::STUDENTS_WITH_NO) {
if ($this->attempts == quiz_attempts_report::ENROLLED_WITHOUT) {
$this->states = null;
$this->onlygraded = false;
}
if ($this->onlygraded) {
$this->states = array(quiz_attempt::FINISHED);
}
if ($this->pagesize < 1) {
$this->pagesize = quiz_attempts_report::DEFAULT_PAGE_SIZE;
}
}
}
+26 -19
View File
@@ -61,12 +61,6 @@ abstract class quiz_attempts_report_table extends table_sql {
/** @var object mod_quiz_attempts_report_options the options affecting this report. */
protected $options;
/** @var bool whether to only display the first/best/last attempt for each student. */
protected $qmfilter;
/** @var int which attempts/students to include in the report.. */
protected $attemptsmode;
/** @var object the ids of the students in the currently selected group, if applicable. */
protected $groupstudents;
@@ -98,8 +92,6 @@ abstract class quiz_attempts_report_table extends table_sql {
$this->quiz = $quiz;
$this->context = $context;
$this->qmsubselect = $qmsubselect;
$this->qmfilter = $options->onlygraded;
$this->attemptsmode = $options->attempts;
$this->groupstudents = $groupstudents;
$this->students = $students;
$this->questions = $questions;
@@ -154,6 +146,15 @@ abstract class quiz_attempts_report_table extends table_sql {
get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'));
}
/**
* Generate the display of the attempt state column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_state($attempt) {
return quiz_attempt::state_name($attempt->state);
}
/**
* Generate the display of the start time column.
* @param object $attempt the table row being output.
@@ -201,7 +202,7 @@ abstract class quiz_attempts_report_table extends table_sql {
* @return string HTML content to go inside the td.
*/
public function col_feedbacktext($attempt) {
if (!$attempt->timefinish) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
@@ -363,16 +364,14 @@ abstract class quiz_attempts_report_table extends table_sql {
u.institution,
u.department,
u.email' . $extrafields . ',
quiza.state,
quiza.sumgrades,
quiza.timefinish,
quiza.timestart,
CASE WHEN quiza.timefinish = 0 THEN null
WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
ELSE 0 END AS duration';
// To explain that last bit, in MySQL, qa.timestart and qa.timefinish
// are unsigned. Since MySQL 5.5.5, when they introduced strict mode,
// subtracting a larger unsigned int from a smaller one gave an error.
// Therefore, we avoid doing that. timefinish can be non-zero and less
// To explain that last bit, timefinish can be non-zero and less
// than timestart when you have two load-balanced servers with very
// badly synchronised clocks, and a student does a really quick attempt.
@@ -382,29 +381,30 @@ abstract class quiz_attempts_report_table extends table_sql {
quiza.userid = u.id AND quiza.quiz = :quizid";
$params = array('quizid' => $this->quiz->id);
if ($this->qmsubselect && $this->qmfilter) {
if ($this->qmsubselect && $this->options->onlygraded) {
$from .= " AND $this->qmsubselect";
}
switch ($this->attemptsmode) {
case quiz_attempts_report::ALL_ATTEMPTS:
switch ($this->options->attempts) {
case quiz_attempts_report::ALL_WITH:
// Show all attempts, including students who are no longer in the course.
$where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
break;
case quiz_attempts_report::STUDENTS_WITH:
case quiz_attempts_report::ENROLLED_WITH:
// Show only students with attempts.
list($usql, $uparams) = $DB->get_in_or_equal(
$reportstudents, SQL_PARAMS_NAMED, 'u');
$params += $uparams;
$where = "u.id $usql AND quiza.preview = 0 AND quiza.id IS NOT NULL";
break;
case quiz_attempts_report::STUDENTS_WITH_NO:
case quiz_attempts_report::ENROLLED_WITHOUT:
// Show only students without attempts.
list($usql, $uparams) = $DB->get_in_or_equal(
$reportstudents, SQL_PARAMS_NAMED, 'u');
$params += $uparams;
$where = "u.id $usql AND quiza.id IS NULL";
break;
case quiz_attempts_report::ALL_STUDENTS:
case quiz_attempts_report::ENROLLED_ALL:
// Show all students with or without attempts.
list($usql, $uparams) = $DB->get_in_or_equal(
$reportstudents, SQL_PARAMS_NAMED, 'u');
@@ -413,6 +413,13 @@ abstract class quiz_attempts_report_table extends table_sql {
break;
}
if ($this->options->states) {
list($statesql, $stateparams) = $DB->get_in_or_equal($this->options->states,
SQL_PARAMS_NAMED, 'state');
$params += $stateparams;
$where .= " AND (quiza.state $statesql OR quiza.state IS NULL)";
}
return array($fields, $from, $where, $params);
}
+5 -4
View File
@@ -162,8 +162,8 @@ class quiz_grading_report extends quiz_default_report {
$where = "quiza.quiz = :mangrquizid AND
quiza.preview = 0 AND
quiza.timefinish <> 0";
$params = array('mangrquizid' => $this->cm->instance);
quiza.state = :statefinished";
$params = array('mangrquizid' => $this->cm->instance, 'statefinished' => quiz_attempt::FINISHED);
$currentgroup = groups_get_activity_group($this->cm, true);
if ($currentgroup) {
@@ -187,13 +187,14 @@ class quiz_grading_report extends quiz_default_report {
global $DB;
list($asql, $params) = $DB->get_in_or_equal($qubaids);
$params[] = quiz_attempt::FINISHED;
$params[] = $this->quiz->id;
$attemptsbyid = $DB->get_records_sql("
SELECT quiza.*, u.firstname, u.lastname, u.idnumber
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
WHERE quiza.uniqueid $asql AND quiza.timefinish <> 0 AND quiza.quiz = ?",
WHERE quiza.uniqueid $asql AND quiza.state == ? AND quiza.quiz = ?",
$params);
$attempts = array();
@@ -489,7 +490,7 @@ class quiz_grading_report extends quiz_default_report {
$attempt = $attempts[$qubaid];
$quba = question_engine::load_questions_usage_by_activity($qubaid);
$attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
$attemptobj->process_all_actions(time());
$attemptobj->process_submitted_actions(time());
}
$transaction->allow_commit();
}
@@ -65,7 +65,7 @@ $string['regradeheader'] = 'Regrading';
$string['regradeselected'] = 'Regrade selected attempts';
$string['show'] = 'Show / download';
$string['showattempts'] = 'Only show / download attempts';
$string['showdetailedmarks'] = 'Show / download marks for each question';
$string['showdetailedmarks'] = 'Marks for each question';
$string['showinggraded'] = 'Showing only the attempt graded for each user.';
$string['showinggradedandungraded'] = 'Showing graded and ungraded attempts for each user. The one attempt for each user that is graded is highlighted. The grading method for this quiz is {$a}.';
$string['studentingroup'] = '\'{$a->coursestudent}\' in group \'{$a->groupname}\'';
+15 -16
View File
@@ -35,22 +35,21 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_form.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_settings_form extends mod_quiz_attempts_report_form {
protected function definition_inner(MoodleQuickForm $mform) {
$showattemptsgrp = array();
if ($this->_customdata['qmsubselect']) {
$showattemptsgrp[] = $this->create_qmfilter_checkbox($mform);
}
if (has_capability('mod/quiz:regrade', $this->_customdata['context'])) {
$showattemptsgrp[] = $mform->createElement('advcheckbox', 'regradefilter',
get_string('showattempts', 'quiz_overview'),
get_string('optonlyregradedattempts', 'quiz_overview'), null, array(0, 1));
}
if ($showattemptsgrp) {
$mform->addGroup($showattemptsgrp, null,
get_string('showattempts', 'quiz_overview'), '<br />', false);
}
$mform->addElement('selectyesno', 'detailedmarks',
get_string('showdetailedmarks', 'quiz_overview'));
protected function other_attempt_fields(MoodleQuickForm $mform) {
if (has_capability('mod/quiz:regrade', $this->_customdata['context'])) {
$mform->addElement('advcheckbox', 'onlyregraded', '',
get_string('optonlyregradedattempts', 'quiz_overview'));
$mform->disabledIf('onlyregraded', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
}
}
protected function other_preference_fields(MoodleQuickForm $mform) {
if (quiz_has_grades($this->_customdata['quiz'])) {
$mform->addElement('selectyesno', 'slotmarks',
get_string('showdetailedmarks', 'quiz_overview'));
} else {
$mform->addElement('hidden', 'slotmarks', 0);
}
}
}
+12 -10
View File
@@ -44,15 +44,15 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
protected function get_url_params() {
$params = parent::get_url_params();
$params['regradefilter'] = $this->onlyregraded;
$params['detailedmarks'] = $this->slotmarks;
$params['onlyregraded'] = $this->onlyregraded;
$params['slotmarks'] = $this->slotmarks;
return $params;
}
public function get_initial_form_data() {
$toform = parent::get_initial_form_data();
$toform->regradefilter = $this->onlyregraded;
$toform->detailedmarks = $this->slotmarks;
$toform->onlyregraded = $this->onlyregraded;
$toform->slotmarks = $this->slotmarks;
return $toform;
}
@@ -60,15 +60,15 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
public function setup_from_form_data($fromform) {
parent::setup_from_form_data($fromform);
$this->onlyregraded = !empty($fromform->regradefilter);
$this->slotmarks = $fromform->detailedmarks;
$this->onlyregraded = !empty($fromform->onlyregraded);
$this->slotmarks = $fromform->slotmarks;
}
public function setup_from_params() {
parent::setup_from_params();
$this->onlyregraded = optional_param('regradefilter', $this->onlyregraded, PARAM_BOOL);
$this->slotmarks = optional_param('detailedmarks', $this->slotmarks, PARAM_BOOL);
$this->onlyregraded = optional_param('onlyregraded', $this->onlyregraded, PARAM_BOOL);
$this->slotmarks = optional_param('slotmarks', $this->slotmarks, PARAM_BOOL);
}
public function setup_from_user_preferences() {
@@ -80,7 +80,9 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
public function update_user_preferences() {
parent::update_user_preferences();
set_user_preference('quiz_overview_slotmarks', $this->slotmarks);
if (quiz_has_grades($this->quiz)) {
set_user_preference('quiz_overview_slotmarks', $this->slotmarks);
}
}
public function resolve_dependencies() {
@@ -94,6 +96,6 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
// if the user has permissions and if the report mode is showing attempts.
$this->checkboxcolumn = has_any_capability(
array('mod/quiz:regrade', 'mod/quiz:deleteattempts'), context_module::instance($this->cm->id))
&& ($this->attempts != quiz_attempts_report::STUDENTS_WITH_NO);
&& ($this->attempts != quiz_attempts_report::ENROLLED_WITHOUT);
}
}
+4 -5
View File
@@ -53,7 +53,6 @@ class quiz_overview_table extends quiz_attempts_report_table {
quiz_overview_options $options, $groupstudents, $students, $questions, $reporturl) {
parent::__construct('mod-quiz-report-overview-report', $quiz , $context,
$qmsubselect, $options, $groupstudents, $students, $questions, $reporturl);
$this->detailedmarks = $options->slotmarks;
}
public function build_table() {
@@ -105,7 +104,7 @@ class quiz_overview_table extends quiz_attempts_report_table {
$record->grade, $this->quiz->id, $this->context))
);
if ($this->detailedmarks) {
if ($this->options->slotmarks) {
$dm = new question_engine_data_mapper();
$qubaids = new qubaid_join($from, 'quiza.uniqueid', $where, $params);
$avggradebyq = $dm->load_average_marks($qubaids, array_keys($this->questions));
@@ -181,7 +180,7 @@ class quiz_overview_table extends quiz_attempts_report_table {
}
public function col_sumgrades($attempt) {
if (!$attempt->timefinish) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
@@ -281,7 +280,7 @@ class quiz_overview_table extends quiz_attempts_report_table {
}
protected function requires_latest_steps_loaded() {
return $this->detailedmarks;
return $this->options->slotmarks;
}
protected function is_latest_step_column($column) {
@@ -298,7 +297,7 @@ class quiz_overview_table extends quiz_attempts_report_table {
public function query_db($pagesize, $useinitialsbar = true) {
parent::query_db($pagesize, $useinitialsbar);
if ($this->detailedmarks && has_capability('mod/quiz:regrade', $this->context)) {
if ($this->options->slotmarks && has_capability('mod/quiz:regrade', $this->context)) {
$this->regradedqs = $this->get_regraded_questions();
}
}
+4 -4
View File
@@ -55,7 +55,7 @@ class quiz_overview_report extends quiz_attempts_report {
$this->form->set_data($options->get_initial_form_data());
if ($options->attempts == self::ALL_ATTEMPTS) {
if ($options->attempts == self::ALL_WITH) {
// This option is only available to users who can access all groups in
// groups mode, so setting allowed to empty (which means all quiz attempts
// are accessible, is not a security porblem.
@@ -116,7 +116,7 @@ class quiz_overview_report extends quiz_attempts_report {
}
$hasstudents = $students && (!$currentgroup || $groupstudents);
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_ATTEMPTS)) {
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
// Construct the SQL.
$fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
' AS uniqueid, ';
@@ -204,7 +204,7 @@ class quiz_overview_report extends quiz_attempts_report {
}
$this->add_user_columns($table, $columns, $headers);
$this->add_state_column($columns, $headers);
$this->add_time_columns($columns, $headers);
if ($options->slotmarks) {
@@ -321,7 +321,7 @@ class quiz_overview_report extends quiz_attempts_report {
$slots = $quba->get_slots();
}
$finished = $attempt->timefinish > 0;
$finished = $attempt->state == quiz_attempt::FINISHED;
foreach ($slots as $slot) {
$qqr = new stdClass();
$qqr->oldfraction = $quba->get_question_fraction($slot);
+9
View File
@@ -127,6 +127,15 @@ WHERE
return $qsbyslot;
}
/**
* @param object $quiz the quiz settings.
* @return bool whether, for this quiz, it is possible to filter attempts to show
* only those that gave the final grade.
*/
function quiz_report_can_filter_only_graded($quiz) {
return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE;
}
/**
* Given the quiz grading method return sub select sql to find the id of the
* one attempt that will be graded for each user. Or return
@@ -23,10 +23,13 @@
*/
$string['cannotloadoptions'] = 'Could not load question options';
$string['include'] = 'Include';
$string['include'] = 'Include the';
$string['mustselectcols'] = 'You must include something.';
$string['pagesize'] = 'Page size';
$string['pluginname'] = 'Responses';
$string['questiontext'] = 'question text';
$string['reportresponses'] = 'Responses';
$string['response'] = 'response';
$string['responses'] = 'Responses';
$string['responsesdownload'] = 'Responses download';
$string['responsesfilename'] = 'responses';
@@ -34,8 +37,10 @@ $string['responsesoptions'] = 'Responses options';
$string['responsesreport'] = 'Responses report';
$string['responsestitle'] = 'Responses';
$string['responsex'] = 'Response {$a}';
$string['rightanswer'] = 'right answer';
$string['rightanswerx'] = 'Right answer {$a}';
$string['showattempts'] = 'Only show / download attempts';
$string['showthe'] = 'Show the';
$string['summaryofquestiontext'] = 'Summary of the question';
$string['summaryofresponse'] = 'Summary of the response given';
$string['summaryofrightanswer'] = 'Summary of the right answer';
+3 -2
View File
@@ -63,7 +63,7 @@ class quiz_responses_report extends quiz_attempts_report {
$this->form->set_data($options->get_initial_form_data());
if ($options->attempts == self::ALL_ATTEMPTS) {
if ($options->attempts == self::ALL_WITH) {
// This option is only available to users who can access all groups in
// groups mode, so setting allowed to empty (which means all quiz attempts
// are accessible, is not a security porblem.
@@ -124,7 +124,7 @@ class quiz_responses_report extends quiz_attempts_report {
}
$hasstudents = $students && (!$currentgroup || $groupstudents);
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_ATTEMPTS)) {
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
list($fields, $from, $where, $params) = $table->base_sql($allowed);
@@ -150,6 +150,7 @@ class quiz_responses_report extends quiz_attempts_report {
}
$this->add_user_columns($table, $columns, $headers);
$this->add_state_column($columns, $headers);
if ($table->is_downloading()) {
$this->add_time_columns($columns, $headers);
+22 -12
View File
@@ -35,19 +35,29 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_form.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_settings_form extends mod_quiz_attempts_report_form {
protected function definition_inner(MoodleQuickForm $mform) {
if ($this->_customdata['qmsubselect']) {
$mform->addElement($this->create_qmfilter_checkbox($mform));
protected function other_preference_fields(MoodleQuickForm $mform) {
$mform->addGroup(array(
$mform->createElement('advcheckbox', 'qtext', '',
get_string('questiontext', 'quiz_responses')),
$mform->createElement('advcheckbox', 'resp', '',
get_string('response', 'quiz_responses')),
$mform->createElement('advcheckbox', 'right', '',
get_string('rightanswer', 'quiz_responses')),
), 'coloptions', get_string('showthe', 'quiz_responses'), array(' '), false);
$mform->disabledIf('qtext', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('resp', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('right', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != quiz_attempts_report::ENROLLED_WITHOUT && !(
$data['qtext'] || $data['resp'] || $data['right'])) {
$errors['coloptions'] = get_string('reportmustselectstate', 'quiz');
}
$colsgroup = array();
$colsgroup[] = $mform->createElement('advcheckbox', 'qtext', '',
get_string('summaryofquestiontext', 'quiz_responses'));
$colsgroup[] = $mform->createElement('advcheckbox', 'resp', '',
get_string('summaryofresponse', 'quiz_responses'));
$colsgroup[] = $mform->createElement('advcheckbox', 'right', '',
get_string('summaryofrightanswer', 'quiz_responses'));
$mform->addGroup($colsgroup, null,
get_string('include', 'quiz_responses'), '<br />', false);
return $errors;
}
}
@@ -105,6 +105,6 @@ class quiz_responses_options extends mod_quiz_attempts_report_options {
// We only want to show the checkbox to delete attempts
// if the user has permissions and if the report mode is showing attempts.
$this->checkboxcolumn = has_capability('mod/quiz:deleteattempts', context_module::instance($this->cm->id))
&& ($this->attempts != quiz_attempts_report::STUDENTS_WITH_NO);
&& ($this->attempts != quiz_attempts_report::ENROLLED_WITHOUT);
}
}
@@ -63,7 +63,7 @@ class quiz_responses_table extends quiz_attempts_report_table {
}
public function col_sumgrades($attempt) {
if (!$attempt->timefinish) {
if ($attempt->state == quiz_attempt::FINISHED) {
return '-';
}
+2 -2
View File
@@ -1076,8 +1076,8 @@ function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
$fromqa = '{quiz_attempts} quiza ';
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.timefinish <> 0';
$qaparams = array('quizid' => $quizid);
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
$qaparams = array('quizid' => $quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
if (!empty($currentgroup) && $groupstudents) {
list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
@@ -182,6 +182,7 @@ class quiz_statistics_response_analyser {
}
foreach ($rows as $row) {
$this->responses[$row->subqid][$row->aid][$row->response] = new stdClass();
$this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
$this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
}
+12 -7
View File
@@ -20,10 +20,9 @@
* It is used either by the student whose attempts this is, after the attempt,
* or by a teacher reviewing another's attempt during or afterwards.
*
* @package mod
* @subpackage quiz
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -112,7 +111,7 @@ $attempt = $attemptobj->get_attempt();
$quiz = $attemptobj->get_quiz();
$overtime = 0;
if ($attempt->timefinish) {
if ($attempt->state == quiz_attempt::FINISHED) {
if ($timetaken = ($attempt->timefinish - $attempt->timestart)) {
if ($quiz->timelimit && $timetaken > ($quiz->timelimit + 60)) {
$overtime = $timetaken - $quiz->timelimit;
@@ -140,6 +139,7 @@ if (!$attemptobj->get_quiz()->showuserpicture && $attemptobj->get_userid() != $U
fullname($student, true)),
);
}
if ($attemptobj->has_capability('mod/quiz:viewreports')) {
$attemptlist = $attemptobj->links_to_other_attempts($attemptobj->review_url(null, $page,
$showall));
@@ -157,7 +157,12 @@ $summarydata['startedon'] = array(
'content' => userdate($attempt->timestart),
);
if ($attempt->timefinish) {
$summarydata['state'] = array(
'title' => get_string('attemptstate', 'quiz'),
'content' => quiz_attempt::state_name($attempt->state),
);
if ($attempt->state == quiz_attempt::FINISHED) {
$summarydata['completedon'] = array(
'title' => get_string('completedon', 'quiz'),
'content' => userdate($attempt->timefinish),
@@ -179,7 +184,7 @@ if (!empty($overtime)) {
$grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
if ($options->marks >= question_display_options::MARK_AND_MAX && quiz_has_grades($quiz)) {
if (!$attempt->timefinish) {
if ($attempt->state != quiz_attempt::FINISHED) {
$summarydata['grade'] = array(
'title' => get_string('grade', 'quiz'),
'content' => get_string('attemptstillinprogress', 'quiz'),
+15
View File
@@ -55,6 +55,21 @@ $quizsettings->add(new admin_setting_configtext_with_advanced('quiz/timelimit',
get_string('timelimitsec', 'quiz'), get_string('configtimelimitsec', 'quiz'),
array('value' => '0', 'fix' => false), PARAM_INT));
// What to do with overdue attempts.
$quizsettings->add(new mod_quiz_admin_setting_overduehandling('quiz/overduehandling',
get_string('overduehandling', 'quiz'), get_string('overduehandling_desc', 'quiz'),
array('value' => 'autoabandon', 'fix' => false), null));
// Grace period time.
$quizsettings->add(new admin_setting_configtext_with_advanced('quiz/graceperiod',
get_string('graceperiod', 'quiz'), get_string('graceperiod_desc', 'quiz'),
array('value' => '86400', 'fix' => false), PARAM_INT));
// Minimum grace period used behind the scenes.
$quizsettings->add(new admin_setting_configtext('quiz/graceperiodmin',
get_string('graceperiodmin', 'quiz'), get_string('graceperiodmin_desc', 'quiz'),
60, PARAM_INT));
// Number of attempts
$options = array(get_string('unlimited'));
for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) {
+24
View File
@@ -195,3 +195,27 @@ class mod_quiz_admin_setting_browsersecurity extends admin_setting_configselect_
return true;
}
}
/**
* Admin settings class for the quiz overdue attempt handling method.
*
* Just so we can lazy-load the choices.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_admin_setting_overduehandling extends admin_setting_configselect_with_advanced {
public function load_choices() {
global $CFG;
if (is_array($this->choices)) {
return true;
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$this->choices = quiz_get_overdue_handling_options();
return true;
}
}
+25 -12
View File
@@ -21,10 +21,9 @@
*
* This code used to be at the top of attempt.php, if you are looking for CVS history.
*
* @package mod
* @subpackage quiz
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package mod_quiz
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
@@ -60,7 +59,8 @@ if (!$quizobj->has_questions()) {
}
// Create an object to manage all the other (non-roles) access rules.
$accessmanager = $quizobj->get_access_manager(time());
$timenow = time();
$accessmanager = $quizobj->get_access_manager($timenow);
if ($quizobj->is_preview_user() && $forcenew) {
$accessmanager->current_attempt_finished();
}
@@ -72,9 +72,9 @@ if (!$quizobj->is_preview_user()) {
// Check to see if a new preview was requested.
if ($quizobj->is_preview_user() && $forcenew) {
// To force the creation of a new preview, we set a finish time on the
// current attempt (if any). It will then automatically be deleted below
$DB->set_field('quiz_attempts', 'timefinish', time(),
// To force the creation of a new preview, we mark the current attempt (if any)
// as finished. It will then automatically be deleted below
$DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED,
array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id));
}
@@ -83,10 +83,22 @@ $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', tru
$lastattempt = end($attempts);
// If an in-progress attempt exists, check password then redirect to it.
if ($lastattempt && !$lastattempt->timefinish) {
if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS ||
$lastattempt->state == quiz_attempt::OVERDUE)) {
$currentattemptid = $lastattempt->id;
$messages = $accessmanager->prevent_access();
// If the attempt is now overdue, deal with that.
$quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true);
// And, if the attempt is now no longer in progress, redirect to the appropriate place.
if ($lastattempt->state == quiz_attempt::OVERDUE) {
redirect($quizobj->summary_url($lastattempt->id));
} else if ($lastattempt->state != quiz_attempt::IN_PROGRESS) {
redirect($quizobj->review_url($lastattempt->id));
}
// If the page number was not explicitly in the URL, go to the current page.
if ($page == -1) {
$page = $lastattempt->currentpage;
}
@@ -152,7 +164,8 @@ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
// Create the new attempt and initialize the question sessions
$attempt = quiz_create_attempt($quizobj->get_quiz(), $attemptnumber, $lastattempt, time(),
$timenow = time(); // Update time now, in case the server is running really slowly.
$attempt = quiz_create_attempt($quizobj->get_quiz(), $attemptnumber, $lastattempt, $timenow,
$quizobj->is_preview_user());
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
@@ -192,8 +205,7 @@ if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
$variantoffset = $attemptnumber;
}
$quba->start_all_questions(
new question_variant_pseudorandom_no_repeats_strategy($variantoffset),
time());
new question_variant_pseudorandom_no_repeats_strategy($variantoffset), $timenow);
// Update attempt layout.
$newlayout = array();
@@ -252,6 +264,7 @@ $eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->attemptid = $attempt->id;
$eventdata->timestart = $attempt->timestart;
$eventdata->timestamp = $attempt->timestart;
$eventdata->userid = $attempt->userid;
$eventdata->quizid = $quizobj->get_quizid();
$eventdata->cmid = $quizobj->get_cmid();
+6
View File
@@ -1,3 +1,9 @@
/** Bits that can appear on any page. */
.path-mod-quiz .statedetails {
display: block;
font-size: 0.7em;
}
/** Attempt and review pages **/
#page-mod-quiz-attempt #page .controls,
#page-mod-quiz-summary #page .controls,
-2
View File
@@ -89,6 +89,4 @@ $PAGE->set_heading($attemptobj->get_course()->fullname);
$accessmanager->setup_attempt_page($PAGE);
// Display the page.
$accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time(), $output);
echo $output->summary_page($attemptobj, $displayoptions);
+1 -1
View File
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$module->version = 2012030901; // The current module version (Date: YYYYMMDDXX)
$module->version = 2012040206; // The current module version (Date: YYYYMMDDXX)
$module->requires = 2012030100.04; // Requires this Moodle version
$module->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics)
$module->cron = 60;
+26 -15
View File
@@ -78,16 +78,37 @@ $completion->set_module_viewed($cm);
// Initialize $PAGE, compute blocks
$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
// Create view object which collects all the information the renderer will need.
$viewobj = new mod_quiz_view_object();
$viewobj->accessmanager = $accessmanager;
$viewobj->canreviewmine = $canreviewmine;
// Get this user's attempts.
$attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
$lastfinishedattempt = end($attempts);
$unfinished = false;
if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
$attempts[] = $unfinishedattempt;
$unfinished = true;
// If the attempt is now overdue, deal with that - and pass isonline = false.
// We want the student notified in this case.
$quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
$unfinished = $unfinishedattempt->state == quiz_attempt::IN_PROGRESS ||
$unfinishedattempt->state == quiz_attempt::OVERDUE;
if (!$unfinished) {
$lastfinishedattempt = $unfinishedattempt;
}
$unfinishedattempt = null; // To make it clear we do not use this again.
}
$numattempts = count($attempts);
$viewobj->attempts = $attempts;
$viewobj->attemptobjs = array();
foreach ($attempts as $attempt) {
$viewobj->attemptobjs[] = new quiz_attempt($attempt, $quiz, $cm, $course, false);
}
// Work out the final grade, checking whether it was overridden in the gradebook.
if (!$canpreview) {
$mygrade = quiz_get_best_grade($quiz, $USER->id);
@@ -123,29 +144,19 @@ $PAGE->set_title($title);
$PAGE->set_heading($course->fullname);
$output = $PAGE->get_renderer('mod_quiz');
/*
* Create view object for use within renderers file
*/
$viewobj = new mod_quiz_view_object();
$viewobj->attempts = $attempts;
$viewobj->accessmanager = $accessmanager;
$viewobj->canreviewmine = $canreviewmine;
// Print table with existing attempts
if ($attempts) {
// Work out which columns we need, taking account what data is available in each attempt.
list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
$viewobj->attemptcolumn = $quiz->attempts != 1;
$viewobj->attemptcolumn = $quiz->attempts != 1;
$viewobj->gradecolumn = $someoptions->marks >= question_display_options::MARK_AND_MAX &&
$viewobj->gradecolumn = $someoptions->marks >= question_display_options::MARK_AND_MAX &&
quiz_has_grades($quiz);
$viewobj->markcolumn = $viewobj->gradecolumn && ($quiz->grade != $quiz->sumgrades);
$viewobj->overallstats = $lastfinishedattempt && $alloptions->marks >= question_display_options::MARK_AND_MAX;
$viewobj->markcolumn = $viewobj->gradecolumn && ($quiz->grade != $quiz->sumgrades);
$viewobj->overallstats = $lastfinishedattempt && $alloptions->marks >= question_display_options::MARK_AND_MAX;
$viewobj->feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback;
} else {
$viewobj->attemptcolumn = 1;
}
$viewobj->timenow = $timenow;