MDL-3030 quiz overdue handling: show attempt state on the view page
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user