MDL-3030 quiz overdue handling: show attempt state on the view page

This commit is contained in:
Tim Hunt
2012-04-27 15:07:27 +01:00
parent fd55f4bf49
commit 2de9be52aa
6 changed files with 159 additions and 75 deletions
+20 -8
View File
@@ -391,6 +391,25 @@ class quiz_access_manager {
}
}
/**
* 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'.
* @return int|false the number of seconds remaining for this attempt.
* False if there is no limit.
*/
public function get_time_left($attempt, $timenow) {
$timeleft = false;
foreach ($this->rules as $rule) {
$ruletimeleft = $rule->time_left($attempt, $timenow);
if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) {
$timeleft = $ruletimeleft;
}
}
return $timeleft;
}
/**
* Will cause the attempt time to start counting down after the page has loaded,
* if that is necessary.
@@ -400,14 +419,7 @@ class quiz_access_manager {
* @param mod_quiz_renderer $output the quiz renderer.
*/
public function show_attempt_timer_if_needed($attempt, $timenow, $output) {
$timeleft = false;
foreach ($this->rules as $rule) {
$ruletimeleft = $rule->time_left($attempt, $timenow);
if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) {
$timeleft = $ruletimeleft;
}
}
$timeleft = $this->get_time_left($attempt, $timenow);
if ($timeleft !== false) {
// Make sure the timer starts just above zero. If $timeleft was <= 0, then
+56 -4
View File
@@ -411,12 +411,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;
@@ -612,6 +622,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 +636,16 @@ 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).
*/
public function is_finished() {
return $this->attempt->timefinish != 0;
return $this->attempt->state == self::FINISHED;
}
/** @return bool whether this attempt is a preview attempt. */
@@ -927,6 +945,40 @@ class quiz_attempt {
return $this->quba->get_question_action_time($slot);
}
/**
* @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($timenow) {
switch ($attempt->state) {
case self::IN_PROGRESS:
$timeleft = $this->get_access_manager($timenow)->get_time_left(
$this->attempt, $timenow);
if ($timeleft === false) {
return false;
} else {
return $timenow + $timeleft;
}
case self::OVERDUE:
return $this->attempt->timefinished + $this->quizobj->get_quiz()->graceperiod;
default:
throw new coding_exception('Unexpected state: ' . $attempt->state);
}
}
// URLs related to this attempt ========================================================
/**
* @return string quiz view url.
+7
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';
@@ -740,7 +741,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';
+55 -49
View File
@@ -856,11 +856,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 +871,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 +896,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, $viewobj->timenow);
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 +956,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, $timenow) {
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($timenow))),
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 +1138,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
+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,
+15 -14
View File
@@ -78,6 +78,11 @@ $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);
@@ -88,6 +93,12 @@ if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id))
}
$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 +134,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;