Commit 1700bd4d authored by M Kassaei's avatar M Kassaei Committed by Tim Hunt
Browse files

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

parent bb93fc24
......@@ -397,6 +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';
......@@ -436,6 +437,7 @@ $string['unknownquestion'] = 'Unknown question: {$a}.';
$string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
$string['unknownquestiontype'] = 'Unknown question type: {$a}.';
$string['unusedcategorydeleted'] = 'This category has been deleted because, after deleting the course, its questions weren\'t used any more.';
$string['updatedisplayoptions'] = 'Update display options';
$string['whethercorrect'] = 'Whether correct';
$string['whethercorrect_help'] = 'This covers both the textual description \'Correct\', \'Partially correct\' or \'Incorrect\', and any coloured highlighting that conveys the same information.';
$string['whichtries'] = 'Which tries';
......
......@@ -125,6 +125,12 @@ 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());
......
......@@ -1527,6 +1527,44 @@ class quiz_attempt {
$transaction->allow_commit();
}
/**
* Process replace question action
* @param int $slot
* @param int $timestamp
*/
public function process_replace_question_actions($slot, $timestamp) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$this->quba->replace_question($slot);
question_engine::save_questions_usage_by_activity($this->quba);
$transaction->allow_commit();
}
/**
* 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;
}
$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;
}
$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'>");
}
/**
* Process all the autosaved data that was part of the current request.
*
......
......@@ -17,6 +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="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."/>
......
......@@ -821,5 +821,19 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2015030500, 'quiz');
}
if ($oldversion < 2015030900) {
// Define field reattemptgradedquestions to be added to quiz.
$table = new xmldb_table('quiz');
$field = new xmldb_field('reattemptgradedquestions', XMLDB_TYPE_INTEGER, '4', null, null, null, 0, 'completionpass');
// Conditionally launch add field completionpass.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2015030900, 'quiz');
}
return true;
}
......@@ -180,6 +180,7 @@ $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.';
......@@ -754,6 +755,8 @@ $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,6 +205,19 @@ 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);
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);
}
}
// Each attempt builds on last.
$mform->addElement('selectyesno', 'attemptonlast',
get_string('eachattemptbuildsonthelast', 'quiz'));
......
......@@ -44,6 +44,12 @@ $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);
......
......@@ -461,6 +461,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
foreach ($slots as $slot) {
$output .= $attemptobj->render_question($slot, false,
$attemptobj->attempt_url($slot, $page));
$output .= $attemptobj->restart_question_button($slot);
}
$output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
......
......@@ -128,6 +128,11 @@ if ($ADMIN->fulltree) {
get_string('howquestionsbehave', 'question'), get_string('howquestionsbehave_desc', 'quiz'),
'deferredfeedback'));
// Restart completed questions (reattemptgradedquestions).
$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/reattemptgradedquestions',
get_string('restartgradedquestions', 'quiz'), get_string('configrestartgradedquestions', 'quiz'),
array('value' => 0, 'adv' => true)));
// Each attempt builds on last.
$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/attemptonlast',
get_string('eachattemptbuildsonthelast', 'quiz'),
......
......@@ -24,6 +24,10 @@
text-align: right;
}
#page-mod-quiz-attempt .resatrt-question-btn {
font-size: 0.75em;
}
#page-mod-quiz-attempt .mod_quiz-blocked_question_warning .que .formulation,
#page-mod-quiz-review .mod_quiz-blocked_question_warning .que .formulation {
background: #eee;
......
@mod @mod_quiz
Feature: Add a quiz
In order to allow students re-attempting graded question
As a teacher
I need to create a quiz, set 'Restart question' field to 'Yes', add questions to the quiz which can be graded automatically.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
| student1 | S1 | Student1 | student1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
When I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
And I add a "Quiz" to section "1" and I fill the form with:
| Name | Quiz 1 |
| Description | Quiz 1 description |
| How questions behave | Immediate feedback |
| Restart graded questions | Yes |
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | TF001 |
| Question text | Answer question TF001 |
| General feedback | Thank you, this is the general feedback |
| Correct answer | False |
| Feedback for the response 'True'. | So you think it is true |
| Feedback for the response 'False'. | So you think it is false |
And I log out
@javascript
Scenario: Log in as a student, attempt the quiz and checking whether you can re-attempt a graded question in the appropriate behaviour settings
And I log in as "student1"
And I follow "Course 1"
And I follow "Quiz 1"
And I press "Attempt quiz now"
Then I should see "TF001"
And I should see "Answer question TF001"
And I set the field "True" to "1"
And I press "Check"
And I should see "Incorrect"
Then I press "Restart question"
And I should see "Not complete"
And I set the field "False" to "1"
And I press "Check"
And I should see "Correct"
......@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2015030500;
$plugin->version = 2015030900;
$plugin->requires = 2014110400;
$plugin->component = 'mod_quiz';
$plugin->cron = 60;
......@@ -102,6 +102,14 @@ abstract class question_behaviour_type {
public function allows_multiple_submitted_responses() {
return false;
}
/**
* Allow user to reattmpt graded question during a quiz attempt
* @return boolean
*/
public function user_can_reattempt_graded_question() {
return false;
}
}
......
......@@ -39,4 +39,11 @@ class qbehaviour_immediatecbm_type extends qbehaviour_deferredcbm_type {
public function get_unused_display_options() {
return array();
}
/**
* Allow user to re-attempt graded questions during a quiz attempt
*/
public function user_can_reattempt_graded_question() {
return true;
}
}
......@@ -36,4 +36,11 @@ class qbehaviour_immediatefeedback_type extends question_behaviour_type {
public function is_archetypal() {
return true;
}
/**
* Allow user to re-attempt graded questions during a quiz attempt
*/
public function user_can_reattempt_graded_question() {
return true;
}
}
......@@ -40,4 +40,13 @@ class qbehaviour_interactive_type extends question_behaviour_type {
public function allows_multiple_submitted_responses() {
return true;
}
/**
* Allow user to re-attempt graded questions during a quiz attempt
*/
public function user_can_reattempt_graded_question() {
return true;
}
}
......@@ -880,6 +880,16 @@ ORDER BY
$this->db->update_record('question_attempts', $record);
}
/**
* Delete a question_attempts row to reflect any changes in a question_attempt
* (but not any of its steps).
* @param question_attempt $qa the question attempt that has been deleted.
*/
public function delete_question_attempt(question_attempt $qa) {
$conditions = array('questionusageid' => $qa->get_usage_id(), 'slot' => $qa->get_slot());
$this->db->delete_records('question_attempts', $conditions);
}
/**
* Delete a question_usage_by_activity and all its associated
*
......@@ -1250,6 +1260,12 @@ class question_engine_unit_of_work implements question_usage_observer {
*/
protected $attemptsmodified = array();
/**
* @var array list of slot => {@link question_attempt}s that
* were already in the usage, and which have been deleted.
*/
protected $attemptsdeleted = array();
/**
* @var array list of slot => {@link question_attempt}s that
* have been added to the usage.
......@@ -1293,8 +1309,28 @@ class question_engine_unit_of_work implements question_usage_observer {
}
}
/**
* Notify when attempt deleted
*
* @see question_usage_observer::notify_attempt_deleted()
*/
public function notify_attempt_deleted(question_attempt $qa) {
$slot = $qa->get_slot();
if (!array_key_exists($slot, $this->attemptsadded)) {
$this->attemptsdeleted[$slot] = $qa;
}
}
/**
* Notify when attempt added
*
* @see question_usage_observer::notify_attempt_added()
*/
public function notify_attempt_added(question_attempt $qa) {
$this->attemptsadded[$qa->get_slot()] = $qa;
$slot = $qa->get_slot();
if (!array_key_exists($slot, $this->attemptsadded)) {
$this->attemptsadded[$slot] = $qa;
}
}
public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
......@@ -1437,6 +1473,10 @@ class question_engine_unit_of_work implements question_usage_observer {
$step, $questionattemptid, $seq, $this->quba->get_owning_context());
}
foreach ($this->attemptsdeleted as $qa) {
$dm->delete_question_attempt($qa);
}
foreach ($this->attemptsadded as $qa) {
$stepdata[] = $dm->insert_question_attempt(
$qa, $this->quba->get_owning_context());
......@@ -1598,9 +1638,9 @@ class question_file_loader implements question_response_files {
protected $name;
/**
* @var string the value to stored in the question_attempt_step_data to
* @var string the value to stored in the question_attempt_step_data to
* represent these files.
*/
*/
protected $value;
/** @var int the context id that the files belong to. */
......
......@@ -822,6 +822,22 @@ class question_usage_by_activity {
$this->observer->notify_attempt_modified($newqa);
}
/**
* Replace a question in this usage.
* @param int $slot the number used to identify this question within this usage.*
*/
public function replace_question($slot) {
global $OUTPUT;
$oldqa = $this->get_question_attempt($slot);
$newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(), $this->observer);
$newqa->set_database_id($oldqa->get_database_id());
$newqa->set_slot($oldqa->get_slot());
$this->questionattempts[$slot] = $newqa;
$this->observer->notify_attempt_deleted($oldqa);
$this->observer->notify_attempt_added($newqa);
$this->start_question($slot);
}
/**
* Regrade all the questions in this usage (without changing their max mark).
* @param bool $finished whether each question should be forced to be finished
......@@ -979,6 +995,12 @@ interface question_usage_observer {
*/
public function notify_attempt_added(question_attempt $qa);
/**
* Called when the fields of a question attempt in this usage are deleted.
* @param question_attempt $qa
*/
public function notify_attempt_deleted(question_attempt $qa);
/**
* Called when a new step is added to a question attempt in this usage.
* @param question_attempt_step $step the new step.
......@@ -1017,6 +1039,8 @@ class question_usage_null_observer implements question_usage_observer {
}
public function notify_attempt_modified(question_attempt $qa) {
}
public function notify_attempt_deleted(question_attempt $qa) {
}
public function notify_attempt_added(question_attempt $qa) {
}
public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment