MDL-68806 quiz: Backwards compatibility for mod_quiz_get_user_attempts
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
issueNumber: MDL-68806
|
||||
notes:
|
||||
mod_quiz:
|
||||
- message: >+
|
||||
The webservice function `mod_quiz_get_user_attempts` is now deprecated
|
||||
in favour of `mod_quiz_get_user_quiz_attempts`.
|
||||
|
||||
|
||||
With the introduction of the new NOT_STARTED quiz attempt state,
|
||||
`mod_quiz_get_user_attempts` has been modified to not return NOT_STARTED
|
||||
attempts, allowing clients such as the mobile app to continue working
|
||||
without modifications.
|
||||
|
||||
|
||||
`mod_quiz_get_user_quiz_attempts` will return attempts in all states, as
|
||||
`mod_quiz_get_user_attempts` did before. Once clients are updated to
|
||||
handle NOT_STARTED attempts, they can migrate to use this function.
|
||||
|
||||
|
||||
A minor modification to `mod_quiz_start_attempt` has been made to allow
|
||||
it to transparently start an existing attempt that is in the NOT_STARTED
|
||||
state, rather than creating a new one.
|
||||
|
||||
type: deprecated
|
||||
@@ -363,7 +363,15 @@ class mod_quiz_external extends external_api {
|
||||
*
|
||||
* @return external_function_parameters
|
||||
* @since Moodle 3.1
|
||||
* @deprecated Since Moodle 5.0 MDL-68806.
|
||||
* @todo Final deprecation in Moodle 6.0 (MDL-80956)
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'mod_quiz_external::get_user_quiz_attempts_parameters',
|
||||
since: '5.0',
|
||||
reason: 'The old API for fetching attempts doesn\'t return true states for NOT_STARTED and SUBMITTED attempts',
|
||||
mdl: 'MDL-68806'
|
||||
)]
|
||||
public static function get_user_attempts_parameters() {
|
||||
return new external_function_parameters (
|
||||
[
|
||||
@@ -379,15 +387,27 @@ class mod_quiz_external extends external_api {
|
||||
/**
|
||||
* Return a list of attempts for the given quiz and user.
|
||||
*
|
||||
* For backwards compatibility, SUBMITTED attempts will be treated as FINISHED with marks hidden, and NOT_STARTED will not
|
||||
* be returned. To return all real states, call get_user_quiz_attempts instead.
|
||||
*
|
||||
* @param int $quizid quiz instance id
|
||||
* @param int $userid user id
|
||||
* @param string $status quiz status: all, finished or unfinished
|
||||
* @param bool $includepreviews whether to include previews or not
|
||||
* @return array of warnings and the list of attempts
|
||||
* @since Moodle 3.1
|
||||
* @deprecated Since Moodle 5.0 MDL-68806.
|
||||
* @todo Final deprecation in Moodle 6.0 (MDL-80956)
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'mod_quiz_external::get_user_quiz_attempts',
|
||||
since: '5.0',
|
||||
reason: 'The old API for fetching attempts doesn\'t return true states for NOT_STARTED and SUBMITTED attempts',
|
||||
mdl: 'MDL-68806'
|
||||
)]
|
||||
public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
|
||||
global $USER;
|
||||
\core\deprecation::emit_deprecation_if_present(__METHOD__);
|
||||
|
||||
$warnings = [];
|
||||
|
||||
@@ -426,9 +446,20 @@ class mod_quiz_external extends external_api {
|
||||
array_column($attempts, 'uniqueid'));
|
||||
$attemptresponse = [];
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->state == quiz_attempt::NOT_STARTED) {
|
||||
continue; // For backwards compatibility, do not return Not Started attempts.
|
||||
}
|
||||
$reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
|
||||
if (!has_capability('mod/quiz:viewreports', $context) &&
|
||||
($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
|
||||
if (
|
||||
$attempt->state == quiz_attempt::SUBMITTED ||
|
||||
(
|
||||
!has_capability('mod/quiz:viewreports', $context) &&
|
||||
(
|
||||
$reviewoptions->marks < question_display_options::MARK_AND_MAX ||
|
||||
$attempt->state != quiz_attempt::FINISHED
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Blank the mark if the teacher does not allow it.
|
||||
$attempt->sumgrades = null;
|
||||
} else if (isset($gradeitemmarks[$attempt->uniqueid])) {
|
||||
@@ -441,6 +472,9 @@ class mod_quiz_external extends external_api {
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($attempt->state == quiz_attempt::SUBMITTED) {
|
||||
$attempt->state = quiz_attempt::FINISHED; // For backwards-compatibility.
|
||||
}
|
||||
$attemptresponse[] = $attempt;
|
||||
}
|
||||
$result = [];
|
||||
@@ -498,13 +532,144 @@ class mod_quiz_external extends external_api {
|
||||
*
|
||||
* @return external_single_structure
|
||||
* @since Moodle 3.1
|
||||
* @deprecated Since Moodle 5.0 MDL-68806.
|
||||
* @todo Final deprecation in Moodle 6.0 (MDL-80956)
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'mod_quiz_external::get_user_quiz_attempts_returns',
|
||||
since: '5.0',
|
||||
reason: 'The old API for fetching attempts doesn\'t return true states for NOT_STARTED and SUBMITTED attempts',
|
||||
mdl: 'MDL-68806'
|
||||
)]
|
||||
public static function get_user_attempts_returns() {
|
||||
$attemptstructure = self::attempt_structure();
|
||||
$attemptstructure->keys['state']->desc .= " For backwards compatibility, attempts in 'submitted' state will return " .
|
||||
"'finished' and attempts in 'notstarted' state will return 'inprogress'. To get attempts with all real states, call " .
|
||||
"get_user_quiz_attempts() instead.";
|
||||
return new external_single_structure(
|
||||
[
|
||||
'attempts' => new external_multiple_structure($attemptstructure),
|
||||
'warnings' => new external_warnings(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark get_user_attempts as deprecated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function get_user_attempts_is_deprecated(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the parameters for get_user_quiz_attempts.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
* @since Moodle 4.5
|
||||
*/
|
||||
public static function get_user_quiz_attempts_parameters(): external_function_parameters {
|
||||
return new external_function_parameters (
|
||||
[
|
||||
'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
|
||||
'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
|
||||
'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
|
||||
'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of attempts for the given quiz and user.
|
||||
*
|
||||
* @param int $quizid quiz instance id
|
||||
* @param int $userid user id
|
||||
* @param string $status quiz status: all, finished or unfinished
|
||||
* @param bool $includepreviews whether to include previews or not
|
||||
* @return array of warnings and the list of attempts
|
||||
* @since Moodle 4.5
|
||||
*/
|
||||
public static function get_user_quiz_attempts(
|
||||
int $quizid,
|
||||
int $userid = 0,
|
||||
string $status = 'finished',
|
||||
bool $includepreviews = false
|
||||
): array {
|
||||
global $USER;
|
||||
|
||||
$warnings = [];
|
||||
|
||||
$params = [
|
||||
'quizid' => $quizid,
|
||||
'userid' => $userid,
|
||||
'status' => $status,
|
||||
'includepreviews' => $includepreviews,
|
||||
];
|
||||
$params = self::validate_parameters(self::get_user_quiz_attempts_parameters(), $params);
|
||||
|
||||
[$quiz, $course, $cm, $context] = self::validate_quiz($params['quizid']);
|
||||
|
||||
if (!in_array($params['status'], ['all', 'finished', 'unfinished'])) {
|
||||
throw new invalid_parameter_exception('Invalid status value');
|
||||
}
|
||||
|
||||
// Default value for userid.
|
||||
if (empty($params['userid'])) {
|
||||
$params['userid'] = $USER->id;
|
||||
}
|
||||
|
||||
$user = core_user::get_user($params['userid'], '*', MUST_EXIST);
|
||||
core_user::require_active_user($user);
|
||||
|
||||
// Extra checks so only users with permissions can view other users attempts.
|
||||
if ($USER->id != $user->id) {
|
||||
require_capability('mod/quiz:viewreports', $context);
|
||||
}
|
||||
|
||||
// Update quiz with override information.
|
||||
$quiz = quiz_update_effective_access($quiz, $params['userid']);
|
||||
$attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
|
||||
$quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$gradeitemmarks = $quizobj->get_grade_calculator()->compute_grade_item_totals_for_attempts(
|
||||
array_column($attempts, 'uniqueid'));
|
||||
$attemptresponse = [];
|
||||
foreach ($attempts as $attempt) {
|
||||
$reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
|
||||
if (!has_capability('mod/quiz:viewreports', $context) &&
|
||||
($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
|
||||
// Blank the mark if the teacher does not allow it.
|
||||
$attempt->sumgrades = null;
|
||||
} else if (isset($gradeitemmarks[$attempt->uniqueid])) {
|
||||
$attempt->gradeitemmarks = [];
|
||||
foreach ($gradeitemmarks[$attempt->uniqueid] as $gradeitem) {
|
||||
$attempt->gradeitemmarks[] = [
|
||||
'name' => \core_external\util::format_string($gradeitem->name, $context),
|
||||
'grade' => $gradeitem->grade,
|
||||
'maxgrade' => $gradeitem->maxgrade,
|
||||
];
|
||||
}
|
||||
}
|
||||
$attemptresponse[] = $attempt;
|
||||
}
|
||||
$result = [];
|
||||
$result['attempts'] = $attemptresponse;
|
||||
$result['warnings'] = $warnings;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the get_user_attempts return value.
|
||||
*
|
||||
* @return external_single_structure
|
||||
* @since Moodle 4.5
|
||||
*/
|
||||
public static function get_user_quiz_attempts_returns(): external_single_structure {
|
||||
return new external_single_structure(
|
||||
[
|
||||
'attempts' => new external_multiple_structure(self::attempt_structure()),
|
||||
'warnings' => new external_warnings(),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -812,7 +977,7 @@ class mod_quiz_external extends external_api {
|
||||
$accessmanager->notify_preflight_check_passed($currentattemptid);
|
||||
}
|
||||
|
||||
if ($currentattemptid) {
|
||||
if ($currentattemptid && $lastattempt->state !== quiz_attempt::NOT_STARTED) {
|
||||
if ($lastattempt->state == quiz_attempt::OVERDUE) {
|
||||
throw new moodle_exception('stateoverdue', 'quiz', $quizobj->view_url());
|
||||
} else {
|
||||
|
||||
@@ -50,12 +50,22 @@ $functions = [
|
||||
'mod_quiz_get_user_attempts' => [
|
||||
'classname' => 'mod_quiz_external',
|
||||
'methodname' => 'get_user_attempts',
|
||||
'description' => 'Return a list of attempts for the given quiz and user.',
|
||||
'description' => 'Return a list of attempts for the given quiz and user. ' .
|
||||
'(Deprecated in favour of mod_quiz_get_user_quiz_attempts).',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'mod/quiz:view',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE]
|
||||
],
|
||||
|
||||
'mod_quiz_get_user_quiz_attempts' => [
|
||||
'classname' => 'mod_quiz_external',
|
||||
'methodname' => 'get_user_quiz_attempts',
|
||||
'description' => 'Return a list of attempts for the given quiz and user.',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'mod/quiz:view',
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
|
||||
'mod_quiz_get_user_best_grade' => [
|
||||
'classname' => 'mod_quiz_external',
|
||||
'methodname' => 'get_user_best_grade',
|
||||
|
||||
@@ -346,6 +346,10 @@ function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) {
|
||||
/**
|
||||
* Create or update the quiz attempt record, and the question usage.
|
||||
*
|
||||
* If the attempt already exists in the database with the NOT_STARTED state, it will be transitioned
|
||||
* to IN_PROGRESS and the timestart updated. If it does not already exist, a new record will be created
|
||||
* already in the IN_PROGRESS state.
|
||||
*
|
||||
* @param quiz_settings $quizobj
|
||||
* @param question_usage_by_activity $quba
|
||||
* @param stdClass $attempt
|
||||
@@ -416,6 +420,9 @@ function quiz_attempt_save_started(
|
||||
/**
|
||||
* Create the quiz attempt record, and the question usage.
|
||||
*
|
||||
* This saves an attempt in the NOT_STARTED state, and is designed for use when pre-creating attempts
|
||||
* ahead of the quiz start time to spread out the processing load.
|
||||
*
|
||||
* @param question_usage_by_activity $quba
|
||||
* @param stdClass $attempt
|
||||
* @return stdClass attempt object with uniqueid and id set.
|
||||
|
||||
+320
@@ -429,6 +429,11 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_attempts
|
||||
*
|
||||
* @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
|
||||
*/
|
||||
public function test_get_user_attempts(): void {
|
||||
|
||||
// Create a quiz with one attempt finished.
|
||||
@@ -436,6 +441,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
$this->assertDebuggingCalled();
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
@@ -445,24 +451,31 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
|
||||
$this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']);
|
||||
|
||||
// Test filters. Only finished.
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
|
||||
// Test filters. All attempts.
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
|
||||
// Test filters. Unfinished.
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(0, $result['attempts']);
|
||||
|
||||
@@ -476,40 +489,55 @@ final class external_test extends externallib_advanced_testcase {
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
// Test filters. All attempts.
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(2, $result['attempts']);
|
||||
|
||||
// Test filters. Unfinished.
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
|
||||
// Test manager can see user attempts.
|
||||
$this->setUser($this->teacher);
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(2, $result['attempts']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
|
||||
// Invalid parameters.
|
||||
try {
|
||||
$this->resetDebugging();
|
||||
mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
|
||||
$this->fail('Exception expected due to missing capability.');
|
||||
} catch (\invalid_parameter_exception $e) {
|
||||
$this->assertDebuggingCalled();
|
||||
$this->assertEquals('invalidparameter', $e->errorcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_attempts with extra grades
|
||||
*
|
||||
* @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
|
||||
*/
|
||||
public function test_get_user_attempts_with_extra_grades(): void {
|
||||
global $DB;
|
||||
|
||||
@@ -525,8 +553,10 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
@@ -537,8 +567,10 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
// Now change the review options, so marks are not displayed, and check the result.
|
||||
$DB->set_field('quiz', 'reviewmarks', 0, ['id' => $quiz->id]);
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
@@ -547,6 +579,8 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test get_user_attempts with marks hidden
|
||||
*
|
||||
* @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
|
||||
*/
|
||||
public function test_get_user_attempts_with_marks_hidden(): void {
|
||||
// Create quiz with one attempt finished and hide the mark.
|
||||
@@ -556,8 +590,10 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
// Student cannot see the grades.
|
||||
$this->setUser($this->student);
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
@@ -569,8 +605,10 @@ final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
// Test manager can see user grades.
|
||||
$this->setUser($this->teacher);
|
||||
$this->resetDebugging();
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
@@ -581,6 +619,288 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_attempts when the attempt is in 'submitted' state.
|
||||
*
|
||||
* @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
|
||||
* @covers \mod_quiz_external::get_user_attempts
|
||||
*/
|
||||
public function test_get_user_attempts_submitted(): void {
|
||||
|
||||
// Create a quiz with one attempt.
|
||||
[$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true);
|
||||
// Submit the attempt but do not finish it.
|
||||
// Process some responses from the student.
|
||||
$tosubmit = [1 => ['answer' => '3.14']];
|
||||
$attemptobj->process_submitted_actions(time(), false, $tosubmit);
|
||||
$attemptobj->process_submit(time(), false);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id);
|
||||
$this->assertDebuggingCalled();
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertNull($result['attempts'][0]['sumgrades']); // No grades.
|
||||
$this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']); // State is returned as finished.
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_attempts when the attempt is in 'notstarted' state. The attempt should not be returned.
|
||||
*
|
||||
* @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
|
||||
* @covers \mod_quiz_external::get_user_attempts
|
||||
*/
|
||||
public function test_get_user_attempts_notstarted(): void {
|
||||
// Create a quiz.
|
||||
[$quiz, , $quizobj, , ] = $this->create_quiz_with_questions();
|
||||
// Create an attempt but do not start it.
|
||||
// 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_not_started($quba, $attempt);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
|
||||
$this->assertDebuggingCalled();
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(0, $result['attempts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_quiz_user_attempts
|
||||
*
|
||||
* @covers \mod_quiz_external::get_user_quiz_attempts
|
||||
*/
|
||||
public function test_get_user_quiz_attempts(): void {
|
||||
|
||||
// Create a quiz with one attempt finished.
|
||||
[$quiz, , $quizobj, $attempt, ] = $this->create_quiz_with_questions(true, true);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
|
||||
$this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']);
|
||||
|
||||
// Test filters. Only finished.
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'finished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
|
||||
// Test filters. All attempts.
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'all', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
|
||||
// Test filters. Unfinished.
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'unfinished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(0, $result['attempts']);
|
||||
|
||||
// Start a new attempt, but not finish it.
|
||||
$timenow = time();
|
||||
$attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
|
||||
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
// Test filters. All attempts.
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'all', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(2, $result['attempts']);
|
||||
|
||||
// Test filters. Unfinished.
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'unfinished', false);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
|
||||
// Test manager can see user attempts.
|
||||
$this->setUser($this->teacher);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'all');
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(2, $result['attempts']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
|
||||
// Invalid parameters.
|
||||
try {
|
||||
mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
|
||||
$this->fail('Exception expected due to missing capability.');
|
||||
} catch (\invalid_parameter_exception $e) {
|
||||
$this->assertEquals('invalidparameter', $e->errorcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_quiz_attempts with extra grades
|
||||
*/
|
||||
public function test_get_user_quiz_attempts_with_extra_grades(): void {
|
||||
global $DB;
|
||||
|
||||
// Create a quiz with one attempt finished.
|
||||
[$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
|
||||
|
||||
// Add some extra grade items.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
|
||||
$readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
|
||||
$structure = $attemptobj->get_quizobj()->get_structure();
|
||||
$structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
|
||||
$structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
|
||||
// Verify additional grades.
|
||||
$this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][0]);
|
||||
$this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][1]);
|
||||
|
||||
// Now change the review options, so marks are not displayed, and check the result.
|
||||
$DB->set_field('quiz', 'reviewmarks', 0, ['id' => $quiz->id]);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertArrayNotHasKey('gradeitemmarks', $result['attempts'][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_quiz_attempts with marks hidden
|
||||
*
|
||||
* @covers \mod_quiz_external::get_user_quiz_attempts
|
||||
*/
|
||||
public function test_get_user_quiz_attempts_with_marks_hidden(): void {
|
||||
// Create quiz with one attempt finished and hide the mark.
|
||||
[$quiz, , , $attempt, ] = $this->create_quiz_with_questions(
|
||||
true, true, 'deferredfeedback', false,
|
||||
['marksduring' => 0, 'marksimmediately' => 0, 'marksopen' => 0, 'marksclosed' => 0]);
|
||||
|
||||
// Student cannot see the grades.
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertEquals(null, $result['attempts'][0]['sumgrades']);
|
||||
|
||||
// Test manager can see user grades.
|
||||
$this->setUser($this->teacher);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_quiz_attempts when the attempt is in 'submitted' state.
|
||||
*
|
||||
* @covers \mod_quiz_external::get_user_quiz_attempts
|
||||
*/
|
||||
public function test_get_user_quiz_attempts_submitted(): void {
|
||||
|
||||
// Create a quiz with one attempt.
|
||||
[$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true);
|
||||
// Submit the attempt but do not finish it.
|
||||
// Process some responses from the student.
|
||||
$tosubmit = [1 => ['answer' => '3.14']];
|
||||
$attemptobj->process_submitted_actions(time(), false, $tosubmit);
|
||||
$attemptobj->process_submit(time(), false);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertNull($result['attempts'][0]['sumgrades']); // No grades.
|
||||
$this->assertEquals(quiz_attempt::SUBMITTED, $result['attempts'][0]['state']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_quiz_attempts when the attempt is in 'notstarted' state.
|
||||
*
|
||||
* @covers \mod_quiz_external::get_user_quiz_attempts
|
||||
*/
|
||||
public function test_get_user_quiz_attempts_notstarted(): void {
|
||||
// Create a quiz.
|
||||
[$quiz, , $quizobj, , ] = $this->create_quiz_with_questions();
|
||||
// Create an attempt but do not start it.
|
||||
// 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_not_started($quba, $attempt);
|
||||
|
||||
$this->setUser($this->student);
|
||||
$result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'all');
|
||||
$result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
|
||||
|
||||
$this->assertCount(1, $result['attempts']);
|
||||
$this->assertEquals($attempt->id, $result['attempts'][0]['id']);
|
||||
$this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
|
||||
$this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
|
||||
$this->assertEquals(1, $result['attempts'][0]['attempt']);
|
||||
$this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
|
||||
$this->assertNull($result['attempts'][0]['sumgrades']);
|
||||
$this->assertEquals(quiz_attempt::NOT_STARTED, $result['attempts'][0]['state']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_best_grade
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user