Cleaned up issues to do with timing. So for example late submissions are now detected correctly.

This commit is contained in:
gustav_delius 2005-05-15 11:45:12 +00:00
parent 9402459460
commit 488cf46b65
6 changed files with 169 additions and 62 deletions

View File

@ -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) {

View 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>

View File

@ -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>

View 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>

View File

@ -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);
}

View File

@ -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