From 8a972ab3771d97ab6e1dcd7dfb37ac47bd16635c Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Wed, 20 Apr 2016 11:15:21 +0200 Subject: [PATCH 1/3] MDL-53870 mod_quiz: New settings for quiz attempts in the mobile app --- mod/quiz/attemptlib.php | 18 +++++++++++++ .../backup/moodle2/backup_quiz_stepslib.php | 5 ++-- .../backup/moodle2/restore_quiz_stepslib.php | 1 + mod/quiz/classes/external.php | 14 ++++++++-- mod/quiz/db/install.xml | 2 ++ mod/quiz/db/upgrade.php | 27 +++++++++++++++++++ mod/quiz/locallib.php | 8 +++++- mod/quiz/tests/external_test.php | 3 ++- mod/quiz/version.php | 2 +- 9 files changed, 73 insertions(+), 7 deletions(-) diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index a774cf4d4ce..578144bd7d0 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -2320,6 +2320,24 @@ class quiz_attempt { $event->trigger(); } + /** + * Update the timemodifiedoffline attempt field. + * This function should be used only when web services are being used. + * + * @param int $time time stamp + * @return boolean false if the field is not updated becase web services aren't being used. + * @since Moodle 3.2 + */ + public function set_offline_modified_time($time) { + global $DB; + + // Update the timemodifiedoffline field only if web services are being used. + if (WS_SERVER) { + $attemptobj->attempt->timemodifiedoffline = $time; + } + return false; + } + } diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index 10ada061ba7..59b3b67013a 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -49,7 +49,8 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru 'questionsperpage', 'navmethod', 'shuffleanswers', 'sumgrades', 'grade', 'timecreated', 'timemodified', 'password', 'subnet', 'browsersecurity', - 'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass')); + 'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass', + 'allowofflineattempts')); // Define elements for access rule subplugin settings. $this->add_subplugin_structure('quizaccess', $quiz, true); @@ -84,7 +85,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru $attempt = new backup_nested_element('attempt', array('id'), array( 'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview', - 'state', 'timestart', 'timefinish', 'timemodified', 'timecheckstate', 'sumgrades')); + 'state', 'timestart', 'timefinish', 'timemodified', 'timemodifiedoffline', 'timecheckstate', 'sumgrades')); // This module is using questions, so produce the related question states and sessions // attaching them to the $attempt element based in 'uniqueid' matching. diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index c8367e3f6ed..5c1cc9abd4e 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -375,6 +375,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $data->timestart = $this->apply_date_offset($data->timestart); $data->timefinish = $this->apply_date_offset($data->timefinish); $data->timemodified = $this->apply_date_offset($data->timemodified); + if (!empty($data->timecheckstate)) { $data->timecheckstate = $this->apply_date_offset($data->timecheckstate); } else { diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 2b7115573b7..0465a430565 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -131,7 +131,8 @@ class mod_quiz_external extends external_api { 'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade', 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass', 'overduehandling', - 'graceperiod', 'preferredbehaviour', 'canredoquestions'); + 'graceperiod', 'preferredbehaviour', 'canredoquestions', + 'allowofflineattempts'); $viewablefields = array_merge($viewablefields, $additionalfields); } @@ -258,6 +259,8 @@ class mod_quiz_external extends external_api { exhausted the maximum number of attempts', VALUE_OPTIONAL), 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL), + 'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted + offline in the mobile app', VALUE_OPTIONAL), 'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL), 'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text', VALUE_OPTIONAL), @@ -445,6 +448,7 @@ class mod_quiz_external extends external_api { 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted. 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL), + 'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL), 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for state changes. NULL means never check.', VALUE_OPTIONAL), 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL), @@ -748,7 +752,8 @@ class mod_quiz_external extends external_api { throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress'); } } - $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt); + $offlineattempt = WS_SERVER ? true : false; + $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt); } $result = array(); @@ -1136,6 +1141,8 @@ class mod_quiz_external extends external_api { $_POST[$element['name']] = $element['value']; } $timenow = time(); + // Update the timemodifiedoffline field. + $attemptobj->set_offline_modified_time($timenow); $attemptobj->process_auto_save($timenow); $transaction->allow_commit(); @@ -1231,7 +1238,10 @@ class mod_quiz_external extends external_api { $timeup = $params['timeup']; $result = array(); + // Update the timemodifiedoffline field. + $attemptobj->set_offline_modified_time($timenow); $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0); + $result['warnings'] = $warnings; return $result; } diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml index decceadc6fb..d41ecbc55cf 100644 --- a/mod/quiz/db/install.xml +++ b/mod/quiz/db/install.xml @@ -46,6 +46,7 @@ + @@ -136,6 +137,7 @@ + diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index f42c0072f83..6c122723a43 100644 --- a/mod/quiz/db/upgrade.php +++ b/mod/quiz/db/upgrade.php @@ -193,5 +193,32 @@ function xmldb_quiz_upgrade($oldversion) { // Moodle v3.1.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2016092000) { + // Define new fields to be added to quiz. + $table = new xmldb_table('quiz'); + + $field = new xmldb_field('allowofflineattempts', XMLDB_TYPE_INTEGER, '1', null, null, null, 0, 'completionpass'); + // Conditionally launch add field allowofflineattempts. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + // Quiz savepoint reached. + upgrade_mod_savepoint(true, 2016092000, 'quiz'); + } + + if ($oldversion < 2016092001) { + // New field for quiz_attemps. + $table = new xmldb_table('quiz_attempts'); + + $field = new xmldb_field('timemodifiedoffline', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0, 'timemodified'); + // Conditionally launch add field timemodifiedoffline. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Quiz savepoint reached. + upgrade_mod_savepoint(true, 2016092001, 'quiz'); + } + return true; } diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index fcd70f8a560..d6f403317b2 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -123,6 +123,7 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen $attempt->timestart = $timenow; $attempt->timefinish = 0; $attempt->timemodified = $timenow; + $attempt->timemodifiedoffline = 0; $attempt->state = quiz_attempt::IN_PROGRESS; $attempt->currentpage = 0; $attempt->sumgrades = null; @@ -2259,10 +2260,11 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman * @param quiz $quizobj quiz object * @param int $attemptnumber the attempt number * @param object $lastattempt last attempt object + * @param bool $offlineattempt whether is an offline attempt or not * @return object the new attempt * @since Moodle 3.1 */ -function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt) { +function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false) { global $DB, $USER; // Delete any previous preview attempts belonging to this user. @@ -2283,6 +2285,10 @@ function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $last $transaction = $DB->start_delegated_transaction(); + // Init the timemodifiedoffline for offline attempts. + if ($offlineattempt) { + $attempt->timemodifiedoffline = $attempt->timemodified; + } $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); $transaction->allow_commit(); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 9aa209efa6d..9b3472ae930 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -199,7 +199,8 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { 'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade', 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions', - 'hasfeedback', 'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions'); + 'hasfeedback', 'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions', + 'allowofflineattempts'); $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet'); // Add expected coursemodule and other data. diff --git a/mod/quiz/version.php b/mod/quiz/version.php index a2ce9496e4c..89938c9f071 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016052300; +$plugin->version = 2016092001; $plugin->requires = 2016051900; $plugin->component = 'mod_quiz'; $plugin->cron = 60; From 5635ead8e6354e7a89ef72f01a7af5d94e8f3f3a Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 26 Apr 2016 11:25:45 +0200 Subject: [PATCH 2/3] MDL-53870 mod_quiz: New access rule for allowing offline attempts --- lib/classes/plugin_manager.php | 2 +- .../lang/en/quizaccess_offlineattempts.php | 35 +++++ mod/quiz/accessrule/offlineattempts/rule.php | 139 ++++++++++++++++++ .../offlineattempts/tests/rule_test.php | 54 +++++++ .../accessrule/offlineattempts/version.php | 29 ++++ 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php create mode 100644 mod/quiz/accessrule/offlineattempts/rule.php create mode 100644 mod/quiz/accessrule/offlineattempts/tests/rule_test.php create mode 100644 mod/quiz/accessrule/offlineattempts/version.php diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 53e8ec1d5ed..c5bf07df475 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1866,7 +1866,7 @@ class core_plugin_manager { ), 'quizaccess' => array( - 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate', + 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate', 'password', 'safebrowser', 'securewindow', 'timelimit' ), diff --git a/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php b/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php new file mode 100644 index 00000000000..8c79287f6d6 --- /dev/null +++ b/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php @@ -0,0 +1,35 @@ +. + +/** + * Strings for the quizaccess_offlineattempts plugin. + * + * @package quizaccess_offlineattempts + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['allowofflineattempts'] = 'Allow quiz to be attempted offline in the mobile app'; +$string['allowofflineattempts_help'] = 'If checked, the user will be able to download the quiz to attempt it offline using the Mobile app. If the user download a quiz for offline, a new attempt will be created. This attempt will be empty until the user synchronize the results of the attempt in the Mobile app.'; +$string['confirmdatasaved'] = 'I confirm that I don’t have unsaved work on my mobile devices before continuing this attempt.'; +$string['mobileapp'] = 'Mobile app'; +$string['offlineattemptserror'] = 'Offline quizzes are not compatible with quizzes using timers, access restriction by password or subnet and quizzes using behaviours different than deferred feedback without or with CBM'; +$string['offlinedatamessage'] = 'You have worked on this attempt in a mobile device, and that data was last saved to this site {$a} ago.'; +$string['pleaseconfirm'] = 'Please, confirm that you don\'t have unsaved work on your devices'; +$string['pluginname'] = 'Offline attempts access rule'; + diff --git a/mod/quiz/accessrule/offlineattempts/rule.php b/mod/quiz/accessrule/offlineattempts/rule.php new file mode 100644 index 00000000000..511be30f145 --- /dev/null +++ b/mod/quiz/accessrule/offlineattempts/rule.php @@ -0,0 +1,139 @@ +. + +/** + * Implementaton of the quizaccess_offlineattempts plugin. + * + * @package quizaccess_offlineattempts + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); + +/** + * A rule implementing the offlineattempts check. + * + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.2 + */ +class quizaccess_offlineattempts extends quiz_access_rule_base { + + public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + global $CFG; + + // If mobile services are off, the user won't be able to use any external app. + if (empty($CFG->enablemobilewebservice) or empty($quizobj->get_quiz()->allowofflineattempts)) { + return null; + } + + return new self($quizobj, $timenow); + } + + public function is_preflight_check_required($attemptid) { + global $SESSION, $DB; + + // First, check if the user did something offline. + if (!empty($attemptid)) { + $timemodifiedoffline = $DB->get_field('quiz_attempts', 'timemodifiedoffline', array('id' => $attemptid)); + if (empty($timemodifiedoffline)) { + return false; + } + return empty($SESSION->offlineattemptscheckedquizzes[$this->quiz->id]); + } else { + // Starting a new attempt, we don't have to check anything here. + return false; + } + } + + public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform, + MoodleQuickForm $mform, $attemptid) { + global $DB; + + $timemodifiedoffline = $DB->get_field('quiz_attempts', 'timemodifiedoffline', array('id' => $attemptid)); + $lasttime = format_time(time() - $timemodifiedoffline); + + $mform->addElement('header', 'offlineattemptsheader', get_string('mobileapp', 'quizaccess_offlineattempts')); + $mform->addElement('static', 'offlinedatamessage', '', + get_string('offlinedatamessage', 'quizaccess_offlineattempts', $lasttime)); + $mform->addElement('advcheckbox', 'confirmdatasaved', null, + get_string('confirmdatasaved', 'quizaccess_offlineattempts')); + } + + public function validate_preflight_check($data, $files, $errors, $attemptid) { + + // The user confirmed that he doesn't have unsaved work. + if (!empty($data['confirmdatasaved'])) { + return $errors; + } + + $errors['confirmdatasaved'] = get_string('pleaseconfirm', 'quizaccess_offlineattempts'); + return $errors; + } + + public function notify_preflight_check_passed($attemptid) { + global $SESSION; + $SESSION->offlineattemptscheckedquizzes[$this->quiz->id] = true; + } + + public function current_attempt_finished() { + global $SESSION; + // Clear the flag in the session that says that the user has already agreed to the notice. + if (!empty($SESSION->offlineattemptscheckedquizzes[$this->quiz->id])) { + unset($SESSION->offlineattemptscheckedquizzes[$this->quiz->id]); + } + } + + public static function add_settings_form_fields( + mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { + global $CFG; + + // Allow to enable the access rule only if the Mobile services are enabled. + if ($CFG->enablemobilewebservice) { + $mform->addElement('selectyesno', 'allowofflineattempts', + get_string('allowofflineattempts', 'quizaccess_offlineattempts')); + $mform->addHelpButton('allowofflineattempts', 'allowofflineattempts', 'quizaccess_offlineattempts'); + $mform->setDefault('allowofflineattempts', 0); + $mform->setAdvanced('allowofflineattempts'); + $mform->disabledIf('allowofflineattempts', 'timelimit[number]', 'neq', 0); + $mform->disabledIf('allowofflineattempts', 'subnet', 'neq', ''); + } + } + + public static function validate_settings_form_fields(array $errors, + array $data, $files, mod_quiz_mod_form $quizform) { + global $CFG; + + if ($CFG->enablemobilewebservice) { + // Do not allow offline attempts if: + // - The quiz uses a timer. + // - The quiz is restricted by subnet. + // - The question behaviour is not deferred feedback or deferred feedback with CBM. + if (!empty($data['allowofflineattempts']) and + (!empty($data['timelimit']) or !empty($data['subnet']) or + ($data['preferredbehaviour'] != 'deferredfeedback' and $data['preferredbehaviour'] != 'deferredcbm'))) { + + $errors['allowofflineattempts'] = get_string('offlineattemptserror', 'quizaccess_offlineattempts'); + } + } + + return $errors; + } +} diff --git a/mod/quiz/accessrule/offlineattempts/tests/rule_test.php b/mod/quiz/accessrule/offlineattempts/tests/rule_test.php new file mode 100644 index 00000000000..ba3856aa2c2 --- /dev/null +++ b/mod/quiz/accessrule/offlineattempts/tests/rule_test.php @@ -0,0 +1,54 @@ +. + +/** + * Unit tests for the quizaccess_offlineattempts plugin. + * + * @package quizaccess_offlineattempts + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quiz/accessrule/offlineattempts/rule.php'); + + +/** + * Unit tests for the quizaccess_offlineattempts plugin. + * + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quizaccess_offlineattempts_testcase extends basic_testcase { + public function test_offlineattempts_access_rule() { + $quiz = new stdClass(); + $quiz->allowofflineattempts = 1; + $cm = new stdClass(); + $cm->id = 0; + $quizobj = new quiz($quiz, $cm, null); + $rule = new quizaccess_offlineattempts($quizobj, 0); + $attempt = new stdClass(); + + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->end_time($attempt)); + $this->assertFalse($rule->time_left_display($attempt, 0)); + } +} diff --git a/mod/quiz/accessrule/offlineattempts/version.php b/mod/quiz/accessrule/offlineattempts/version.php new file mode 100644 index 00000000000..437eb774e28 --- /dev/null +++ b/mod/quiz/accessrule/offlineattempts/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version information for the quizaccess_offlineattempts plugin. + * + * @package quizaccess_offlineattempts + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016042600; +$plugin->requires = 2016042200; +$plugin->component = 'quizaccess_offlineattempts'; From 74804b57a4e7c8b88039ba6da33b09f4831488f8 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 26 Apr 2016 12:13:14 +0200 Subject: [PATCH 3/3] MDL-53870 mod_quiz: Return sequence and time information for questions --- mod/quiz/classes/external.php | 10 ++++++++- mod/quiz/tests/external_test.php | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 0465a430565..03b1bdf2591 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -877,6 +877,11 @@ class mod_quiz_external extends external_api { 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'), 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'), 'html' => new external_value(PARAM_RAW, 'the question rendered'), + 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL), + 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt', + VALUE_OPTIONAL), + 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data', + VALUE_OPTIONAL), 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'), 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL), 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL), @@ -912,7 +917,10 @@ class mod_quiz_external extends external_api { 'type' => $attemptobj->get_question_type_name($slot), 'page' => $attemptobj->get_question_page($slot), 'flagged' => $attemptobj->is_question_flagged($slot), - 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code() + 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(), + 'sequencecheck' => $attemptobj->get_question_attempt($slot)->get_sequence_check_count(), + 'lastactiontime' => $attemptobj->get_question_attempt($slot)->get_last_step()->get_timecreated(), + 'hasautosavedstep' => $attemptobj->get_question_attempt($slot)->has_autosaved_step() ); if ($attemptobj->is_real_question($slot)) { diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index 9b3472ae930..97f51e72512 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -851,6 +851,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { public function test_get_attempt_data() { global $DB; + $timenow = time(); // Create a new quiz with one attempt started. list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); @@ -878,6 +879,9 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertEquals(0, $result['questions'][0]['page']); $this->assertEmpty($result['questions'][0]['mark']); $this->assertEquals(1, $result['questions'][0]['maxmark']); + $this->assertEquals(1, $result['questions'][0]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); // Now try the last page. $result = mod_quiz_external::get_attempt_data($attempt->id, 1); @@ -894,6 +898,9 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); $this->assertFalse($result['questions'][0]['flagged']); $this->assertEquals(1, $result['questions'][0]['page']); + $this->assertEquals(1, $result['questions'][0]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); // Finish previous attempt. $attemptobj->process_finish(time(), false); @@ -982,6 +989,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { */ public function test_get_attempt_summary() { + $timenow = time(); // Create a new quiz with one attempt started. list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); @@ -998,6 +1006,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertFalse($result['questions'][1]['flagged']); $this->assertEmpty($result['questions'][0]['mark']); $this->assertEmpty($result['questions'][1]['mark']); + $this->assertEquals(1, $result['questions'][0]['sequencecheck']); + $this->assertEquals(1, $result['questions'][1]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); + $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); // Submit a response for the first question. $tosubmit = array(1 => array('answer' => '3.14')); @@ -1014,6 +1028,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertFalse($result['questions'][1]['flagged']); $this->assertEmpty($result['questions'][0]['mark']); $this->assertEmpty($result['questions'][1]['mark']); + $this->assertEquals(2, $result['questions'][0]['sequencecheck']); + $this->assertEquals(1, $result['questions'][1]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); + $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); } @@ -1022,6 +1042,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { */ public function test_save_attempt() { + $timenow = time(); // Create a new quiz with one attempt started. list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); @@ -1053,6 +1074,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertFalse($result['questions'][1]['flagged']); $this->assertEmpty($result['questions'][0]['mark']); $this->assertEmpty($result['questions'][1]['mark']); + $this->assertEquals(1, $result['questions'][0]['sequencecheck']); + $this->assertEquals(1, $result['questions'][1]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); + $this->assertEquals(true, $result['questions'][0]['hasautosavedstep']); + $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); // Now, second slot. $prefix = $quba->get_field_prefix(2); @@ -1073,7 +1100,9 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { // Check it's marked as completed only the first one. $this->assertEquals('complete', $result['questions'][0]['state']); + $this->assertEquals(1, $result['questions'][0]['sequencecheck']); $this->assertEquals('complete', $result['questions'][1]['state']); + $this->assertEquals(1, $result['questions'][1]['sequencecheck']); } @@ -1083,6 +1112,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { public function test_process_attempt() { global $DB; + $timenow = time(); // Create a new quiz with two questions and one attempt started. list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); @@ -1114,6 +1144,12 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->assertFalse($result['questions'][1]['flagged']); $this->assertEmpty($result['questions'][0]['mark']); $this->assertEmpty($result['questions'][1]['mark']); + $this->assertEquals(2, $result['questions'][0]['sequencecheck']); + $this->assertEquals(2, $result['questions'][0]['sequencecheck']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); + $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); // Now, second slot. $prefix = $quba->get_field_prefix(2);