diff --git a/mod/h5pactivity/classes/external/get_results.php b/mod/h5pactivity/classes/external/get_results.php new file mode 100644 index 00000000000..a37ab62afef --- /dev/null +++ b/mod/h5pactivity/classes/external/get_results.php @@ -0,0 +1,303 @@ +. + +/** + * This is the external method for getting the information needed to present a results report. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +use mod_h5pactivity\local\manager; +use mod_h5pactivity\local\report\results as report_results; +use external_api; +use external_function_parameters; +use external_value; +use external_multiple_structure; +use external_single_structure; +use external_warnings; +use moodle_exception; +use context_module; +use stdClass; + +/** + * This is the external method for getting the information needed to present a results report. + * + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_results extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'), + 'attemptids' => new external_multiple_structure( + new external_value(PARAM_INT, 'The attempt id'), + 'Attempt ids', VALUE_DEFAULT, [] + ), + ] + ); + } + + /** + * Return user attempts results information in a h5p activity. + * + * In case an empty array of attempt ids is passed, the method will load all + * activity attempts from the current user. + * + * @throws moodle_exception if the user cannot see the report + * @param int $h5pactivityid The h5p activity id + * @param int[] $attemptids The attempt ids + * @return stdClass report data + */ + public static function execute(int $h5pactivityid, array $attemptids = []): stdClass { + global $USER; + + $params = external_api::validate_parameters(self::execute_parameters(), [ + 'h5pactivityid' => $h5pactivityid, + 'attemptids' => $attemptids, + ]); + $h5pactivityid = $params['h5pactivityid']; + $attemptids = $params['attemptids']; + + $warnings = []; + + // Request and permission validation. + list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity'); + + $context = context_module::instance($cm->id); + self::validate_context($context); + + $manager = manager::create_from_coursemodule($cm); + + if (empty($attemptids)) { + $attemptids = []; + foreach ($manager->get_user_attempts($USER->id) as $attempt) { + $attemptids[] = $attempt->get_id(); + } + } + + $attempts = []; + foreach ($attemptids as $attemptid) { + $report = $manager->get_report(null, $attemptid); + + if ($report && $report instanceof report_results) { + $attempts[] = self::export_attempt($report); + } else { + $warnings[] = [ + 'item' => 'h5pactivity_attempts', + 'itemid' => $attemptid, + 'warningcode' => '1', + 'message' => "Cannot access attempt", + ]; + } + } + + $result = (object)[ + 'activityid' => $h5pactivityid, + 'attempts' => $attempts, + 'warnings' => $warnings, + ]; + + return $result; + } + + /** + * Return a data object from an attempt. + * + * @param report_results $report the attempt data + * @return stdClass a WS compatible version of the attempt + */ + private static function export_attempt(report_results $report): stdClass { + + $data = $report->export_data_for_external(); + + $attemptdata = $data->attempt; + + $attempt = (object)[ + 'id' => $attemptdata->id, + 'h5pactivityid' => $attemptdata->h5pactivityid, + 'userid' => $attemptdata->userid, + 'timecreated' => $attemptdata->timecreated, + 'timemodified' => $attemptdata->timemodified, + 'attempt' => $attemptdata->attempt, + 'rawscore' => $attemptdata->rawscore, + 'maxscore' => $attemptdata->maxscore, + 'duration' => (empty($attemptdata->duration)) ? 0 : $attemptdata->duration, + 'scaled' => (empty($attemptdata->scaled)) ? 0 : $attemptdata->scaled, + 'results' => [], + ]; + if (isset($attemptdata->completion) && $attemptdata->completion !== null) { + $attempt->completion = $attemptdata->completion; + } + if (isset($attemptdata->success) && $attemptdata->success !== null) { + $attempt->success = $attemptdata->success; + } + foreach ($data->results as $result) { + $attempt->results[] = self::export_result($result); + } + return $attempt; + } + + /** + * Return a data object from a result. + * + * @param stdClass $data the result data + * @return stdClass a WS compatible version of the result + */ + private static function export_result(stdClass $data): stdClass { + $result = (object)[ + 'id' => $data->id, + 'attemptid' => $data->attemptid, + 'subcontent' => $data->subcontent, + 'timecreated' => $data->timecreated, + 'interactiontype' => $data->interactiontype, + 'description' => $data->description, + 'rawscore' => $data->rawscore, + 'maxscore' => $data->maxscore, + 'duration' => $data->duration, + 'optionslabel' => $data->optionslabel ?? get_string('choice', 'mod_h5pactivity'), + 'correctlabel' => $data->correctlabel ?? get_string('correct_answer', 'mod_h5pactivity'), + 'answerlabel' => $data->answerlabel ?? get_string('attempt_answer', 'mod_h5pactivity'), + 'track' => $data->track ?? false, + ]; + if (isset($data->completion) && $data->completion !== null) { + $result->completion = $data->completion; + } + if (isset($data->success) && $data->success !== null) { + $result->success = $data->success; + } + if (isset($data->options)) { + $result->options = $data->options; + } + if (isset($data->content)) { + $result->content = $data->content; + } + return $result; + } + + /** + * Describes the get_h5pactivity_access_information return value. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'activityid' => new external_value(PARAM_INT, 'Activity course module ID'), + 'attempts' => new external_multiple_structure( + self::get_attempt_returns(), 'The complete attempts list' + ), + 'warnings' => new external_warnings(), + ], 'Activity attempts results data'); + } + + /** + * Return the external structure of an attempt + * @return external_single_structure + */ + private static function get_attempt_returns(): external_single_structure { + + $result = new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'ID of the context'), + 'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'), + 'userid' => new external_value(PARAM_INT, 'ID of the user'), + 'timecreated' => new external_value(PARAM_INT, 'Attempt creation'), + 'timemodified' => new external_value(PARAM_INT, 'Attempt modified'), + 'attempt' => new external_value(PARAM_INT, 'Attempt number'), + 'rawscore' => new external_value(PARAM_INT, 'Attempt score value'), + 'maxscore' => new external_value(PARAM_INT, 'Attempt max score'), + 'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'), + 'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL), + 'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL), + 'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'), + 'results' => new external_multiple_structure( + self::get_result_returns(), + 'The results of the attempt', VALUE_OPTIONAL + ), + ], 'The attempt general information'); + return $result; + } + + /** + * Return the external structure of a result + * @return external_single_structure + */ + private static function get_result_returns(): external_single_structure { + + $result = new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'ID of the context'), + 'attemptid' => new external_value(PARAM_INT, 'ID of the H5P attempt'), + 'subcontent' => new external_value(PARAM_NOTAGS, 'Subcontent identifier'), + 'timecreated' => new external_value(PARAM_INT, 'Result creation'), + 'interactiontype' => new external_value(PARAM_NOTAGS, 'Interaction type'), + 'description' => new external_value(PARAM_TEXT, 'Result description'), + 'rawscore' => new external_value(PARAM_INT, 'Result score value'), + 'maxscore' => new external_value(PARAM_INT, 'Result max score'), + 'duration' => new external_value(PARAM_INT, 'Result duration in seconds', VALUE_OPTIONAL, 0), + 'completion' => new external_value(PARAM_INT, 'Result completion', VALUE_OPTIONAL), + 'success' => new external_value(PARAM_INT, 'Result success', VALUE_OPTIONAL), + 'optionslabel' => new external_value(PARAM_NOTAGS, 'Label used for result options', VALUE_OPTIONAL), + 'correctlabel' => new external_value(PARAM_NOTAGS, 'Label used for correct answers', VALUE_OPTIONAL), + 'answerlabel' => new external_value(PARAM_NOTAGS, 'Label used for user answers', VALUE_OPTIONAL), + 'track' => new external_value(PARAM_BOOL, 'If the result has valid track information', VALUE_OPTIONAL), + 'options' => new external_multiple_structure( + new external_single_structure([ + 'description' => new external_value(PARAM_TEXT, 'Option description'), + 'id' => new external_value(PARAM_INT, 'Option identifier'), + 'correctanswer' => self::get_answer_returns('The option correct answer'), + 'useranswer' => self::get_answer_returns('The option user answer'), + ]), + 'The statement options', VALUE_OPTIONAL + ), + ], 'A single result statement tracking information'); + return $result; + } + + /** + * Return the external structure of an answer or correctanswer + * + * @param string $description the return description + * @return external_single_structure + */ + private static function get_answer_returns(string $description): external_single_structure { + + $result = new external_single_structure([ + 'answer' => new external_value(PARAM_NOTAGS, 'Option text value', VALUE_OPTIONAL), + 'correct' => new external_value(PARAM_BOOL, 'If has to be displayed as correct', VALUE_OPTIONAL), + 'incorrect' => new external_value(PARAM_BOOL, 'If has to be displayed as incorrect', VALUE_OPTIONAL), + 'text' => new external_value(PARAM_BOOL, 'If has to be displayed as simple text', VALUE_OPTIONAL), + 'checked' => new external_value(PARAM_BOOL, 'If has to be displayed as a checked option', VALUE_OPTIONAL), + 'unchecked' => new external_value(PARAM_BOOL, 'If has to be displayed as a unchecked option', VALUE_OPTIONAL), + 'pass' => new external_value(PARAM_BOOL, 'If has to be displayed as passed', VALUE_OPTIONAL), + 'fail' => new external_value(PARAM_BOOL, 'If has to be displayed as failed', VALUE_OPTIONAL), + ], $description); + return $result; + } +} diff --git a/mod/h5pactivity/classes/local/manager.php b/mod/h5pactivity/classes/local/manager.php index 4c471bf0da6..840de03f405 100644 --- a/mod/h5pactivity/classes/local/manager.php +++ b/mod/h5pactivity/classes/local/manager.php @@ -417,7 +417,10 @@ class manager { */ public function get_attempt(int $attemptid): ?attempt { global $DB; - $record = $DB->get_record('h5pactivity_attempts', ['id' => $attemptid]); + $record = $DB->get_record('h5pactivity_attempts', [ + 'id' => $attemptid, + 'h5pactivityid' => $this->instance->id, + ]); if (!$record) { return null; } diff --git a/mod/h5pactivity/classes/local/report/results.php b/mod/h5pactivity/classes/local/report/results.php index c5690f9c715..61fbb8b8423 100644 --- a/mod/h5pactivity/classes/local/report/results.php +++ b/mod/h5pactivity/classes/local/report/results.php @@ -95,4 +95,20 @@ class results implements report { $widget = new reportresults($attempt, $this->user, $cm->course); echo $OUTPUT->render($widget); } + + /** + * Get the export data form this report. + * + * This method is used to render the report in mobile. + */ + public function export_data_for_external(): stdClass { + global $PAGE; + + $manager = $this->manager; + $attempt = $this->attempt; + $cm = $manager->get_coursemodule(); + + $widget = new reportresults($attempt, $this->user, $cm->course); + return $widget->export_for_template($PAGE->get_renderer('core')); + } } diff --git a/mod/h5pactivity/db/services.php b/mod/h5pactivity/db/services.php index db62e5e35ba..904df636692 100644 --- a/mod/h5pactivity/db/services.php +++ b/mod/h5pactivity/db/services.php @@ -53,4 +53,13 @@ $functions = [ 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'mod_h5pactivity_get_results' => [ + 'classname' => 'mod_h5pactivity\external\get_results', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Return the information needed to list a user attempt results.', + 'type' => 'read', + 'capabilities' => 'mod/h5pactivity:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], ]; diff --git a/mod/h5pactivity/tests/external/get_results_test.php b/mod/h5pactivity/tests/external/get_results_test.php new file mode 100644 index 00000000000..0a76597ae6f --- /dev/null +++ b/mod/h5pactivity/tests/external/get_results_test.php @@ -0,0 +1,428 @@ +. + +/** + * External function test for get_results. + * + * @package mod_h5pactivity + * @category external + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use mod_h5pactivity\local\manager; +use external_api; +use externallib_advanced_testcase; +use dml_missing_record_exception; + +/** + * External function test for get_results. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_results_testcase extends externallib_advanced_testcase { + + /** + * Test the behaviour of get_results. + * + * @dataProvider execute_data + * @param int $enabletracking the activity tracking enable + * @param int $reviewmode the activity review mode + * @param string $loginuser the user which calls the webservice + * @param string|null $participant the user to get the data + * @param bool $createattempts if the student user has attempts created + * @param int|null $count the expected number of attempts returned (null for exception) + */ + public function test_execute(int $enabletracking, int $reviewmode, string $loginuser, + ?string $participant, bool $createattempts, ?int $count): void { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]); + + $manager = manager::create_from_instance($activity); + $cm = $manager->get_coursemodule(); + + // Prepare users: 1 teacher, 1 student and 1 unenroled user. + $users = [ + 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), + 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + 'other' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + ]; + + $attempts = []; + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + if ($createattempts) { + $user = $users['student']; + $params = ['cmid' => $cm->id, 'userid' => $user->id]; + $attempts['student'] = $generator->create_content($activity, $params); + } + + // Create another 2 attempts for the user "other" to validate no cross attempts are returned. + $user = $users['other']; + $params = ['cmid' => $cm->id, 'userid' => $user->id]; + $attempts['other'] = $generator->create_content($activity, $params); + + // Execute external method. + $this->setUser($users[$loginuser]); + + $attemptid = $attempts[$participant]->id ?? 0; + + $result = get_results::execute($activity->id, [$attemptid]); + $result = external_api::clean_returnvalue( + get_results::execute_returns(), + $result + ); + + // Validate general structure. + $this->assertArrayHasKey('activityid', $result); + $this->assertArrayHasKey('attempts', $result); + $this->assertArrayHasKey('warnings', $result); + + $this->assertEquals($activity->id, $result['activityid']); + + if ($count === null) { + $this->assertCount(1, $result['warnings']); + $this->assertCount(0, $result['attempts']); + return; + } + + $this->assertCount(0, $result['warnings']); + $this->assertCount(1, $result['attempts']); + + // Validate attempt. + $attempt = $result['attempts'][0]; + $this->assertEquals($attemptid, $attempt['id']); + + // Validate results. + $this->assertArrayHasKey('results', $attempt); + $this->assertCount($count, $attempt['results']); + foreach ($attempt['results'] as $value) { + $this->assertEquals($attemptid, $value['attemptid']); + $this->assertArrayHasKey('subcontent', $value); + $this->assertArrayHasKey('rawscore', $value); + $this->assertArrayHasKey('maxscore', $value); + $this->assertArrayHasKey('duration', $value); + $this->assertArrayHasKey('track', $value); + if (isset($value['options'])) { + foreach ($value['options'] as $option) { + $this->assertArrayHasKey('description', $option); + $this->assertArrayHasKey('id', $option); + } + } + } + } + + /** + * Data provider for the test_execute tests. + * + * @return array + */ + public function execute_data(): array { + return [ + 'Teacher reviewing an attempt' => [ + 1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', true, 1 + ], + 'Teacher try to review an inexistent attempt' => [ + 1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', false, null + ], + 'Teacher reviewing attempt with student review mode off' => [ + 1, manager::REVIEWNONE, 'editingteacher', 'student', true, 1 + ], + 'Student reviewing own attempt' => [ + 1, manager::REVIEWCOMPLETION, 'student', 'student', true, 1 + ], + 'Student reviewing an inexistent attempt' => [ + 1, manager::REVIEWCOMPLETION, 'student', 'student', false, null + ], + 'Student reviewing own attempt with review mode off' => [ + 1, manager::REVIEWNONE, 'student', 'student', true, null + ], + 'Student try to stalk other student attempt' => [ + 1, manager::REVIEWCOMPLETION, 'student', 'other', false, null + ], + 'Teacher trying to review an attempt without tracking enabled' => [ + 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null + ], + 'Student trying to review an attempt without tracking enabled' => [ + 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null + ], + 'Student trying to stalk another student attempt without tracking enabled' => [ + 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null + ], + ]; + } + + /** + * Test the behaviour of get_results. + * + * @dataProvider execute_multipleattempts_data + * @param string $loginuser the user which calls the webservice + * @param array $getattempts the attempts to get the data + * @param array $warnings warnigns expected + * @param array $reports data expected + * + */ + public function test_execute_multipleattempts(string $loginuser, + array $getattempts, array $warnings, array $reports): void { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); + + $manager = manager::create_from_instance($activity); + $cm = $manager->get_coursemodule(); + + // Prepare users: 1 teacher, 2 student. + $users = [ + 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), + 'student1' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + 'student2' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + ]; + + $attempts = []; + + // Generate attempts for student 1 and 2. + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + $user = $users['student1']; + $params = ['cmid' => $cm->id, 'userid' => $user->id]; + $attempts['student1_1'] = $generator->create_content($activity, $params); + $attempts['student1_2'] = $generator->create_content($activity, $params); + + $user = $users['student2']; + $params = ['cmid' => $cm->id, 'userid' => $user->id]; + $attempts['student2_1'] = $generator->create_content($activity, $params); + $attempts['student2_2'] = $generator->create_content($activity, $params); + + // Execute external method. + $this->setUser($users[$loginuser]); + + $attemptids = []; + foreach ($getattempts as $getattempt) { + $attemptids[] = $attempts[$getattempt]->id ?? 0; + } + + $result = get_results::execute($activity->id, $attemptids); + $result = external_api::clean_returnvalue( + get_results::execute_returns(), + $result + ); + + // Validate general structure. + $this->assertArrayHasKey('activityid', $result); + $this->assertArrayHasKey('attempts', $result); + $this->assertArrayHasKey('warnings', $result); + + $this->assertEquals($activity->id, $result['activityid']); + + $this->assertCount(count($warnings), $result['warnings']); + $this->assertCount(count($reports), $result['attempts']); + + // Validate warnings. + $expectedwarnings = []; + foreach ($warnings as $warningattempt) { + $id = $attempts[$warningattempt]->id ?? 0; + $expectedwarnings[$id] = $warningattempt; + } + foreach ($result['warnings'] as $warning) { + $this->assertEquals('h5pactivity_attempts', $warning['item']); + $this->assertEquals(1, $warning['warningcode']); + $this->assertArrayHasKey($warning['itemid'], $expectedwarnings); + } + + // Validate attempts. + $expectedattempts = []; + foreach ($reports as $expectedattempt) { + $id = $attempts[$expectedattempt]->id; + $expectedattempts[$id] = $expectedattempt; + } + foreach ($result['attempts'] as $value) { + $this->assertArrayHasKey($value['id'], $expectedattempts); + } + } + + /** + * Data provider for the test_execute_multipleattempts tests. + * + * @return array + */ + public function execute_multipleattempts_data(): array { + return [ + // Teacher cases. + 'Teacher reviewing students attempts' => [ + 'editingteacher', ['student1_1', 'student2_1'], [], ['student1_1', 'student2_1'] + ], + 'Teacher reviewing invalid attempt' => [ + 'editingteacher', ['student1_1', 'invalid'], ['invalid'], ['student1_1'] + ], + 'Teacher reviewing empty attempts list' => [ + 'editingteacher', [], [], [] + ], + // Student cases. + 'Student reviewing own students attempts' => [ + 'student1', ['student1_1', 'student1_2'], [], ['student1_1', 'student1_2'] + ], + 'Student reviewing invalid attempt' => [ + 'student1', ['student1_1', 'invalid'], ['invalid'], ['student1_1'] + ], + 'Student reviewing trying to access another user attempts' => [ + 'student1', ['student1_1', 'student2_1'], ['student2_1'], ['student1_1'] + ], + 'Student reviewing empty attempts list' => [ + 'student1', [], [], ['student1_1', 'student1_2'] + ], + ]; + } + + /** + * Test the behaviour of get_results using mixed activityid. + * + * @dataProvider execute_mixactivities_data + * @param string $activityname the activity name to use + * @param string $attemptname the attempt name to use + * @param string $expectedwarnings expected warning attempt + * @param string $expectedattempt expected result attempt + * + */ + public function test_execute_mixactivities(string $activityname, string $attemptname, + string $expectedwarnings, string $expectedattempt): void { + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create 2 courses. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + + // Prepare users: 1 teacher, 1 student. + $user = $this->getDataGenerator()->create_and_enrol($course1, 'student'); + $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student'); + + // Create our base activity. + $activity11 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]); + $manager11 = manager::create_from_instance($activity11); + $cm11 = $manager11->get_coursemodule(); + + // Create a second activity in the same course to check if the retuned attempt is the correct one. + $activity12 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]); + $manager12 = manager::create_from_instance($activity12); + $cm12 = $manager12->get_coursemodule(); + + // Create a second activity on a different course. + $activity21 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course2]); + $manager21 = manager::create_from_instance($activity21); + $cm21 = $manager21->get_coursemodule(); + + $activities = [ + '11' => $activity11->id, + '12' => $activity12->id, + '21' => $activity21->id, + 'inexistent' => 0, + ]; + + // Generate attempts. + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + $params = ['cmid' => $cm11->id, 'userid' => $user->id]; + $attempt11 = $generator->create_content($activity11, $params); + $params = ['cmid' => $cm12->id, 'userid' => $user->id]; + $attempt12 = $generator->create_content($activity12, $params); + $params = ['cmid' => $cm21->id, 'userid' => $user->id]; + $attempt21 = $generator->create_content($activity21, $params); + + $attempts = [ + '11' => $attempt11->id, + '12' => $attempt12->id, + '21' => $attempt21->id, + 'inexistent' => 0, + ]; + + if ($activityname == 'inexistent') { + $this->expectException(dml_missing_record_exception::class); + } + + // Execute external method. + $this->setUser($user); + + $attemptid = $attempts[$attemptname]; + + $result = get_results::execute($activities[$activityname], [$attemptid]); + $result = external_api::clean_returnvalue( + get_results::execute_returns(), + $result + ); + + // Validate general structure. + $this->assertArrayHasKey('activityid', $result); + $this->assertArrayHasKey('attempts', $result); + $this->assertArrayHasKey('warnings', $result); + + if (empty($expectedwarnings)) { + $this->assertEmpty($result['warnings']); + } else { + $this->assertEquals('h5pactivity_attempts', $result['warnings'][0]['item']); + $this->assertEquals(1, $result['warnings'][0]['warningcode']); + $this->assertEquals($attempts[$expectedwarnings], $result['warnings'][0]['itemid']); + } + + if (empty($expectedattempt)) { + $this->assertEmpty($result['attempts']); + } else { + $this->assertEquals($attempts[$expectedattempt], $result['attempts'][0]['id']); + } + } + + /** + * Data provider for the test_execute_multipleattempts tests. + * + * @return array + */ + public function execute_mixactivities_data(): array { + return [ + // Teacher cases. + 'Correct activity id' => [ + '11', '11', '', '11' + ], + 'Wrong activity id' => [ + '21', '11', '11', '' + ], + 'Inexistent activity id' => [ + 'inexistent', '11', '', '' + ], + 'Inexistent attempt id' => [ + '11', 'inexistent', 'inexistent', '' + ], + ]; + } +} diff --git a/mod/h5pactivity/tests/generator/lib.php b/mod/h5pactivity/tests/generator/lib.php index cabec2fedd3..c5516326bbb 100644 --- a/mod/h5pactivity/tests/generator/lib.php +++ b/mod/h5pactivity/tests/generator/lib.php @@ -165,6 +165,7 @@ class mod_h5pactivity_generator extends testing_module_generator { $result->subcontent = '14fcc986-728b-47f3-915b-'.$userid; $result->interactiontype = 'matching'; + $result->correctpattern = '["0[.]1[,]1[.]0[,]2[.]2"]'; $result->response = '1[.]0[,]0[.]1[,]2[.]2'; $result->additionals = '{"source":[{"id":"0","description":{"en-US":"A berry"}}'. ',{"id":"1","description":{"en-US":"An orange berry"}},'. diff --git a/mod/h5pactivity/tests/local/manager_test.php b/mod/h5pactivity/tests/local/manager_test.php index ff2e8b6149f..bbbf9eac79f 100644 --- a/mod/h5pactivity/tests/local/manager_test.php +++ b/mod/h5pactivity/tests/local/manager_test.php @@ -738,6 +738,70 @@ class manager_testcase extends \advanced_testcase { ]; } + /** + * Test get_attempt method. + * + * @dataProvider get_attempt_data + * @param string $attemptname the attempt to use + * @param string|null $result the expected attempt ID or null for none + */ + public function test_get_attempt(string $attemptname, ?string $result): void { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); + $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); + + $otheractivity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); + $othercm = get_coursemodule_from_id('h5pactivity', $otheractivity->cmid, 0, false, MUST_EXIST); + + $manager = manager::create_from_instance($activity); + + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $attempts = ['inexistent' => 0]; + + $this->generate_fake_attempts($activity, $user, 1); + $attempt = attempt::last_attempt($user, $cm); + $attempts['current'] = $attempt->get_id(); + + $this->generate_fake_attempts($otheractivity, $user, 1); + $attempt = attempt::last_attempt($user, $othercm); + $attempts['other'] = $attempt->get_id(); + + $attempt = $manager->get_attempt($attempts[$attemptname]); + if ($result === null) { + $this->assertNull($attempt); + } else { + $this->assertEquals($attempts[$attemptname], $attempt->get_id()); + $this->assertEquals($activity->id, $attempt->get_h5pactivityid()); + $this->assertEquals($user->id, $attempt->get_userid()); + $this->assertEquals(4, $attempt->get_attempt()); + } + } + + /** + * Data provider for test_get_attempt. + * + * @return array + */ + public function get_attempt_data(): array { + return [ + 'Get the current activity attempt' => [ + 'current', 'current' + ], + 'Try to get another activity attempt' => [ + 'other', null + ], + 'Try to get an inexistent attempt' => [ + 'inexistent', null + ], + ]; + } + /** * Insert fake attempt data into h5pactiviyt_attempts. * diff --git a/mod/h5pactivity/version.php b/mod/h5pactivity/version.php index 91081ce2060..6bfe3b6bf81 100644 --- a/mod/h5pactivity/version.php +++ b/mod/h5pactivity/version.php @@ -25,5 +25,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_h5pactivity'; -$plugin->version = 2020052000; +$plugin->version = 2020052100; $plugin->requires = 2020013000;