mirror of
https://github.com/moodle/moodle.git
synced 2025-05-31 21:19:49 +02:00
Merge branch 'MDL-52788-master' of git://github.com/jleyva/moodle
This commit is contained in:
commit
94ffadcdcd
@ -539,4 +539,24 @@ class quiz_access_manager {
|
||||
$this->attempt_must_be_in_popup(), $this->get_popup_options());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the preflight checks using the given data in all the rules supporting them.
|
||||
*
|
||||
* @param array $data passed data for validation
|
||||
* @param array $files un-used, Moodle seems to not support it anymore
|
||||
* @param int|null $attemptid the id of the current attempt, if there is one,
|
||||
* otherwise null.
|
||||
* @return array of errors, empty array means no erros
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public function validate_preflight_check($data, $files, $attemptid) {
|
||||
$errors = array();
|
||||
foreach ($this->rules as $rule) {
|
||||
if ($rule->is_preflight_check_required($attemptid)) {
|
||||
$errors = $rule->validate_preflight_check($data, $files, $errors, $attemptid);
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
@ -62,12 +62,9 @@ class mod_quiz_preflight_check_form extends moodleform {
|
||||
public function validation($data, $files) {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
foreach ($this->_customdata['rules'] as $rule) {
|
||||
if ($rule->is_preflight_check_required($this->_customdata['attemptid'])) {
|
||||
$errors = $rule->validate_preflight_check($data, $files, $errors,
|
||||
$this->_customdata['attemptid']);
|
||||
}
|
||||
}
|
||||
$timenow = time();
|
||||
$accessmanager = $this->_customdata['quizobj']->get_access_manager($timenow);
|
||||
$errors = array_merge($errors, $accessmanager->validate_preflight_check($data, $files, $this->_customdata['attemptid']));
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
@ -405,6 +405,40 @@ class mod_quiz_external extends external_api {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a single attempt structure.
|
||||
*
|
||||
* @return external_single_structure the attempt structure
|
||||
*/
|
||||
private static function attempt_structure() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
|
||||
'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
|
||||
VALUE_OPTIONAL),
|
||||
'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
|
||||
VALUE_OPTIONAL),
|
||||
'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
|
||||
VALUE_OPTIONAL),
|
||||
'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
|
||||
details of the the question_attempts that make up this quiz
|
||||
attempt.', VALUE_OPTIONAL),
|
||||
'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
|
||||
'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
|
||||
'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
|
||||
'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
|
||||
\'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
|
||||
'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
|
||||
'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),
|
||||
'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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the get_user_attempts return value.
|
||||
*
|
||||
@ -414,34 +448,7 @@ class mod_quiz_external extends external_api {
|
||||
public static function get_user_attempts_returns() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'attempts' => new external_multiple_structure(
|
||||
new external_single_structure(
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
|
||||
'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
|
||||
VALUE_OPTIONAL),
|
||||
'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
|
||||
VALUE_OPTIONAL),
|
||||
'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
|
||||
VALUE_OPTIONAL),
|
||||
'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
|
||||
details of the the question_attempts that make up this quiz
|
||||
attempt.', VALUE_OPTIONAL),
|
||||
'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
|
||||
'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
|
||||
'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
|
||||
'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
|
||||
\'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
|
||||
'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
|
||||
'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),
|
||||
'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),
|
||||
)
|
||||
)
|
||||
),
|
||||
'attempts' => new external_multiple_structure(self::attempt_structure()),
|
||||
'warnings' => new external_warnings(),
|
||||
)
|
||||
);
|
||||
@ -636,4 +643,135 @@ class mod_quiz_external extends external_api {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the parameters for start_attempt.
|
||||
*
|
||||
* @return external_external_function_parameters
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public static function start_attempt_parameters() {
|
||||
return new external_function_parameters (
|
||||
array(
|
||||
'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
|
||||
'preflightdata' => new external_multiple_structure(
|
||||
new external_single_structure(
|
||||
array(
|
||||
'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
|
||||
'value' => new external_value(PARAM_RAW, 'data value'),
|
||||
)
|
||||
), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
|
||||
),
|
||||
'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new attempt at a quiz.
|
||||
*
|
||||
* @param int $quizid quiz instance id
|
||||
* @param array $preflightdata preflight required data (like passwords)
|
||||
* @param bool $forcenew Whether to force a new attempt or not.
|
||||
* @return array of warnings and the attempt basic data
|
||||
* @since Moodle 3.1
|
||||
* @throws moodle_quiz_exception
|
||||
*/
|
||||
public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
|
||||
global $DB, $USER;
|
||||
|
||||
$warnings = array();
|
||||
$attempt = array();
|
||||
|
||||
$params = array(
|
||||
'quizid' => $quizid,
|
||||
'preflightdata' => $preflightdata,
|
||||
'forcenew' => $forcenew,
|
||||
);
|
||||
$params = self::validate_parameters(self::start_attempt_parameters(), $params);
|
||||
$forcenew = $params['forcenew'];
|
||||
|
||||
// Request and permission validation.
|
||||
$quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
|
||||
list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
self::validate_context($context);
|
||||
|
||||
$quizobj = quiz::create($cm->instance, $USER->id);
|
||||
|
||||
// Check questions.
|
||||
if (!$quizobj->has_questions()) {
|
||||
throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
|
||||
}
|
||||
|
||||
// Create an object to manage all the other (non-roles) access rules.
|
||||
$timenow = time();
|
||||
$accessmanager = $quizobj->get_access_manager($timenow);
|
||||
|
||||
// Validate permissions for creating a new attempt and start a new preview attempt if required.
|
||||
list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
|
||||
quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
|
||||
|
||||
// Check access.
|
||||
if (!$quizobj->is_preview_user() && $messages) {
|
||||
// Create warnings with the exact messages.
|
||||
foreach ($messages as $message) {
|
||||
$warnings[] = array(
|
||||
'item' => 'quiz',
|
||||
'itemid' => $quiz->id,
|
||||
'warningcode' => '1',
|
||||
'message' => clean_text($message, PARAM_TEXT)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($accessmanager->is_preflight_check_required($currentattemptid)) {
|
||||
// Need to do some checks before allowing the user to continue.
|
||||
|
||||
$provideddata = array();
|
||||
foreach ($params['preflightdata'] as $data) {
|
||||
$provideddata[$data['name']] = $data['value'];
|
||||
}
|
||||
|
||||
$errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new moodle_quiz_exception($quizobj, array_shift($errors));
|
||||
}
|
||||
|
||||
// Pre-flight check passed.
|
||||
$accessmanager->notify_preflight_check_passed($currentattemptid);
|
||||
}
|
||||
|
||||
if ($currentattemptid) {
|
||||
if ($lastattempt->state == quiz_attempt::OVERDUE) {
|
||||
throw new moodle_quiz_exception($quizobj, 'stateoverdue');
|
||||
} else {
|
||||
throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
|
||||
}
|
||||
}
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt);
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$result['attempt'] = $attempt;
|
||||
$result['warnings'] = $warnings;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the start_attempt return value.
|
||||
*
|
||||
* @return external_single_structure
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public static function start_attempt_returns() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'attempt' => self::attempt_structure(),
|
||||
'warnings' => new external_warnings(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -73,4 +73,13 @@ $functions = array(
|
||||
'capabilities' => 'mod/quiz:view',
|
||||
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
|
||||
),
|
||||
|
||||
'mod_quiz_start_attempt' => array(
|
||||
'classname' => 'mod_quiz_external',
|
||||
'methodname' => 'start_attempt',
|
||||
'description' => 'Starts a new attempt at a quiz.',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'mod/quiz:attempt',
|
||||
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
|
||||
),
|
||||
);
|
||||
|
@ -2151,5 +2151,126 @@ function quiz_view($quiz, $course, $cm, $context) {
|
||||
// Completion.
|
||||
$completion = new completion_info($course);
|
||||
$completion->set_module_viewed($cm);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate permissions for creating a new attempt and start a new preview attempt if required.
|
||||
*
|
||||
* @param quiz $quizobj quiz object
|
||||
* @param quiz_access_manager $accessmanager quiz access manager
|
||||
* @param bool $forcenew whether was required to start a new preview attempt
|
||||
* @param int $page page to jump to in the attempt
|
||||
* @param bool $redirect whether to redirect or throw exceptions (for web or ws usage)
|
||||
* @return array an array containing the attempt information, access error messages and the page to jump to in the attempt
|
||||
* @throws moodle_quiz_exception
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessmanager, $forcenew, $page, $redirect) {
|
||||
global $DB, $USER;
|
||||
$timenow = time();
|
||||
|
||||
if ($quizobj->is_preview_user() && $forcenew) {
|
||||
$accessmanager->current_attempt_finished();
|
||||
}
|
||||
|
||||
// Check capabilities.
|
||||
if (!$quizobj->is_preview_user()) {
|
||||
$quizobj->require_capability('mod/quiz:attempt');
|
||||
}
|
||||
|
||||
// Check to see if a new preview was requested.
|
||||
if ($quizobj->is_preview_user() && $forcenew) {
|
||||
// To force the creation of a new preview, we mark the current attempt (if any)
|
||||
// as finished. It will then automatically be deleted below.
|
||||
$DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED,
|
||||
array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id));
|
||||
}
|
||||
|
||||
// Look for an existing attempt.
|
||||
$attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true);
|
||||
$lastattempt = end($attempts);
|
||||
|
||||
$attemptnumber = null;
|
||||
// If an in-progress attempt exists, check password then redirect to it.
|
||||
if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS ||
|
||||
$lastattempt->state == quiz_attempt::OVERDUE)) {
|
||||
$currentattemptid = $lastattempt->id;
|
||||
$messages = $accessmanager->prevent_access();
|
||||
|
||||
// If the attempt is now overdue, deal with that.
|
||||
$quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true);
|
||||
|
||||
// And, if the attempt is now no longer in progress, redirect to the appropriate place.
|
||||
if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) {
|
||||
if ($redirect) {
|
||||
redirect($quizobj->review_url($lastattempt->id));
|
||||
} else {
|
||||
throw new moodle_quiz_exception($quizobj, 'attemptalreadyclosed');
|
||||
}
|
||||
}
|
||||
|
||||
// If the page number was not explicitly in the URL, go to the current page.
|
||||
if ($page == -1) {
|
||||
$page = $lastattempt->currentpage;
|
||||
}
|
||||
|
||||
} else {
|
||||
while ($lastattempt && $lastattempt->preview) {
|
||||
$lastattempt = array_pop($attempts);
|
||||
}
|
||||
|
||||
// Get number for the next or unfinished attempt.
|
||||
if ($lastattempt) {
|
||||
$attemptnumber = $lastattempt->attempt + 1;
|
||||
} else {
|
||||
$lastattempt = false;
|
||||
$attemptnumber = 1;
|
||||
}
|
||||
$currentattemptid = null;
|
||||
|
||||
$messages = $accessmanager->prevent_access() +
|
||||
$accessmanager->prevent_new_attempt(count($attempts), $lastattempt);
|
||||
|
||||
if ($page == -1) {
|
||||
$page = 0;
|
||||
}
|
||||
}
|
||||
return array($currentattemptid, $attemptnumber, $lastattempt, $messages, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and start a new attempt deleting the previous preview attempts.
|
||||
*
|
||||
* @param quiz $quizobj quiz object
|
||||
* @param int $attemptnumber the attempt number
|
||||
* @param object $lastattempt last attempt object
|
||||
* @return object the new attempt
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt) {
|
||||
global $DB, $USER;
|
||||
|
||||
// Delete any previous preview attempts belonging to this user.
|
||||
quiz_delete_previews($quizobj->get_quiz(), $USER->id);
|
||||
|
||||
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||
|
||||
// Create the new attempt and initialize the question sessions
|
||||
$timenow = time(); // Update time now, in case the server is running really slowly.
|
||||
$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user());
|
||||
|
||||
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
|
||||
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
|
||||
} else {
|
||||
$attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt);
|
||||
}
|
||||
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
$transaction->allow_commit();
|
||||
|
||||
return $attempt;
|
||||
}
|
||||
|
@ -62,67 +62,10 @@ if (!$quizobj->has_questions()) {
|
||||
// Create an object to manage all the other (non-roles) access rules.
|
||||
$timenow = time();
|
||||
$accessmanager = $quizobj->get_access_manager($timenow);
|
||||
if ($quizobj->is_preview_user() && $forcenew) {
|
||||
$accessmanager->current_attempt_finished();
|
||||
}
|
||||
|
||||
// Check capabilities.
|
||||
if (!$quizobj->is_preview_user()) {
|
||||
$quizobj->require_capability('mod/quiz:attempt');
|
||||
}
|
||||
|
||||
// Check to see if a new preview was requested.
|
||||
if ($quizobj->is_preview_user() && $forcenew) {
|
||||
// To force the creation of a new preview, we mark the current attempt (if any)
|
||||
// as finished. It will then automatically be deleted below.
|
||||
$DB->set_field('quiz_attempts', 'state', quiz_attempt::FINISHED,
|
||||
array('quiz' => $quizobj->get_quizid(), 'userid' => $USER->id));
|
||||
}
|
||||
|
||||
// Look for an existing attempt.
|
||||
$attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true);
|
||||
$lastattempt = end($attempts);
|
||||
|
||||
// If an in-progress attempt exists, check password then redirect to it.
|
||||
if ($lastattempt && ($lastattempt->state == quiz_attempt::IN_PROGRESS ||
|
||||
$lastattempt->state == quiz_attempt::OVERDUE)) {
|
||||
$currentattemptid = $lastattempt->id;
|
||||
$messages = $accessmanager->prevent_access();
|
||||
|
||||
// If the attempt is now overdue, deal with that.
|
||||
$quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true);
|
||||
|
||||
// And, if the attempt is now no longer in progress, redirect to the appropriate place.
|
||||
if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) {
|
||||
redirect($quizobj->review_url($lastattempt->id));
|
||||
}
|
||||
|
||||
// If the page number was not explicitly in the URL, go to the current page.
|
||||
if ($page == -1) {
|
||||
$page = $lastattempt->currentpage;
|
||||
}
|
||||
|
||||
} else {
|
||||
while ($lastattempt && $lastattempt->preview) {
|
||||
$lastattempt = array_pop($attempts);
|
||||
}
|
||||
|
||||
// Get number for the next or unfinished attempt.
|
||||
if ($lastattempt) {
|
||||
$attemptnumber = $lastattempt->attempt + 1;
|
||||
} else {
|
||||
$lastattempt = false;
|
||||
$attemptnumber = 1;
|
||||
}
|
||||
$currentattemptid = null;
|
||||
|
||||
$messages = $accessmanager->prevent_access() +
|
||||
$accessmanager->prevent_new_attempt(count($attempts), $lastattempt);
|
||||
|
||||
if ($page == -1) {
|
||||
$page = 0;
|
||||
}
|
||||
}
|
||||
// Validate permissions for creating a new attempt and start a new preview attempt if required.
|
||||
list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
|
||||
quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, $page, true);
|
||||
|
||||
// Check access.
|
||||
if (!$quizobj->is_preview_user() && $messages) {
|
||||
@ -165,27 +108,7 @@ if ($currentattemptid) {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any previous preview attempts belonging to this user.
|
||||
quiz_delete_previews($quizobj->get_quiz(), $USER->id);
|
||||
|
||||
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||
|
||||
// Create the new attempt and initialize the question sessions
|
||||
$timenow = time(); // Update time now, in case the server is running really slowly.
|
||||
$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user());
|
||||
|
||||
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
|
||||
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
|
||||
} else {
|
||||
$attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt);
|
||||
}
|
||||
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
$transaction->allow_commit();
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt);
|
||||
|
||||
// Redirect to the attempt page.
|
||||
redirect($quizobj->attempt_url($attempt->id, $page));
|
||||
|
@ -585,4 +585,121 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test start_attempt
|
||||
*/
|
||||
public function test_start_attempt() {
|
||||
global $DB;
|
||||
|
||||
// Create a new quiz with attempts.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$data = array('course' => $this->course->id,
|
||||
'sumgrades' => 1);
|
||||
$quiz = $quizgenerator->create_instance($data);
|
||||
$context = context_module::instance($quiz->cmid);
|
||||
|
||||
try {
|
||||
mod_quiz_external::start_attempt($quiz->id);
|
||||
$this->fail('Exception expected due to missing questions.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('noquestionsfound', $e->errorcode);
|
||||
}
|
||||
|
||||
// Create a question.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
|
||||
quiz_add_quiz_question($question->id, $quiz);
|
||||
|
||||
$quizobj = quiz::create($quiz->id, $this->student->id);
|
||||
|
||||
// Set grade to pass.
|
||||
$item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
|
||||
'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
|
||||
$item->gradepass = 80;
|
||||
$item->update();
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
// Try to open attempt in closed quiz.
|
||||
$quiz->timeopen = time() - WEEKSECS;
|
||||
$quiz->timeclose = time() - DAYSECS;
|
||||
$DB->update_record('quiz', $quiz);
|
||||
$result = mod_quiz_external::start_attempt($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
|
||||
|
||||
$this->assertEquals([], $result['attempt']);
|
||||
$this->assertCount(1, $result['warnings']);
|
||||
|
||||
// Now with a password.
|
||||
$quiz->timeopen = 0;
|
||||
$quiz->timeclose = 0;
|
||||
$quiz->password = 'abc';
|
||||
$DB->update_record('quiz', $quiz);
|
||||
|
||||
try {
|
||||
mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad')));
|
||||
$this->fail('Exception expected due to invalid passwod.');
|
||||
} catch (moodle_exception $e) {
|
||||
$this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
|
||||
}
|
||||
|
||||
// Now, try everything correct.
|
||||
$result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
|
||||
|
||||
$this->assertEquals(1, $result['attempt']['attempt']);
|
||||
$this->assertEquals($this->student->id, $result['attempt']['userid']);
|
||||
$this->assertEquals($quiz->id, $result['attempt']['quiz']);
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
$attemptid = $result['attempt']['id'];
|
||||
|
||||
// We are good, try to start a new attempt now.
|
||||
|
||||
try {
|
||||
mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
|
||||
$this->fail('Exception expected due to attempt not finished.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('attemptstillinprogress', $e->errorcode);
|
||||
}
|
||||
|
||||
// Finish the started attempt.
|
||||
|
||||
// Process some responses from the student.
|
||||
$timenow = time();
|
||||
$attemptobj = quiz_attempt::create($attemptid);
|
||||
$tosubmit = array(1 => array('answer' => '3.14'));
|
||||
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
|
||||
|
||||
// Finish the attempt.
|
||||
$attemptobj = quiz_attempt::create($attemptid);
|
||||
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
|
||||
$attemptobj->process_finish($timenow, false);
|
||||
|
||||
// We should be able to start a new attempt.
|
||||
$result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
|
||||
|
||||
$this->assertEquals(2, $result['attempt']['attempt']);
|
||||
$this->assertEquals($this->student->id, $result['attempt']['userid']);
|
||||
$this->assertEquals($quiz->id, $result['attempt']['quiz']);
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
|
||||
// Test user with no capabilities.
|
||||
// We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
|
||||
assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
|
||||
// Empty all the caches that may be affected by this change.
|
||||
accesslib_clear_all_caches_for_unit_testing();
|
||||
course_modinfo::clear_instance_cache();
|
||||
|
||||
try {
|
||||
mod_quiz_external::start_attempt($quiz->id);
|
||||
$this->fail('Exception expected due to missing capability.');
|
||||
} catch (required_capability_exception $e) {
|
||||
$this->assertEquals('nopermissions', $e->errorcode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2015111606;
|
||||
$plugin->version = 2015111607;
|
||||
$plugin->requires = 2015111000;
|
||||
$plugin->component = 'mod_quiz';
|
||||
$plugin->cron = 60;
|
||||
|
Loading…
x
Reference in New Issue
Block a user