MDL-52863 mod_quiz: New Web Service mod_quiz_process_attempt

This commit is contained in:
Juan Leyva 2016-01-21 12:55:28 +01:00
parent 4911b5b254
commit 98e6869088
4 changed files with 236 additions and 20 deletions

View File

@ -777,19 +777,18 @@ class mod_quiz_external extends external_api {
/**
* Utility function for validating a given attempt
*
* @param array $params Array of parameters including the attemptid and preflight data
* @param array $params array of parameters including the attemptid and preflight data
* @param bool $checkaccessrules whether to check the quiz access rules or not
* @param bool $failifoverdue whether to return error if the attempt is overdue
* @return array containing the attempt object and access messages
* @throws moodle_quiz_exception
* @since Moodle 3.1
*/
protected static function validate_attempt($params) {
protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
global $USER;
$attemptobj = quiz_attempt::create($params['attemptid']);
// If the attempt is now overdue, or abandoned, deal with that.
$attemptobj->handle_if_time_expired(time(), true);
$context = context_module::instance($attemptobj->get_cm()->id);
self::validate_context($context);
@ -804,21 +803,26 @@ class mod_quiz_external extends external_api {
$attemptobj->require_capability('mod/quiz:attempt');
}
// Check the access rules.
$accessmanager = $attemptobj->get_access_manager(time());
$messages = array();
if ($checkaccessrules) {
// If the attempt is now overdue, or abandoned, deal with that.
$attemptobj->handle_if_time_expired(time(), true);
$messages = $accessmanager->prevent_access();
if (!$ispreviewuser && $messages) {
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
}
}
// Attempt closed?.
if ($attemptobj->is_finished()) {
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
} else if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
} else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
}
// Check the access rules.
$accessmanager = $attemptobj->get_access_manager(time());
$messages = $accessmanager->prevent_access();
if (!$ispreviewuser && $messages) {
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
}
// User submitted data (like the quiz password).
if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
$provideddata = array();
@ -1049,7 +1053,7 @@ class mod_quiz_external extends external_api {
);
$params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
list($attemptobj, $messages) = self::validate_attempt($params);
list($attemptobj, $messages) = self::validate_attempt($params, true, false);
$result = array();
$result['warnings'] = $warnings;
@ -1158,4 +1162,96 @@ class mod_quiz_external extends external_api {
);
}
/**
* Describes the parameters for process_attempt.
*
* @return external_external_function_parameters
* @since Moodle 3.1
*/
public static function process_attempt_parameters() {
return new external_function_parameters (
array(
'attemptid' => new external_value(PARAM_INT, 'attempt id'),
'data' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_RAW, 'data name'),
'value' => new external_value(PARAM_RAW, 'data value'),
)
),
'the data to be saved', VALUE_DEFAULT, array()
),
'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
VALUE_DEFAULT, false),
'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()
)
)
);
}
/**
* Process responses during an attempt at a quiz and also deals with attempts finishing.
*
* @param int $attemptid attempt id
* @param array $data the data to be saved
* @param bool $finishattempt whether to finish or not the attempt
* @param bool $timeup whether the WS was called by a timer when the time is up
* @param array $preflightdata preflight required data (like passwords)
* @return array of warnings and the attempt state after the processing
* @since Moodle 3.1
*/
public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
$warnings = array();
$params = array(
'attemptid' => $attemptid,
'data' => $data,
'finishattempt' => $finishattempt,
'timeup' => $timeup,
'preflightdata' => $preflightdata,
);
$params = self::validate_parameters(self::process_attempt_parameters(), $params);
// Do not check access manager rules.
list($attemptobj, $messages) = self::validate_attempt($params, false);
// Create the $_POST object required by the question engine.
$_POST = array();
foreach ($params['data'] as $element) {
$_POST[$element['name']] = $element['value'];
}
$timenow = time();
$finishattempt = $params['finishattempt'];
$timeup = $params['timeup'];
$result = array();
$result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
$result['warnings'] = $warnings;
return $result;
}
/**
* Describes the process_attempt return value.
*
* @return external_single_structure
* @since Moodle 3.1
*/
public static function process_attempt_returns() {
return new external_single_structure(
array(
'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
inprogress, finished, overdue, abandoned'),
'warnings' => new external_warnings(),
)
);
}
}

View File

@ -110,4 +110,13 @@ $functions = array(
'capabilities' => 'mod/quiz:attempt',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_quiz_process_attempt' => array(
'classname' => 'mod_quiz_external',
'methodname' => 'process_attempt',
'description' => 'Process responses during an attempt at a quiz and also deals with attempts finishing.',
'type' => 'write',
'capabilities' => 'mod/quiz:attempt',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);

