Commit 5e63b335 authored by Tim Hunt's avatar Tim Hunt
Browse files

MDL-40992 quiz: option to let students redo questions within an attempt

This feature is designed for use on pracice or formative quizzes.
It is available for quizzes that use Interactive or Immediate feedback
behaviour.

If the teacher turns this on in the quiz settings, then once a student
has finished a question, they get a 'Redo question' button beside the
question. If they click it, then the question they finished is replaced
by a new one so they can try again to practise that particul skill or
bit of knowledge a bit more.

When randomisation is involved, the studnets will be given a question or
variant that they have not seen before if possible.
parent f6579bea
......@@ -397,9 +397,7 @@ $string['questiontext'] = 'Question text';
$string['requiresgrading'] = 'Requires grading';
$string['responsehistory'] = 'Response history';
$string['restart'] = 'Start again';
$string['restartquestion'] = 'Restart question';
$string['restartwiththeseoptions'] = 'Start again with these options';
$string['updatedisplayoptions'] = 'Update display options';
$string['rightanswer'] = 'Right answer';
$string['rightanswer_help'] = 'an automatically generated summary of the correct response. This can be limited, so you may wish to consider explaining the correct solution in the general feedback for the question, and turning this option off.';
$string['saved'] = 'Saved: {$a}';
......
......@@ -125,12 +125,6 @@ if ($attemptobj->get_currentpage() != $page) {
$DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $attemptid));
}
// Process replace question action, when user press on 'Replace question' link.
$replacequestioninslot = optional_param('replacequestioninslot', 0, PARAM_INT);
if ($replacequestioninslot) {
$attemptobj->process_replace_question_actions($replacequestioninslot, time());
}
// Initialise the JavaScript.
$headtags = $attemptobj->get_html_head_contributions($page);
$PAGE->requires->js_init_call('M.mod_quiz.init_attempt_form', null, false, quiz_get_js_module());
......
......@@ -951,11 +951,11 @@ class quiz_attempt {
}
/**
* Return the list of question ids for either a given page of the quiz, or for the
* Return the list of slot numbers for either a given page of the quiz, or for the
* whole quiz.
*
* @param mixed $page string 'all' or integer page number.
* @return array the reqested list of question ids.
* @return array the requested list of slot numbers.
*/
public function get_slots($page = 'all') {
if ($page === 'all') {
......@@ -969,6 +969,23 @@ class quiz_attempt {
}
}
/**
* Return the list of slot numbers for either a given page of the quiz, or for the
* whole quiz.
*
* @param mixed $page string 'all' or integer page number.
* @return array the requested list of slot numbers.
*/
public function get_active_slots($page = 'all') {
$activeslots = array();
foreach ($this->get_slots($page) as $slot) {
if (!$this->is_blocked_by_previous_question($slot)) {
$activeslots[] = $slot;
}
}
return $activeslots;
}
/**
* Get the question_attempt object for a particular question in this attempt.
* @param int $slot the number used to identify this question within this attempt.
......@@ -978,6 +995,22 @@ class quiz_attempt {
return $this->quba->get_question_attempt($slot);
}
/**
* Get the question_attempt object for a particular question in this attempt.
* @param int $slot the number used to identify this question within this attempt.
* @return question_attempt
*/
public function all_question_attempts_originally_in_slot($slot) {
$qas = array();
foreach ($this->quba->get_attempt_iterator() as $qa) {
if ($qa->get_metadata('originalslot') == $slot) {
$qas[] = $qa;
}
}
$qas[] = $this->quba->get_question_attempt($slot);
return $qas;
}
/**
* Is a particular question in this attempt a real question, or something like a description.
* @param int $slot the number used to identify this question within this attempt.
......@@ -1004,13 +1037,39 @@ class quiz_attempt {
* @return bool whether the previous question must have been completed before this one can be seen.
*/
public function is_blocked_by_previous_question($slot) {
return $slot > 1 && $this->slots[$slot]->requireprevious &&
return $slot > 1 && isset($this->slots[$slot]) && $this->slots[$slot]->requireprevious &&
!$this->get_quiz()->shufflequestions &&
$this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ &&
!$this->quba->get_question_state($slot - 1)->is_finished() &&
!$this->get_question_state($slot - 1)->is_finished() &&
$this->quba->can_question_finish_during_attempt($slot - 1);
}
/**
* Is it possible for this question to be re-started within this attempt?
*
* @param int $slot the number used to identify this question within this attempt.
* @return whether the student should be given the option to restart this question now.
*/
public function can_question_be_redone_now($slot) {
return $this->get_quiz()->canredoquestions && !$this->is_finished() &&
$this->get_question_state($slot)->is_finished();
}
/**
* Given a slot in this attempt, which may or not be a redone question, return the original slot.
*
* @param int $slot identifies a particular question in this attempt.
* @return int the slot where this question was originally.
*/
public function get_original_slot($slot) {
$originalslot = $this->quba->get_question_attempt_metadata($slot, 'originalslot');
if ($originalslot) {
return $originalslot;
} else {
return $slot;
}
}
/**
* Get the displayed question number for a slot.
* @param int $slot the number used to identify this question within this attempt.
......@@ -1042,6 +1101,16 @@ class quiz_attempt {
return $this->quba->get_question($slot)->name;
}
/**
* Return the {@link question_state} that this question is in.
*
* @param int $slot the number used to identify this question within this attempt.
* @return question_state the state this question is in.
*/
public function get_question_state($slot) {
return $this->quba->get_question_state($slot);
}
/**
* Return the grade obtained on a particular question, if the user is permitted
* to see it. You must previously have called load_question_states to load the
......@@ -1275,12 +1344,13 @@ class quiz_attempt {
* Generate the HTML that displayes the question in its current state, with
* the appropriate display options.
*
* @param int $id the id of a question in this quiz attempt.
* @param int $slot identifies the question in the attempt.
* @param bool $reviewing is the being printed on an attempt or a review page.
* @param mod_quiz_renderer $renderer the quiz renderer.
* @param moodle_url $thispageurl the URL of the page this question is being printed on.
* @return string HTML for the question in its current state.
*/
public function render_question($slot, $reviewing, $thispageurl = null) {
public function render_question($slot, $reviewing, mod_quiz_renderer $renderer, $thispageurl = null) {
if ($this->is_blocked_by_previous_question($slot)) {
$placeholderqa = $this->make_blocked_question_placeholder($slot);
......@@ -1290,20 +1360,64 @@ class quiz_attempt {
$displayoptions->readonly = true;
return html_writer::div($placeholderqa->render($displayoptions,
$this->get_question_number($slot)),
$this->get_question_number($this->get_original_slot($slot))),
'mod_quiz-blocked_question_warning');
}
return $this->quba->render_question($slot,
$this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
$this->get_question_number($slot));
return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, null);
}
/**
* Helper used by {@link render_question()} and {@link render_question_at_step()}.
*
* @param int $slot identifies the question in the attempt.
* @param bool $reviewing is the being printed on an attempt or a review page.
* @param moodle_url $thispageurl the URL of the page this question is being printed on.
* @param mod_quiz_renderer $renderer the quiz renderer.
* @param int|null $seq the seq number of the past state to display.
* @return string HTML fragment.
*/
protected function render_question_helper($slot, $reviewing, $thispageurl, mod_quiz_renderer $renderer, $seq) {
$originalslot = $this->get_original_slot($slot);
$number = $this->get_question_number($originalslot);
$displayoptions = $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl);
if ($slot != $originalslot) {
$originalmaxmark = $this->get_question_attempt($slot)->get_max_mark();
$this->get_question_attempt($slot)->set_max_mark($this->get_question_attempt($originalslot)->get_max_mark());
}
if ($this->can_question_be_redone_now($slot)) {
$displayoptions->extrainfocontent = $renderer->redo_question_button(
$slot, $displayoptions->readonly);
}
if ($displayoptions->history && $displayoptions->questionreviewlink) {
$links = $this->links_to_other_redos($slot, $displayoptions->questionreviewlink);
if ($links) {
$displayoptions->extrahistorycontent = html_writer::tag('p',
get_string('redoesofthisquestion', 'quiz', $renderer->render($links)));
}
}
if ($seq === null) {
$output = $this->quba->render_question($slot, $displayoptions, $number);
} else {
$output = $this->quba->render_question_at_step($slot, $seq, $displayoptions, $number);
}
if ($slot != $originalslot) {
$this->get_question_attempt($slot)->set_max_mark($originalmaxmark);
}
return $output;
}
/**
* Create a fake question to be displayed in place of a question that is blocked
* until the previous question has been answered.
*
* @param unknown $slot int slot number of the question to replace.
* @param int $slot int slot number of the question to replace.
* @return question_definition the placeholde question.
*/
protected function make_blocked_question_placeholder($slot) {
......@@ -1345,13 +1459,12 @@ class quiz_attempt {
* @param int $id the id of a question in this quiz attempt.
* @param int $seq the seq number of the past state to display.
* @param bool $reviewing is the being printed on an attempt or a review page.
* @param mod_quiz_renderer $renderer the quiz renderer.
* @param string $thispageurl the URL of the page this question is being printed on.
* @return string HTML for the question in its current state.
*/
public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
return $this->quba->render_question_at_step($slot, $seq,
$this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
$this->get_question_number($slot));
public function render_question_at_step($slot, $seq, $reviewing, mod_quiz_renderer $renderer, $thispageurl = '') {
return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, $seq);
}
/**
......@@ -1401,11 +1514,18 @@ class quiz_attempt {
}
/**
* Given a URL containing attempt={this attempt id}, return an array of variant URLs
* Return an array of variant URLs to other attempts at this quiz.
*
* The $url passed in must contain an attempt parameter.
*
* The {@link mod_quiz_links_to_other_attempts} object returned contains an
* array with keys that are the attempt number, 1, 2, 3.
* The array values are either a {@link moodle_url} with the attmept parameter
* updated to point to the attempt id of the other attempt, or null corresponding
* to the current attempt number.
*
* @param moodle_url $url a URL.
* @return string HTML fragment. Comma-separated list of links to the other
* attempts with the attempt number as the link text. The curent attempt is
* included but is not a link.
* @return mod_quiz_links_to_other_attempts containing array int => null|moodle_url.
*/
public function links_to_other_attempts(moodle_url $url) {
$attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all');
......@@ -1424,6 +1544,47 @@ class quiz_attempt {
return $links;
}
/**
* Return an array of variant URLs to other redos of the question in a particular slot.
*
* The $url passed in must contain a slot parameter.
*
* The {@link mod_quiz_links_to_other_attempts} object returned contains an
* array with keys that are the redo number, 1, 2, 3.
* The array values are either a {@link moodle_url} with the slot parameter
* updated to point to the slot that has that redo of this question; or null
* corresponding to the redo identified by $slot.
*
* @param int $slot identifies a question in this attempt.
* @param moodle_url $baseurl the base URL to modify to generate each link.
* @return mod_quiz_links_to_other_attempts|null containing array int => null|moodle_url,
* or null if the question in this slot has not been redone.
*/
public function links_to_other_redos($slot, moodle_url $baseurl) {
$originalslot = $this->get_original_slot($slot);
$qas = $this->all_question_attempts_originally_in_slot($originalslot);
if (count($qas) <= 1) {
return null;
}
$links = new mod_quiz_links_to_other_attempts();
$index = 1;
foreach ($qas as $qa) {
if ($qa->get_slot() == $slot) {
$links->links[$index] = null;
} else {
$url = new moodle_url($baseurl, array('slot' => $qa->get_slot()));
$links->links[$index] = new action_link($url, $index,
new popup_action('click', $url, 'reviewquestion',
array('width' => 450, 'height' => 650)),
array('title' => get_string('reviewresponse', 'question')));
}
$index++;
}
return $links;
}
// Methods for processing ==================================================
/**
......@@ -1528,41 +1689,54 @@ class quiz_attempt {
}
/**
* Process replace question action
* @param int $slot
* @param int $timestamp
* Replace a question in an attempt with a new attempt at the same qestion.
* @param int $slot the questoin to restart.
* @param int $timestamp the timestamp to record for this action.
*/
public function process_replace_question_actions($slot, $timestamp) {
public function process_redo_question($slot, $timestamp) {
global $DB;
$transaction = $DB->start_delegated_transaction();
if (!$this->can_question_be_redone_now($slot)) {
throw new coding_exception('Attempt to restart the question in slot ' . $slot .
' when it is not in a state to be restarted.');
}
$this->quba->replace_question($slot);
question_engine::save_questions_usage_by_activity($this->quba);
$qubaids = new \mod_quiz\question\qubaids_for_users_attempts(
$this->get_quizid(), $this->get_userid());
$transaction->allow_commit();
}
$transaction = $DB->start_delegated_transaction();
/**
* Return a button which allows students reattempting the current question
*
* @param int $slot, the number of the current slot
*/
public function restart_question_button($slot) {
// If 'reattemptgradedquestions' field is not set, do not display the 'Restart question' button.
if (!$this->get_quiz()->reattemptgradedquestions) {
return;
$questiondata = $DB->get_record('question',
array('id' => $this->slots[$slot]->questionid));
if ($questiondata->qtype != 'random') {
$newqusetionid = $questiondata->id;
} else {
$randomloader = new \core_question\bank\random_question_loader($qubaids, array());
$newqusetionid = $randomloader->get_next_question_id($questiondata->category,
(bool) $questiondata->questiontext);
if ($newqusetionid === null) {
throw new moodle_exception('notenoughrandomquestions', 'quiz',
$quizobj->view_url(), $questiondata);
}
}
$qa = $this->get_question_attempt($slot);
// If question is not graded, do not display the 'Restart question' button.
if (!$qa->get_state()->is_graded()) {
return;
$newquestion = question_bank::load_question($newqusetionid);
if ($newquestion->get_num_variants() == 1) {
$variant = 1;
} else {
$variantstrategy = new core_question\engine\variants\least_used_strategy(
$this->quba, $qubaids);
$variant = $variantstrategy->choose_variant($newquestion->get_num_variants(),
$newquestion->get_variants_selection_seed());
}
$buttonvalue = get_string('restartquestion', 'question');
return html_writer::tag('div',
"<input class='submit btn resatrt-question-btn' type='submit' value='$buttonvalue' name='restartquestioninslot'>
<input type='hidden' value='$slot' name='restartquestionincurrentslot'>");
$newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion);
$this->quba->start_question($slot);
$this->quba->set_max_mark($newslot, 0);
$this->quba->set_question_attempt_metadata($newslot, 'originalslot', $slot);
question_engine::save_questions_usage_by_activity($this->quba);
$transaction->allow_commit();
}
/**
......
......@@ -41,7 +41,7 @@ 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', 'timelimit',
'overduehandling', 'graceperiod', 'preferredbehaviour', 'attempts_number',
'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions', 'attempts_number',
'attemptonlast', 'grademethod', 'decimalpoints', 'questiondecimalpoints',
'reviewattempt', 'reviewcorrectness', 'reviewmarks',
'reviewspecificfeedback', 'reviewgeneralfeedback',
......
......@@ -17,7 +17,7 @@
<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'."/>
<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."/>
<FIELD NAME="preferredbehaviour" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false" COMMENT="The behaviour to ask questions to use."/>
<FIELD NAME="reattemptgradedquestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the graded questions in 'Immediate feedback', 'Immediate feedback with CBM' and 'Interactive with multiple tries behaviours' behaviours can be reattempted within a quiz attempt."/>
<FIELD NAME="canredoquestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Allows students to redo any completed question within a quiz attempt."/>
<FIELD NAME="attempts" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The maximum number of attempts a student is allowed."/>
<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)."/>
<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."/>
......
......@@ -822,9 +822,9 @@ function xmldb_quiz_upgrade($oldversion) {
}
if ($oldversion < 2015030900) {
// Define field reattemptgradedquestions to be added to quiz.
// Define field canredoquestions to be added to quiz.
$table = new xmldb_table('quiz');
$field = new xmldb_field('reattemptgradedquestions', XMLDB_TYPE_INTEGER, '4', null, null, null, 0, 'completionpass');
$field = new xmldb_field('canredoquestions', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, 0, 'preferredbehaviour');
// Conditionally launch add field completionpass.
if (!$dbman->field_exists($table, $field)) {
......
......@@ -140,6 +140,14 @@ $string['cannotstartgradesmismatch'] = 'Cannot start an attempt at this quiz. Th
$string['cannotstartmissingquestion'] = 'Cannot start an attempt at this quiz. The quiz definition includes a question that does not exist.';
$string['cannotstartnoquestions'] = 'Cannot start an attempt at this quiz. The quiz has not been set up yet. No questions have been added.';
$string['cannotwrite'] = 'Cannot write to export file ({$a})';
$string['canredoquestions'] = 'Allow redo within an attempt';
$string['canredoquestions_desc'] = 'If enabled, then when students have finished attempting particular question, they will see a Redo question button. This allows them to attempt another version of the same question, without having to submit the entire quiz attempt and start another one. This option is mainly useful for practice quizzes.
This setting only affects questions (for example not Essay questions) and behaviours (for example Immediate feedback, or Interactive with multiple tries) where it is possible for student to finish the question before the attempt is submitted.';
$string['canredoquestions_help'] = 'If enabled, then when students have finished attempting particular question, they will see a Redo question button. This allows them to attempt another version of the same question, without having to submit the entire quiz attempt and start another one. This option is mainly useful for practice quizzes.
This setting only affects questions (for example not Essay questions) and behaviours (for example Immediate feedback, or Interactive with multiple tries) where it is possible for student to finish the question before the attempt is submitted.';
$string['canredoquestionsyes'] = 'Students may redo another version of any finished question';
$string['caseno'] = 'No, case is unimportant';
$string['casesensitive'] = 'Case sensitivity';
$string['caseyes'] = 'Yes, case must match';
......@@ -180,7 +188,6 @@ $string['configpenaltyscheme'] = 'Penalty subtracted for each wrong response in
$string['configpopup'] = 'Force the attempt to open in a popup window, and use JavaScript tricks to try to restrict copy and paste, etc. during quiz attempts.';
$string['configrequirepassword'] = 'Students must enter this password before they can attempt the quiz.';
$string['configrequiresubnet'] = 'Students can only attempt the quiz from these computers.';
$string['configrestartgradedquestions'] = 'If enabled, it allows students to restart graded questions in \'Immediate feedback\', \'Immediate feedback with CBM\' and \'Interactive with multiple tries behaviours\'';
$string['configreviewoptions'] = 'These options control what information users can see when they review a quiz attempt or look at the quiz reports.';
$string['configshowblocks'] = 'Show blocks during quiz attempts.';
$string['configshowuserpicture'] = 'Show the user\'s picture on screen during attempts.';
......@@ -449,6 +456,7 @@ $string['manualgrading'] = 'Grading';
$string['mark'] = 'Submit';
$string['markall'] = 'Submit page';
$string['marks'] = 'Marks';
$string['marks_help'] = 'The numerical marks for each question, and the overall attempt score.';
$string['match'] = 'Matching';
$string['matchanswer'] = 'Matching answer';
$string['matchanswerno'] = 'Matching answer {$a}';
......@@ -689,6 +697,8 @@ $string['readytosend'] = 'You are about to send your whole quiz to be graded. A
$string['reattemptquiz'] = 'Re-attempt quiz';
$string['recentlyaddedquestion'] = 'Recently added question!';
$string['recurse'] = 'Include questions from subcategories too';
$string['redoquestion'] = 'Redo question';
$string['redoesofthisquestion'] = 'Other questions attempted here: {$a}';
$string['regrade'] = 'Regrade all attempts';
$string['regradecomplete'] = 'All attempts have been regraded';
$string['regradecount'] = '{$a->changed} out of {$a->attempt} grades were changed';
......@@ -753,10 +763,6 @@ $string['reviewbefore'] = 'Allow review while quiz is open';
$string['reviewclosed'] = 'After the quiz is closed';
$string['reviewduring'] = 'During the attempt';
$string['reviewimmediately'] = 'Immediately after the attempt';
$string['marks'] = 'Marks';
$string['marks_help'] = 'The numerical marks for each question, and the overall attempt score.';
$string['restartgradedquestions'] = 'Restart graded questions';
$string['restartgradedquestions_help'] = 'If enabled, it allows students to restart graded questions in \'Immediate feedback\', \'Immediate feedback with CBM\' and \'Interactive with multiple tries behaviours\'';
$string['reviewnever'] = 'Never allow review';
$string['reviewofattempt'] = 'Review of attempt {$a}';
$string['reviewofpreview'] = 'Review of preview';
......
......@@ -205,17 +205,16 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->addHelpButton('preferredbehaviour', 'howquestionsbehave', 'question');
$mform->setDefault('preferredbehaviour', $quizconfig->preferredbehaviour);
// TODO: Store the 'reattemptgradedquestions' field when new DB structure in place.
$mform->addElement('selectyesno', 'reattemptgradedquestions', get_string('restartgradedquestions', 'quiz'));
$mform->addHelpButton('reattemptgradedquestions', 'restartgradedquestions', 'quiz');
$mform->setAdvanced('reattemptgradedquestions', $quizconfig->reattemptgradedquestions_adv);
$mform->setDefault('reattemptgradedquestions', $quizconfig->reattemptgradedquestions);
// Can redo completed questions.
$redochoices = array(0 => get_string('no'), 1 => get_string('canredoquestionsyes', 'quiz'));
$mform->addElement('select', 'canredoquestions', get_string('canredoquestions', 'quiz'), $redochoices);
$mform->addHelpButton('canredoquestions', 'canredoquestions', 'quiz');
$mform->setAdvanced('canredoquestions', $quizconfig->canredoquestions_adv);
$mform->setDefault('canredoquestions', $quizconfig->canredoquestions);
foreach ($behaviours as $behaviour => $notused) {
$qbt = question_engine::get_behaviour_type($behaviour);
if (!$qbt->user_can_reattempt_graded_question()) {
$mform->disabledIf('reattemptgradedquestions', 'preferredbehaviour', 'eq', $behaviour);
if (!question_engine::can_questions_finish_during_the_attempt($behaviour)) {
$mform->disabledIf('canredoquestions', 'preferredbehaviour', 'eq', $behaviour);
}
}
// Each attempt builds on last.
......
......@@ -44,12 +44,6 @@ $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);
// Process replace question action, when user press on 'Replace question' link.
if (isset($_POST['restartquestioninslot'])) {
redirect(new moodle_url('/mod/quiz/attempt.php',
array('attempt' => $attemptid, 'page' => $thispage, 'replacequestioninslot' => $_POST['restartquestionincurrentslot'])));
}
$transaction = $DB->start_delegated_transaction();
$attemptobj = quiz_attempt::create($attemptid);
......@@ -150,6 +144,14 @@ if (!$finishattempt) {
$attemptobj->attempt_url(null, $thispage), $e->getMessage(), $debuginfo);
}
if (!$becomingoverdue) {
foreach ($attemptobj->get_slots() as $slot) {
if (optional_param('redoslot' . $slot, false, PARAM_BOOL)) {
$attemptobj->process_redo_question($slot, $timenow);
}
}
}
} else {
// The student is too late.
$attemptobj->process_going_overdue($timenow, true);
......
......@@ -79,9 +79,9 @@ class mod_quiz_renderer extends plugin_renderer_base {
$output .= $this->review_summary_table($summarydata, 0);
if (!is_null($seq)) {
$output .= $attemptobj->render_question_at_step($slot, $seq, true);
$output .= $attemptobj->render_question_at_step($slot, $seq, true, $this);
} else {
$output .= $attemptobj->render_question($slot, true);
$output .= $attemptobj->render_question($slot, true, $this);
}
$output .= $this->close_window_button();
......@@ -182,7 +182,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
mod_quiz_display_options $displayoptions) {
$output = '';
foreach ($slots as $slot) {
$output .= $attemptobj->render_question($slot, $reviewing,
$output .= $attemptobj->render_question($slot, $reviewing, $this,
$attemptobj->review_url($slot, $page, $showall));
}
return $output;
......@@ -382,10 +382,12 @@ class mod_quiz_renderer extends plugin_renderer_base {
mod_quiz_links_to_other_attempts $links) {
$attemptlinks = array();
foreach ($links->links as $attempt => $url) {
if ($url) {
$attemptlinks[] = html_writer::link($url, $attempt);
} else {
if (!$url) {
$attemptlinks[] = html_writer::tag('strong', $attempt);
} else if ($url instanceof renderable) {
$attemptlinks[] = $this->render($url);
} else {
$attemptlinks[] = html_writer::link($url, $attempt);
}
}
return implode(', ', $attemptlinks);
......@@ -459,9 +461,8 @@ class mod_quiz_renderer extends plugin_renderer_base {
// Print all the questions.
foreach ($slots as $slot) {
$output .= $attemptobj->render_question($slot, false,
$attemptobj->attempt_url($slot, $page));
$output .= $attemptobj->restart_question_button($slot);
$output .= $attemptobj->render_question($slot, false, $this,
$attemptobj->attempt_url($slot, $page), $this);
}
$output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
......@@ -487,7 +488,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
// if you navigate before the form has finished loading, it does not wipe all
// the student's answers.
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
'value' => implode(',', $slots)));
'value' => implode(',', $attemptobj->get_active_slots($page))));
// Finish the form.
$output .= html_writer::end_tag('div');
......@@ -498,6 +499,22 @@ class mod_quiz_renderer extends plugin_renderer_base {
return $output;
}
/**
* Render a button which allows students to redo a question in the attempt.
*
* @param int $slot the number of the slot to generate the button for.
* @param bool $disabled if true, output the button disabled.
* @return string HTML fragment.
*/
public function redo_question_button($slot, $disabled) {
$attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot,
'value' => get_string('redoquestion', 'quiz'), 'class' => 'mod_quiz-redo_question_button');
if ($disabled) {