MDL-52788 mod_quiz: New Web Service mod_quiz_start_attempt

This commit is contained in:
Juan Leyva 2016-01-15 14:43:08 +01:00
parent 0057c2ced6
commit b89544404e
4 changed files with 293 additions and 29 deletions

View file

@ -406,16 +406,12 @@ class mod_quiz_external extends external_api {
}
/**
* Describes the get_user_attempts return value.
* Describes a single attempt structure.
*
* @return external_single_structure
* @since Moodle 3.1
* @return external_single_structure the attempt structure
*/
public static function get_user_attempts_returns() {
private static function attempt_structure() {
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.',
@ -440,8 +436,19 @@ class mod_quiz_external extends external_api {
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.
*
* @return external_single_structure
* @since Moodle 3.1
*/
public static function get_user_attempts_returns() {
return new external_single_structure(
array(
'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(),
)
);
}
}

View file

@ -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)
),
);

View file

@ -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);
}
}
}

View file

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