mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 16:32:18 +02:00
Cleaned up issues to do with timing. So for example late submissions are now detected correctly.
This commit is contained in:
parent
9402459460
commit
488cf46b65
@ -22,6 +22,10 @@
|
||||
$timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer.
|
||||
$forcenew = optional_param('forcenew', false, PARAM_BOOL); // Teacher has requested new preview
|
||||
|
||||
// remember the current time as the time any responses were submitted
|
||||
// (so as to make sure students don't get penalized for slow processing on this page)
|
||||
$timestamp = time();
|
||||
|
||||
// We treat automatically closed attempts just like normally closed attempts
|
||||
if ($timeup) {
|
||||
$finishattempt = 1;
|
||||
@ -104,15 +108,6 @@
|
||||
error(get_string('nomoreattempts', 'quiz'), "view.php?id={$cm->id}");
|
||||
}
|
||||
|
||||
$timenow = time();
|
||||
if (($timenow < $quiz->timeopen || $timenow > $quiz->timeclose)) {
|
||||
if ($isteacher) {
|
||||
notify(get_string('notavailabletostudents', 'quiz'));
|
||||
} else {
|
||||
error(get_string('notavailable', 'quiz'), "view.php?id={$cm->id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Check subnet access
|
||||
if ($quiz->subnet and !address_in_subnet(getremoteaddr(), $quiz->subnet)) {
|
||||
if ($isteacher) {
|
||||
@ -161,7 +156,7 @@
|
||||
if ($isteacher and $forcenew) { // teacher wants a new preview
|
||||
// so we set a finish time on the current attempt (if any).
|
||||
// It will then automatically be deleted below
|
||||
set_field('quiz_attempts', 'timefinish', time(), 'quiz', $quiz->id, 'userid', $USER->id);
|
||||
set_field('quiz_attempts', 'timefinish', $timestamp, 'quiz', $quiz->id, 'userid', $USER->id);
|
||||
}
|
||||
|
||||
$attempt = get_record('quiz_attempts', 'quiz', $quiz->id,
|
||||
@ -204,13 +199,15 @@
|
||||
}
|
||||
} else {
|
||||
// log continuation of attempt only if some time has lapsed
|
||||
if ((time() - $attempt->timemodified) > 600) { // 10 minutes have elapsed
|
||||
if (($timestamp - $attempt->timemodified) > 600) { // 10 minutes have elapsed
|
||||
add_to_log($course->id, 'quiz', 'continue attempt',
|
||||
"review.php?attempt=$attempt->id",
|
||||
"$quiz->id", $cm->id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($attempt->timestart) { // shouldn't really happen, just for robustness
|
||||
$attempt->timestart = time();
|
||||
}
|
||||
|
||||
/// Load all the questions and states needed by this script
|
||||
|
||||
@ -282,11 +279,12 @@
|
||||
if (!isset($actions[$i])) {
|
||||
$actions[$i]->responses = array('' => '');
|
||||
}
|
||||
$actions[$i]->timestamp = $timestamp;
|
||||
quiz_process_responses($questions[$i], $states[$i], $actions[$i], $quiz, $attempt);
|
||||
quiz_save_question_session($questions[$i], $states[$i]);
|
||||
}
|
||||
|
||||
$attempt->timemodified = time();
|
||||
$attempt->timemodified = $timestamp;
|
||||
|
||||
// We have now finished processing form data
|
||||
}
|
||||
@ -296,7 +294,7 @@
|
||||
if ($finishattempt) {
|
||||
|
||||
// Set the attempt to be finished
|
||||
$attempt->timefinish = time();
|
||||
$attempt->timefinish = $timestamp;
|
||||
|
||||
// Find all the questions for this attempt for which the newest
|
||||
// state is not also the newest graded state
|
||||
@ -327,6 +325,7 @@
|
||||
foreach($closequestions as $key => $question) {
|
||||
$action->event = QUIZ_EVENTCLOSE;
|
||||
$action->responses = $closestates[$key]->responses;
|
||||
$action->timestamp = $colsestates[$key]->timestamp;
|
||||
quiz_process_responses($question, $closestates[$key], $action, $quiz, $attempt);
|
||||
quiz_save_question_session($question, $closestates[$key]);
|
||||
}
|
||||
@ -336,7 +335,6 @@
|
||||
"$quiz->id", $cm->id);
|
||||
}
|
||||
|
||||
|
||||
/// Update the quiz attempt and the overall grade for the quiz
|
||||
if ($responses || $finishattempt) {
|
||||
if (!update_record('quiz_attempts', $attempt)) {
|
||||
@ -347,31 +345,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// Check access to quiz page
|
||||
|
||||
// check the quiz times
|
||||
if (($timestamp < $quiz->timeopen || $timestamp > $quiz->timeclose)) {
|
||||
if ($isteacher) {
|
||||
notify(get_string('notavailabletostudents', 'quiz'));
|
||||
} else {
|
||||
print_continue(get_string('notavailable', 'quiz'), "view.php?id={$cm->id}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($finishattempt) {
|
||||
redirect('review.php?attempt='.$attempt->id);
|
||||
}
|
||||
|
||||
/// Get time limit if any.
|
||||
$timelimit = $quiz->timelimit * 60;
|
||||
|
||||
if ($timelimit > 0) {
|
||||
$timestart = $attempt->timestart;
|
||||
if ($timestart) {
|
||||
$timesincestart = $timenow - $timestart;
|
||||
$timerstartvalue = $timelimit - $timesincestart;
|
||||
} else {
|
||||
$timerstartvalue = $timelimit;
|
||||
}
|
||||
if ($timerstartvalue <= 0) {
|
||||
$timerstartvalue = 1;
|
||||
}
|
||||
if(($timelimit + 60) <= $timesincestart) {
|
||||
// To pass it on to quiz_grade_responses
|
||||
$quiz->timesincestart = $timesincestart;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Print the quiz page ////////////////////////////////////////////////////////
|
||||
|
||||
/// Print the attempt number or preview heading
|
||||
@ -387,12 +375,6 @@
|
||||
print_heading($strattemptnum);
|
||||
}
|
||||
|
||||
/// Add the javascript timer in the title bar if the closing time appears close
|
||||
$secondsleft = $quiz->timeclose - time();
|
||||
if ($secondsleft > 0 and $secondsleft < 24*3600) { // less than a day remaining
|
||||
include('jsclock.php');
|
||||
}
|
||||
|
||||
/// Start the form
|
||||
if($quiz->timelimit > 0) {
|
||||
// Make sure javascript is enabled for time limited quizzes
|
||||
@ -472,9 +454,23 @@
|
||||
// Finish the form
|
||||
echo "</form>\n";
|
||||
|
||||
|
||||
$secondsleft = $quiz->timeclose - time();
|
||||
// If time limit is set include floating timer.
|
||||
if ($timelimit > 0) {
|
||||
if ($quiz->timelimit > 0) {
|
||||
|
||||
$timesincestart = time() - $attempt->timestart;
|
||||
$timerstartvalue = min($quiz->timelimit*60 - $timesincestart, $secondsleft);
|
||||
if ($timerstartvalue <= 0) {
|
||||
$timerstartvalue = 1;
|
||||
}
|
||||
|
||||
require('jstimer.php');
|
||||
} else {
|
||||
// Add the javascript timer in the title bar if the closing time appears close
|
||||
if ($secondsleft > 0 and $secondsleft < 24*3600) { // less than a day remaining
|
||||
include('jsclock.php');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isteacher) {
|
||||
|
38
mod/quiz/doc/eventtypes.html
Normal file
38
mod/quiz/doc/eventtypes.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE HTML PUBLIC> <HTML>
|
||||
<HEAD>
|
||||
<TITLE>Event types</TITLE>
|
||||
<META http-equiv="Content-Type" content="text/html;charset=iso-8859-1">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
|
||||
<h1>Event types</h1>
|
||||
|
||||
<p>The $state object (and the quiz_states table) has a field $event
|
||||
which indicates the event that led to the state's creation. The
|
||||
field can take the value of any of the following constants (defined
|
||||
in locallib.php):</p>
|
||||
|
||||
<ul>
|
||||
<li>EVENTOPEN: The attempt has just been opened and this is the initial
|
||||
state for which no attempts have come in yet.</li>
|
||||
<li>EVENTSAVE: The responses are just being saved, either because the student
|
||||
requested this explicitly or because the student navigated to another
|
||||
quiz page.</li>
|
||||
<li>EVENTVALIDATE: The student requested a validation of the responses.
|
||||
<li>EVENTGRADE: The responses are being graded but the question session
|
||||
is not closed.</li>
|
||||
<li>EVENTCLOSE: The responses are being graded and the question session
|
||||
is closed. Usually this happens because the whole attempt closes,
|
||||
either because the student requests it or because the time is up
|
||||
or we are beyond the due date.</li>
|
||||
<li>EVENTDUPLICATEGRADE: This is a strange one. It indicates that the
|
||||
responses would have been graded had they not been found to be
|
||||
identical to previous responses.</li>
|
||||
</ul>
|
||||
|
||||
<p>When new responses are being processed by
|
||||
quiz_process_responses() then this function is being passed the
|
||||
event type in $action->event while the responses are in
|
||||
$action->responses.</p>
|
||||
|
||||
</BODY> </HTML>
|
@ -60,6 +60,15 @@ per response.</p>
|
||||
|
||||
<h2>Where it is done in the code</h2>
|
||||
|
||||
The function quiz_apply_penalty_and_timelimit() subtracts the penalty in
|
||||
$state->sumpenalty from the raw grade in $state->raw_grade to obtain
|
||||
$state->grade for the response. However it is ensured that the grade
|
||||
of a new attempt at the question never falls below the previously
|
||||
achieved grade. This function also increases $state->sumpenalty by
|
||||
the amount in $state->penalty. The assumption is that
|
||||
$state->penalty has just been set appropriately by the code calling
|
||||
this function, e.g., quiz_process_responses.
|
||||
|
||||
<h2>About wrapped questions</h2>
|
||||
|
||||
</BODY>
|
||||
|
39
mod/quiz/doc/timelimit.html
Normal file
39
mod/quiz/doc/timelimit.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE HTML PUBLIC> <HTML>
|
||||
<HEAD>
|
||||
<TITLE>Time limit</TITLE>
|
||||
<META http-equiv="Content-Type" content="text/html;charset=iso-8859-1">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
|
||||
<h1>Time limit</h1>
|
||||
|
||||
<p>A quiz can have a time limit. This is stored in minutes in
|
||||
$quiz->timelimit. So before using this in time calculations it
|
||||
always has to be multiplied by 60 to turn it into seconds like all
|
||||
other timestamps in moodle and php. If $quiz->timelimit is zero it
|
||||
means there is no timelimit.</p>
|
||||
|
||||
<p>If a student asks to start an attempt on view.php for a quiz with
|
||||
a timelimit then he is shown a javascript message alerting him to
|
||||
the timelimit and is asked to confirm.</p>
|
||||
|
||||
<p>For quizzes with timelimit attempt.php shows a javascript timer
|
||||
that counts down and automatically submits and closes the attempt
|
||||
when the time is up.</p>
|
||||
|
||||
<p>Confusingly there are two javascript timers in the quiz module.
|
||||
jsclock.php provides a countdown in the title bar that counts down
|
||||
to the quiz closing time if this is less than a day away. This has
|
||||
nothing to do with the timelimit. jstimer.php provides the countdown
|
||||
timer that implements the timelimit. It in turn uses timer.js.</p>
|
||||
|
||||
<p>The time a response was submitted by the student is recorded by
|
||||
attempt.php right at the top of the page and is then passed on to
|
||||
quiz_process_responses in $action->timestamp. This puts it into
|
||||
$state->timestamp. Finally, after the responses have been graded,
|
||||
the function quiz_apply_penalty_and_timelimit() checks that the
|
||||
responses are within the timelimit to within 5% and if not it sets
|
||||
the grade to zero (or the previously obtained grade, if that is
|
||||
higher).</p>
|
||||
|
||||
</BODY> </HTML>
|
@ -1233,8 +1233,7 @@ function quiz_restore_state(&$question, &$state) {
|
||||
|
||||
// Set the changed field to false; any code which changes the
|
||||
// question session must set this to true and must increment
|
||||
// ->seq_number; it can do this by calling
|
||||
// quiz_mark_session_change. The quiz_save_question_session
|
||||
// ->seq_number. The quiz_save_question_session
|
||||
// function will save the new state object database if the field is
|
||||
// set to true.
|
||||
$state->changed = false;
|
||||
@ -1484,8 +1483,10 @@ function quiz_regrade_question_in_quizzes($question, $quizlist) {
|
||||
* @param object $question Full question object, passed by reference
|
||||
* @param object $state Full state object, passed by reference
|
||||
* @param object $action object with the fields ->responses which
|
||||
* is an array holding the student responses and
|
||||
* ->action which specifies the action, e.g., QUIZ_EVENTGRADE
|
||||
* is an array holding the student responses,
|
||||
* ->action which specifies the action, e.g., QUIZ_EVENTGRADE,
|
||||
* and ->timestamp which is a timestamp from when the responses
|
||||
* were submitted by the student.
|
||||
* @param object $quiz The quiz object
|
||||
* @param object $attempt The attempt is passed by reference so that
|
||||
* during grading its ->sumgrades field can be updated
|
||||
@ -1510,7 +1511,7 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
}
|
||||
// Check if we are grading the question; compare against last graded
|
||||
// responses, not last given responses in this case
|
||||
if (QUIZ_EVENTGRADE == $action->event || QUIZ_EVENTCLOSE == $action->event) {
|
||||
if (quiz_isgradingevent($action->event)) {
|
||||
$state->responses = $state->last_graded->responses;
|
||||
}
|
||||
// Check for unchanged responses (exactly unchanged, not equivalent).
|
||||
@ -1518,7 +1519,7 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
$sameresponses = (($state->responses == $action->responses) or
|
||||
($state->responses == array(''=>'') && array_keys(array_count_values($action->responses))===array('')));
|
||||
|
||||
if ($sameresponses && isset($action->event) and QUIZ_EVENTCLOSE != $action->event
|
||||
if ($sameresponses and QUIZ_EVENTCLOSE != $action->event
|
||||
and QUIZ_EVENTVALIDATE != $action->event) {
|
||||
return true;
|
||||
}
|
||||
@ -1527,9 +1528,10 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
// responses
|
||||
$newstate = clone($state->last_graded);
|
||||
$newstate->responses = $action->responses;
|
||||
$newstate->seq_number = $state->seq_number;
|
||||
$newstate->changed = false;
|
||||
$newstate->seq_number = $state->seq_number + 1;
|
||||
$newstate->changed = true; // will assure that it gets saved to the database
|
||||
$newstate->last_graded = $state->last_graded;
|
||||
$newstate->timestamp = $action->timestamp;
|
||||
$state = $newstate;
|
||||
|
||||
// Set the event to the action we will perform. The question type specific
|
||||
@ -1537,7 +1539,7 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
// attempt at the question causes the session to close
|
||||
$state->event = $action->event;
|
||||
|
||||
if (QUIZ_EVENTSAVE == $action->event || QUIZ_EVENTVALIDATE == $action->event) {
|
||||
if (!quiz_isgradingevent($action->event)) {
|
||||
// Grade the response but don't update the overall grade
|
||||
$QUIZ_QTYPES[$question->qtype]->grade_responses(
|
||||
$question, $state, $quiz);
|
||||
@ -1571,7 +1573,7 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
$QUIZ_QTYPES[$question->qtype]->grade_responses(
|
||||
$question, $state, $quiz);
|
||||
// Calculate overall grade using correct penalty method
|
||||
quiz_apply_penalty($question, $state, $quiz);
|
||||
quiz_apply_penalty_and_timelimit($question, $state, $attempt, $quiz);
|
||||
// Update the last graded state (don't simplify!)
|
||||
unset($state->last_graded);
|
||||
$state->last_graded = clone($state);
|
||||
@ -1589,7 +1591,7 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
$QUIZ_QTYPES[$question->qtype]->grade_responses(
|
||||
$question, $state, $quiz);
|
||||
// Calculate overall grade using correct penalty method
|
||||
quiz_apply_penalty($question, $state, $quiz);
|
||||
quiz_apply_penalty_and_timelimit($question, $state, $attempt, $quiz);
|
||||
}
|
||||
// Force the state to close (as the attempt is closing)
|
||||
$state->event = QUIZ_EVENTCLOSE;
|
||||
@ -1606,11 +1608,17 @@ function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt)
|
||||
|
||||
$attempt->sumgrades += (float)$state->last_graded->grade;
|
||||
}
|
||||
quiz_mark_session_change($state);
|
||||
$attempt->timemodified = time();
|
||||
$attempt->timemodified = $action->timestamp;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if event requires grading
|
||||
*/
|
||||
function quiz_isgradingevent($event) {
|
||||
return (QUIZ_EVENTGRADE == $event || QUIZ_EVENTCLOSE == $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare current responses to all previous graded responses
|
||||
*
|
||||
@ -1645,13 +1653,14 @@ function quiz_search_for_duplicate_responses(&$question, &$state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the penalty for the previous attempts to the raw grade for the current
|
||||
* Applies the penalty from the previous attempts to the raw grade for the current
|
||||
* attempt
|
||||
*
|
||||
* The grade for the question in the current state is computed by applying the
|
||||
* penalty accumulated over the previous marked attempts at the question to the
|
||||
* raw grade using the penalty scheme in use in the quiz. The ->grade field of
|
||||
* the state object is modified to reflect the new grade.
|
||||
* The grade for the question in the current state is computed by subtracting the
|
||||
* penalty accumulated over the previous marked attempts at the question from the
|
||||
* raw grade. If the timestamp is more than 1 minute beyond the start of the attempt
|
||||
* the grade is set to zero. The ->grade field of the state object is modified to
|
||||
* reflect the new grade but is never allowed to decrease.
|
||||
* @param object $question The question for which the penalty is to be applied.
|
||||
* @param object $state The state for which the grade is to be set from the
|
||||
* raw grade and the cumulative penalty from the last
|
||||
@ -1661,13 +1670,28 @@ function quiz_search_for_duplicate_responses(&$question, &$state) {
|
||||
* @param object $quiz The quiz to which the question belongs. The penalty
|
||||
* scheme to apply is given by the ->penaltyscheme field.
|
||||
*/
|
||||
function quiz_apply_penalty(&$question, &$state, $quiz) {
|
||||
function quiz_apply_penalty_and_timelimit(&$question, &$state, $attempt, $quiz) {
|
||||
// deal with penaly
|
||||
if ($quiz->penaltyscheme) {
|
||||
$state->grade = $state->raw_grade - $state->sumpenalty;
|
||||
$state->sumpenalty += (float) $state->penalty;
|
||||
} else {
|
||||
$state->grade = $state->raw_grade;
|
||||
}
|
||||
|
||||
// deal with timeimit
|
||||
if ($quiz->timelimit) {
|
||||
// We allow for 5% uncertainty in the following test
|
||||
if (($state->timestamp - $attempt->timestart) > ($quiz->timelimit * 63)) {
|
||||
$state->grade = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// deal with quiz closing time
|
||||
if ($state->timestamp > ($quiz->timeclose + 60)) { // allowing 1 minute lateness
|
||||
$state->grade = 0;
|
||||
}
|
||||
|
||||
// Ensure that the grade does not go down
|
||||
$state->grade = max($state->grade, $state->last_graded->grade);
|
||||
}
|
||||
|
@ -150,6 +150,7 @@
|
||||
|
||||
$event = $finishattempt ? QUIZ_EVENTCLOSE : ($markall ? QUIZ_EVENTGRADE : QUIZ_EVENTSAVE);
|
||||
if ($actions = quiz_extract_responses($questions, $form, $event)) {
|
||||
$actions[$id]->timestamp = 0; // We do not care about timelimits here
|
||||
quiz_process_responses($questions[$id], $states[$historylength][$id], $actions[$id], $quiz, $attempt);
|
||||
if (QUIZ_EVENTGRADE != $curstate->event && QUIZ_EVENTCLOSE != $curstate->event) {
|
||||
// Update the current state rather than creating a new one
|
||||
|
Loading…
x
Reference in New Issue
Block a user