View File

@ -44,10 +44,12 @@ class testable_mod_quiz_external extends mod_quiz_external {
* Public accessor.
*
* @param array $params Array of parameters including the attemptid and preflight data
* @param bool $checkaccessrules whether to check the quiz access rules or not
* @param bool $failifoverdue whether to return error if the attempt is overdue
* @return array containing the attempt object and access messages
*/
public static function validate_attempt($params) {
return parent::validate_attempt($params);
public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
}
}
@ -779,11 +781,16 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
$quiz->timeopen = time() - WEEKSECS;
$quiz->timeclose = time() - DAYSECS;
$DB->update_record('quiz', $quiz);
// This should work, ommit access rules.
testable_mod_quiz_external::validate_attempt($params, false);
// Get a generic error because prior to checking the dates the attempt is closed.
try {
testable_mod_quiz_external::validate_attempt($params);
$this->fail('Exception expected due to passed dates.');
} catch (moodle_quiz_exception $e) {
$this->assertEquals('attemptalreadyclosed', $e->errorcode);
$this->assertEquals('attempterror', $e->errorcode);
}
// Finish the attempt.
@ -791,7 +798,7 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
$attemptobj->process_finish(time(), false);
try {
testable_mod_quiz_external::validate_attempt($params);
testable_mod_quiz_external::validate_attempt($params, false);
$this->fail('Exception expected due to attempt finished.');
} catch (moodle_quiz_exception $e) {
$this->assertEquals('attemptalreadyclosed', $e->errorcode);
@ -1010,4 +1017,108 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
}
/**
* Test process_attempt
*/
public function test_process_attempt() {
global $DB;
// Create a new quiz with two questions and one attempt started.
list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
// Response for slot 1.
$prefix = $quba->get_field_prefix(1);
$data = array(
array('name' => 'slots', 'value' => 1),
array('name' => $prefix . ':sequencecheck',
'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
array('name' => $prefix . 'answer', 'value' => 1),
);
$this->setUser($this->student);
$result = mod_quiz_external::process_attempt($attempt->id, $data);
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
// Now, get the summary.
$result = mod_quiz_external::get_attempt_summary($attempt->id);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
// Check it's marked as completed only the first one.
$this->assertEquals('complete', $result['questions'][0]['state']);
$this->assertEquals('todo', $result['questions'][1]['state']);
$this->assertEquals(1, $result['questions'][0]['number']);
$this->assertEquals(2, $result['questions'][1]['number']);
$this->assertFalse($result['questions'][0]['flagged']);
$this->assertFalse($result['questions'][1]['flagged']);
$this->assertEmpty($result['questions'][0]['mark']);
$this->assertEmpty($result['questions'][1]['mark']);
// Now, second slot.
$prefix = $quba->get_field_prefix(2);
$data = array(
array('name' => 'slots', 'value' => 2),
array('name' => $prefix . ':sequencecheck',
'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
array('name' => $prefix . 'answer', 'value' => 1),
array('name' => $prefix . ':flagged', 'value' => 1),
);
$result = mod_quiz_external::process_attempt($attempt->id, $data);
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
// Now, get the summary.
$result = mod_quiz_external::get_attempt_summary($attempt->id);
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
// Check it's marked as completed only the first one.
$this->assertEquals('complete', $result['questions'][0]['state']);
$this->assertEquals('complete', $result['questions'][1]['state']);
$this->assertFalse($result['questions'][0]['flagged']);
$this->assertTrue($result['questions'][1]['flagged']);
// Finish the attempt.
$result = mod_quiz_external::process_attempt($attempt->id, array(), true);
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::FINISHED, $result['state']);
// Start new attempt.
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$timenow = time();
$attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Force grace period, attempt going to overdue.
$quiz->timeclose = $timenow - 10;
$quiz->graceperiod = 60;
$quiz->overduehandling = 'graceperiod';
$DB->update_record('quiz', $quiz);
$result = mod_quiz_external::process_attempt($attempt->id, array());
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
// New attempt.
$timenow = time();
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj, 3, 2, $timenow, false, $this->student->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Force abandon.
$quiz->timeclose = $timenow - HOURSECS;
$DB->update_record('quiz', $quiz);
$result = mod_quiz_external::process_attempt($attempt->id, array());
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::ABANDONED, $result['state']);
}
}

View File

@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2015111610;
$plugin->version = 2015111611;
$plugin->requires = 2015111000;
$plugin->component = 'mod_quiz';
$plugin->cron = 60;