Merge branch 'MDL-52813-master' of git://github.com/jleyva/moodle
This commit is contained in:
@@ -1232,6 +1232,17 @@ class quiz_attempt {
|
||||
return $this->quba->get_question_action_time($slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the question type name for a given slot within the current attempt.
|
||||
*
|
||||
* @param int $slot the number used to identify this question within this attempt.
|
||||
* @return string the question type name
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public function get_question_type_name($slot) {
|
||||
return $this->quba->get_question($slot)->get_type_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time remaining for an in-progress attempt, if the time is short
|
||||
* enought that it would be worth showing a timer.
|
||||
|
||||
@@ -774,4 +774,232 @@ 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
|
||||
* @return array containing the attempt object and access messages
|
||||
* @throws moodle_quiz_exception
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
protected static function validate_attempt($params) {
|
||||
global $USER;
|
||||
|
||||
$attemptobj = quiz_attempt::create($params['attemptid']);
|
||||
|
||||
$context = context_module::instance($attemptobj->get_cm()->id);
|
||||
self::validate_context($context);
|
||||
|
||||
// Check that this attempt belongs to this user.
|
||||
if ($attemptobj->get_userid() != $USER->id) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
|
||||
}
|
||||
|
||||
// General capabilities check.
|
||||
$ispreviewuser = $attemptobj->is_preview_user();
|
||||
if (!$ispreviewuser) {
|
||||
$attemptobj->require_capability('mod/quiz:attempt');
|
||||
}
|
||||
|
||||
// Attempt closed?.
|
||||
if ($attemptobj->is_finished()) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
|
||||
} else if ($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();
|
||||
foreach ($params['preflightdata'] as $data) {
|
||||
$provideddata[$data['name']] = $data['value'];
|
||||
}
|
||||
|
||||
$errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
|
||||
if (!empty($errors)) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
|
||||
}
|
||||
// Pre-flight check passed.
|
||||
$accessmanager->notify_preflight_check_passed($params['attemptid']);
|
||||
}
|
||||
|
||||
// Check if the page is out of range.
|
||||
if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
|
||||
}
|
||||
|
||||
// Prevent out of sequence access.
|
||||
if ($attemptobj->get_currentpage() != $params['page']) {
|
||||
if ($attemptobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $attemptobj->get_currentpage() > $params['page']) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
|
||||
}
|
||||
}
|
||||
|
||||
// Check slots.
|
||||
$slots = $attemptobj->get_slots($params['page']);
|
||||
|
||||
if (empty($slots)) {
|
||||
throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
|
||||
}
|
||||
|
||||
return array($attemptobj, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a single question structure.
|
||||
*
|
||||
* @return external_single_structure the question structure
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
private static function question_structure() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'slot' => new external_value(PARAM_INT, 'slot number'),
|
||||
'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'),
|
||||
'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),
|
||||
'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
|
||||
'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL),
|
||||
'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return questions information for a given attempt.
|
||||
*
|
||||
* @param quiz_attempt $attemptobj the quiz attempt object
|
||||
* @param bool $review whether if we are in review mode or not
|
||||
* @param mixed $page string 'all' or integer page number
|
||||
* @return array array of questions including data
|
||||
*/
|
||||
private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
|
||||
global $PAGE;
|
||||
|
||||
$questions = array();
|
||||
$contextid = $attemptobj->get_quizobj()->get_context()->id;
|
||||
$displayoptions = $attemptobj->get_display_options($review);
|
||||
$renderer = $PAGE->get_renderer('mod_quiz');
|
||||
|
||||
foreach ($attemptobj->get_slots($page) as $slot) {
|
||||
|
||||
$question = array(
|
||||
'slot' => $slot,
|
||||
'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()
|
||||
);
|
||||
|
||||
if ($attemptobj->is_real_question($slot)) {
|
||||
$question['number'] = $attemptobj->get_question_number($slot);
|
||||
$question['state'] = (string) $attemptobj->get_question_state($slot);
|
||||
$question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
|
||||
}
|
||||
if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
|
||||
$question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark();
|
||||
}
|
||||
if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
|
||||
$question['mark'] = $attemptobj->get_question_mark($slot);
|
||||
}
|
||||
|
||||
$questions[] = $question;
|
||||
}
|
||||
return $questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the parameters for get_attempt_data.
|
||||
*
|
||||
* @return external_external_function_parameters
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public static function get_attempt_data_parameters() {
|
||||
return new external_function_parameters (
|
||||
array(
|
||||
'attemptid' => new external_value(PARAM_INT, 'attempt id'),
|
||||
'page' => new external_value(PARAM_INT, 'page number'),
|
||||
'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()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information for the given attempt page for a quiz attempt in progress.
|
||||
*
|
||||
* @param int $attemptid attempt id
|
||||
* @param int $page page number
|
||||
* @param array $preflightdata preflight required data (like passwords)
|
||||
* @return array of warnings and the attempt data, next page, message and questions
|
||||
* @since Moodle 3.1
|
||||
* @throws moodle_quiz_exceptions
|
||||
*/
|
||||
public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
|
||||
|
||||
$warnings = array();
|
||||
|
||||
$params = array(
|
||||
'attemptid' => $attemptid,
|
||||
'page' => $page,
|
||||
'preflightdata' => $preflightdata,
|
||||
);
|
||||
$params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
|
||||
|
||||
list($attemptobj, $messages) = self::validate_attempt($params);
|
||||
|
||||
if ($attemptobj->is_last_page($params['page'])) {
|
||||
$nextpage = -1;
|
||||
} else {
|
||||
$nextpage = $params['page'] + 1;
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$result['attempt'] = $attemptobj->get_attempt();
|
||||
$result['messages'] = $messages;
|
||||
$result['nextpage'] = $nextpage;
|
||||
$result['warnings'] = $warnings;
|
||||
$result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the get_attempt_data return value.
|
||||
*
|
||||
* @return external_single_structure
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public static function get_attempt_data_returns() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'attempt' => self::attempt_structure(),
|
||||
'messages' => new external_multiple_structure(
|
||||
new external_value(PARAM_TEXT, 'access message'),
|
||||
'access messages, will only be returned for users with mod/quiz:preview capability,
|
||||
for other users this method will throw an exception if there are messages'),
|
||||
'nextpage' => new external_value(PARAM_INT, 'next page number'),
|
||||
'questions' => new external_multiple_structure(self::question_structure()),
|
||||
'warnings' => new external_warnings(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,4 +82,13 @@ $functions = array(
|
||||
'capabilities' => 'mod/quiz:attempt',
|
||||
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
|
||||
),
|
||||
|
||||
'mod_quiz_get_attempt_data' => array(
|
||||
'classname' => 'mod_quiz_external',
|
||||
'methodname' => 'get_attempt_data',
|
||||
'description' => 'Returns information for the given attempt page for a quiz attempt in progress.',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'mod/quiz:attempt',
|
||||
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
|
||||
),
|
||||
);
|
||||
|
||||
@@ -30,6 +30,27 @@ global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Silly class to access mod_quiz_external internal methods.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2016 Juan Leyva <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
class testable_mod_quiz_external extends mod_quiz_external {
|
||||
|
||||
/**
|
||||
* Public accessor.
|
||||
*
|
||||
* @param array $params Array of parameters including the attemptid and preflight data
|
||||
* @return array containing the attempt object and access messages
|
||||
*/
|
||||
public static function validate_attempt($params) {
|
||||
return parent::validate_attempt($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quiz module external functions tests
|
||||
*
|
||||
@@ -66,6 +87,65 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
|
||||
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a quiz with questions including a started or finished attempt optionally
|
||||
*
|
||||
* @param boolean $startattempt whether to start a new attempt
|
||||
* @param boolean $finishattempt whether to finish the new attempt
|
||||
* @return array array containing the quiz, context and the attempt
|
||||
*/
|
||||
private function create_quiz_with_questions($startattempt = false, $finishattempt = false) {
|
||||
|
||||
// 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);
|
||||
|
||||
// Create a couple of questions.
|
||||
$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);
|
||||
$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();
|
||||
|
||||
if ($startattempt or $finishattempt) {
|
||||
// Now, do one 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, 1, false, $timenow, false, $this->student->id);
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
|
||||
if ($finishattempt) {
|
||||
// Process some responses from the student.
|
||||
$tosubmit = array(1 => array('answer' => '3.14'));
|
||||
$attemptobj->process_submitted_actions(time(), false, $tosubmit);
|
||||
|
||||
// Finish the attempt.
|
||||
$attemptobj->process_finish(time(), false);
|
||||
}
|
||||
return array($quiz, $context, $quizobj, $attempt, $attemptobj);
|
||||
} else {
|
||||
return array($quiz, $context, $quizobj);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Test get quizzes by courses
|
||||
*/
|
||||
@@ -273,45 +353,8 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
|
||||
*/
|
||||
public function test_get_user_attempts() {
|
||||
|
||||
// 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);
|
||||
|
||||
// Create a couple of questions.
|
||||
$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();
|
||||
|
||||
// Start the passing 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, 1, false, $timenow, false, $this->student->id);
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
// Process some responses from the student.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$tosubmit = array(1 => array('answer' => '3.14'));
|
||||
$attemptobj->process_submitted_actions($timenow, false, $tosubmit);
|
||||
|
||||
// Finish the attempt.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
|
||||
$attemptobj->process_finish($timenow, false);
|
||||
// Create a quiz with one attempt finished.
|
||||
list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
@@ -591,34 +634,8 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
|
||||
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();
|
||||
// Create a new quiz with questions.
|
||||
list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
@@ -702,4 +719,197 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validate_attempt
|
||||
*/
|
||||
public function test_validate_attempt() {
|
||||
global $DB;
|
||||
|
||||
// Create a new quiz with one attempt started.
|
||||
list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
// Invalid attempt.
|
||||
try {
|
||||
$params = array('attemptid' => -1, 'page' => 0);
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to invalid attempt id.');
|
||||
} catch (dml_missing_record_exception $e) {
|
||||
$this->assertEquals('invalidrecord', $e->errorcode);
|
||||
}
|
||||
|
||||
// Test OK case.
|
||||
$params = array('attemptid' => $attempt->id, 'page' => 0);
|
||||
$result = testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
|
||||
$this->assertEquals([], $result[1]);
|
||||
|
||||
// Test with preflight data.
|
||||
$quiz->password = 'abc';
|
||||
$DB->update_record('quiz', $quiz);
|
||||
|
||||
try {
|
||||
$params = array('attemptid' => $attempt->id, 'page' => 0,
|
||||
'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad')));
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$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.
|
||||
$params['preflightdata'][0]['value'] = 'abc';
|
||||
$result = testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
|
||||
$this->assertEquals([], $result[1]);
|
||||
|
||||
// Try to open attempt in closed quiz.
|
||||
$quiz->timeopen = time() - WEEKSECS;
|
||||
$quiz->timeclose = time() - DAYSECS;
|
||||
$DB->update_record('quiz', $quiz);
|
||||
|
||||
try {
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to passed dates.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('attempterror', $e->errorcode);
|
||||
}
|
||||
|
||||
// Page out of range.
|
||||
$quiz->timeopen = 0;
|
||||
$quiz->timeclose = 0;
|
||||
$DB->update_record('quiz', $quiz);
|
||||
$params['page'] = 4;
|
||||
try {
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to page out of range.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('Invalid page number', $e->errorcode);
|
||||
}
|
||||
|
||||
// Finish the attempt.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_finish(time(), false);
|
||||
|
||||
try {
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to attempt finished.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('attemptalreadyclosed', $e->errorcode);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to missing permissions.');
|
||||
} catch (required_capability_exception $e) {
|
||||
$this->assertEquals('nopermissions', $e->errorcode);
|
||||
}
|
||||
|
||||
// Now try with a different user.
|
||||
$this->setUser($this->teacher);
|
||||
|
||||
$params['page'] = 0;
|
||||
try {
|
||||
testable_mod_quiz_external::validate_attempt($params);
|
||||
$this->fail('Exception expected due to not your attempt.');
|
||||
} catch (moodle_quiz_exception $e) {
|
||||
$this->assertEquals('notyourattempt', $e->errorcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_attempt_data
|
||||
*/
|
||||
public function test_get_attempt_data() {
|
||||
global $DB;
|
||||
|
||||
// Create a new quiz with one attempt started.
|
||||
list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
|
||||
|
||||
$quizobj = $attemptobj->get_quizobj();
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
// We receive one question per page.
|
||||
$result = mod_quiz_external::get_attempt_data($attempt->id, 0);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
|
||||
|
||||
$this->assertEquals($attempt, (object) $result['attempt']);
|
||||
$this->assertEquals(1, $result['nextpage']);
|
||||
$this->assertCount(0, $result['messages']);
|
||||
$this->assertCount(1, $result['questions']);
|
||||
$this->assertEquals(1, $result['questions'][0]['slot']);
|
||||
$this->assertEquals(1, $result['questions'][0]['number']);
|
||||
$this->assertEquals('numerical', $result['questions'][0]['type']);
|
||||
$this->assertEquals('todo', $result['questions'][0]['state']);
|
||||
$this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
|
||||
$this->assertFalse($result['questions'][0]['flagged']);
|
||||
$this->assertEquals(0, $result['questions'][0]['page']);
|
||||
$this->assertEmpty($result['questions'][0]['mark']);
|
||||
$this->assertEquals(1, $result['questions'][0]['maxmark']);
|
||||
|
||||
// Now try the last page.
|
||||
$result = mod_quiz_external::get_attempt_data($attempt->id, 1);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
|
||||
|
||||
$this->assertEquals($attempt, (object) $result['attempt']);
|
||||
$this->assertEquals(-1, $result['nextpage']);
|
||||
$this->assertCount(0, $result['messages']);
|
||||
$this->assertCount(1, $result['questions']);
|
||||
$this->assertEquals(2, $result['questions'][0]['slot']);
|
||||
$this->assertEquals(2, $result['questions'][0]['number']);
|
||||
$this->assertEquals('numerical', $result['questions'][0]['type']);
|
||||
$this->assertEquals('todo', $result['questions'][0]['state']);
|
||||
$this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
|
||||
$this->assertFalse($result['questions'][0]['flagged']);
|
||||
$this->assertEquals(1, $result['questions'][0]['page']);
|
||||
|
||||
// Finish previous attempt.
|
||||
$attemptobj->process_finish(time(), false);
|
||||
|
||||
// Change setting and expect two pages.
|
||||
$quiz->questionsperpage = 4;
|
||||
$DB->update_record('quiz', $quiz);
|
||||
quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
|
||||
|
||||
// Start with new attempt with the new layout.
|
||||
$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, 1, $timenow);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
// We receive two questions per page.
|
||||
$result = mod_quiz_external::get_attempt_data($attempt->id, 0);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
|
||||
$this->assertCount(2, $result['questions']);
|
||||
$this->assertEquals(-1, $result['nextpage']);
|
||||
|
||||
// Check questions looks good.
|
||||
$found = 0;
|
||||
foreach ($questions as $question) {
|
||||
foreach ($result['questions'] as $rquestion) {
|
||||
if ($rquestion['slot'] == $question->slot) {
|
||||
$this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false);
|
||||
$found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->assertEquals(2, $found);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2015111607;
|
||||
$plugin->version = 2015111608;
|
||||
$plugin->requires = 2015111000;
|
||||
$plugin->component = 'mod_quiz';
|
||||
$plugin->cron = 60;
|
||||
|
||||
Reference in New Issue
Block a user