From 9fa0e253aedd48da078543e346d28d24af2e0a49 Mon Sep 17 00:00:00 2001 From: cescobedo Date: Thu, 21 May 2020 21:13:37 +0200 Subject: [PATCH 1/1] MDL-68448 mod_h5pactivity: Add new ws get_h5pactivities_by_courses This WS is required by the Moodle app in order to fetch the activity information for displaying it to app users. --- .../external/get_h5pactivities_by_courses.php | 136 ++++++++++ .../external/h5pactivity_summary_exporter.php | 241 ++++++++++++++++++ mod/h5pactivity/db/services.php | 11 + .../get_h5pactivities_by_courses_test.php | 182 +++++++++++++ 4 files changed, 570 insertions(+) create mode 100644 mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php create mode 100644 mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php create mode 100644 mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php diff --git a/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php b/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php new file mode 100644 index 00000000000..e1546dd1029 --- /dev/null +++ b/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php @@ -0,0 +1,136 @@ +. + +/** + * This is the external method for returning a list of h5p activities. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @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 external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; +use external_util; +use external_warnings; +use context_module; +use core_h5p\factory; + +/** + * This is the external method for returning a list of h5p activities. + * + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_h5pactivities_by_courses extends external_api { + /** + * Parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters ( + [ + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, [] + ), + ] + ); + } + + /** + * Returns a list of h5p activities in a provided list of courses. + * If no list is provided all h5p activities that the user can view will be returned. + * + * @param array $courseids course ids + * @return array of h5p activities and warnings + * @since Moodle 3.9 + */ + public static function execute(array $courseids): array { + global $PAGE; + + $warnings = []; + $returnedh5pactivities = []; + + $params = external_api::validate_parameters(self::execute_parameters(), [ + 'courseids' => $courseids + ]); + + $mycourses = []; + if (empty($params['courseids'])) { + $mycourses = enrol_get_my_courses(); + $params['courseids'] = array_keys($mycourses); + } + + // Ensure there are courseids to loop through. + if (!empty($params['courseids'])) { + + $factory = new factory(); + + list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses); + $output = $PAGE->get_renderer('core'); + + // Get the h5p activities in this course, this function checks users visibility permissions. + // We can avoid then additional validate_context calls. + $h5pactivities = get_all_instances_in_courses('h5pactivity', $courses); + foreach ($h5pactivities as $h5pactivity) { + $context = context_module::instance($h5pactivity->coursemodule); + // Remove fields that are not from the h5p activity (added by get_all_instances_in_courses). + unset($h5pactivity->coursemodule, $h5pactivity->context, + $h5pactivity->visible, $h5pactivity->section, + $h5pactivity->groupmode, $h5pactivity->groupingid); + + $exporter = new h5pactivity_summary_exporter($h5pactivity, + ['context' => $context, 'factory' => $factory]); + $summary = $exporter->export($output); + $returnedh5pactivities[] = $summary; + } + } + + $result = [ + 'h5pactivities' => $returnedh5pactivities, + 'warnings' => $warnings + ]; + return $result; + } + + /** + * Describes the get_h5pactivities_by_courses return value. + * + * @return external_single_structure + * @since Moodle 3.9 + */ + public static function execute_returns() { + return new external_single_structure( + [ + 'h5pactivities' => new external_multiple_structure( + h5pactivity_summary_exporter::get_read_structure() + ), + 'warnings' => new external_warnings(), + ] + ); + } +} \ No newline at end of file diff --git a/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php b/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php new file mode 100644 index 00000000000..0088715be4b --- /dev/null +++ b/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php @@ -0,0 +1,241 @@ +. + +/** + * Class for exporting h5p activity data. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_h5pactivity\external; + +use core\external\exporter; +use renderer_base; +use external_util; +use external_files; +use core_h5p\factory; +use core_h5p\api; + +/** + * Class for exporting h5p activity data. + * + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class h5pactivity_summary_exporter extends exporter { + + /** + * Properties definition. + * + * @return array + */ + protected static function define_properties() { + + return [ + 'id' => [ + 'type' => PARAM_INT, + 'description' => 'The primary key of the record.', + ], + 'course' => [ + 'type' => PARAM_INT, + 'description' => 'Course id this h5p activity is part of.', + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'description' => 'The name of the activity module instance.', + ], + 'timecreated' => [ + 'type' => PARAM_INT, + 'description' => 'Timestamp of when the instance was added to the course.', + 'optional' => true, + ], + 'timemodified' => [ + 'type' => PARAM_INT, + 'description' => 'Timestamp of when the instance was last modified.', + 'optional' => true, + ], + 'intro' => [ + 'default' => '', + 'type' => PARAM_RAW, + 'description' => 'H5P activity description.', + 'null' => NULL_ALLOWED, + ], + 'introformat' => [ + 'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN], + 'type' => PARAM_INT, + 'default' => FORMAT_MOODLE, + 'description' => 'The format of the intro field.', + ], + 'grade' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'description' => 'The maximum grade for submission.', + 'optional' => true, + ], + 'displayoptions' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'description' => 'H5P Button display options.', + ], + 'enabletracking' => [ + 'type' => PARAM_INT, + 'default' => 1, + 'description' => 'Enable xAPI tracking.', + ], + 'grademethod' => [ + 'type' => PARAM_INT, + 'default' => 1, + 'description' => 'Which H5P attempt is used for grading.', + ], + 'contenthash' => [ + 'type' => PARAM_ALPHANUM, + 'description' => 'Sha1 hash of file content.', + 'optional' => true, + ], + ]; + } + + /** + * Related objects definition. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + 'factory' => 'core_h5p\\factory' + ]; + } + + /** + * Other properties definition. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'coursemodule' => [ + 'type' => PARAM_INT + ], + 'introfiles' => [ + 'type' => external_files::get_properties_for_exporter(), + 'multiple' => true + ], + 'package' => [ + 'type' => external_files::get_properties_for_exporter(), + 'multiple' => true + ], + 'deployedfile' => [ + 'optional' => true, + 'description' => 'H5P file deployed.', + 'type' => [ + 'filename' => array( + 'type' => PARAM_FILE, + 'description' => 'File name.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'filepath' => array( + 'type' => PARAM_PATH, + 'description' => 'File path.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'filesize' => array( + 'type' => PARAM_INT, + 'description' => 'File size.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'fileurl' => array( + 'type' => PARAM_URL, + 'description' => 'Downloadable file url.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'description' => 'Time modified.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'mimetype' => array( + 'type' => PARAM_RAW, + 'description' => 'File mime type.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ) + ] + ], + ]; + } + + /** + * Assign values to the defined other properties. + * + * @param renderer_base $output The output renderer object. + * @return array + */ + protected function get_other_values(renderer_base $output) { + $context = $this->related['context']; + $factory = $this->related['factory']; + + $values = [ + 'coursemodule' => $context->instanceid, + ]; + + $values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false); + + $values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, false); + + // Only if this H5P activity has been deployed, return the exported file. + $fileh5p = api::get_export_info_from_context_id($context->id, $factory, 'mod_h5pactivity', 'package'); + if ($fileh5p) { + $values['deployedfile'] = $fileh5p; + } + + return $values; + } + + /** + * Get the formatting parameters for the intro. + * + * @return array with the formatting parameters + */ + protected function get_format_parameters_for_intro() { + return [ + 'component' => 'mod_h5pactivity', + 'filearea' => 'intro', + 'options' => ['noclean' => true], + ]; + } + + /** + * Get the formatting parameters for the package. + * + * @return array with the formatting parameters + */ + protected function get_format_parameters_for_package() { + return [ + 'component' => 'mod_h5pactivity', + 'filearea' => 'package', + 'itemid' => 0, + 'options' => ['noclean' => true], + ]; + } +} diff --git a/mod/h5pactivity/db/services.php b/mod/h5pactivity/db/services.php index db62e5e35ba..ee50c8fa000 100644 --- a/mod/h5pactivity/db/services.php +++ b/mod/h5pactivity/db/services.php @@ -53,4 +53,15 @@ $functions = [ 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'mod_h5pactivity_get_h5pactivities_by_courses' => [ + 'classname' => 'mod_h5pactivity\external\get_h5pactivities_by_courses', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Returns a list of h5p activities in a list of + provided courses, if no list is provided all h5p activities + that the user can view will be returned.', + 'type' => 'read', + 'capabilities' => 'mod/h5pactivity:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], ]; diff --git a/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php b/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php new file mode 100644 index 00000000000..62050d2ce1b --- /dev/null +++ b/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php @@ -0,0 +1,182 @@ +. + +/** + * External function test for get_h5pactivities_by_courses. + * + * @package mod_h5pactivity + * @category external + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @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 external_api; +use externallib_advanced_testcase; +use stdClass; +use context_module; + +/** + * External function test for get_h5pactivities_by_courses. + * + * @package mod_h5pactivity + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_h5pactivities_by_courses_testcase extends externallib_advanced_testcase { + + /** + * Test test_get_h5pactivities_by_courses user student. + */ + public function test_get_h5pactivities_by_courses() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create 2 courses. + // Course 1 -> 2 activities with H5P files package without deploy. + // Course 2 -> 1 activity with H5P file package deployed. + $course1 = $this->getDataGenerator()->create_course(); + $params = [ + 'course' => $course1->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + // Add filename to make easier the asserts. + $activities[0]->filename = 'filltheblanks.h5p'; + $params = [ + 'course' => $course1->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card-887.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + // Add filename to make easier the asserts. + $activities[1]->filename = 'greeting-card-887.h5p'; + + $course2 = $this->getDataGenerator()->create_course(); + $params = [ + 'course' => $course2->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/guess-the-answer.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + $activities[2]->filename = 'guess-the-answer.h5p'; + + $context = context_module::instance($activities[2]->cmid); + // Create a fake deploy H5P file. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package'); + + // Create a user and enrol as student in both courses. + $user = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $maninstance1 = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'manual'], '*', MUST_EXIST); + $maninstance2 = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'manual'], '*', MUST_EXIST); + $manual = enrol_get_plugin('manual'); + $manual->enrol_user($maninstance1, $user->id, $studentrole->id); + $manual->enrol_user($maninstance2, $user->id, $studentrole->id); + + // Check the activities returned by the first course. + $this->setUser($user); + $courseids = [$course1->id]; + $result = get_h5pactivities_by_courses::execute($courseids); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(2, $result['h5pactivities']); + $this->assert_activities($activities, $result); + $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); + $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); + + // Call the external function without passing course id. + // Expected result, all the courses, course1 and course2. + $result = get_h5pactivities_by_courses::execute([]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(3, $result['h5pactivities']); + // We need to sort the $result by id. + // Because we are not sure how it is ordered with more than one course. + array_multisort(array_map(function($element) { + return $element['id']; + }, $result['h5pactivities']), SORT_ASC, $result['h5pactivities']); + $this->assert_activities($activities, $result); + $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); + $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); + // Only the activity from the second course has been deployed. + $this->assertEquals($deployedfile['filename'], $result['h5pactivities'][2]['deployedfile']['filename']); + $this->assertEquals($deployedfile['filepath'], $result['h5pactivities'][2]['deployedfile']['filepath']); + $this->assertEquals($deployedfile['filesize'], $result['h5pactivities'][2]['deployedfile']['filesize']); + $this->assertEquals($deployedfile['timemodified'], $result['h5pactivities'][2]['deployedfile']['timemodified']); + $this->assertEquals($deployedfile['mimetype'], $result['h5pactivities'][2]['deployedfile']['mimetype']); + $this->assertEquals($deployedfile['fileurl'], $result['h5pactivities'][2]['deployedfile']['fileurl']); + + // Unenrol user from second course. + $manual->unenrol_user($maninstance2, $user->id); + // Remove the last activity from the array. + array_pop($activities); + + // Call the external function without passing course id. + $result = get_h5pactivities_by_courses::execute([]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(2, $result['h5pactivities']); + $this->assert_activities($activities, $result); + + // Call for the second course we unenrolled the user from, expected warning. + $result = get_h5pactivities_by_courses::execute([$course2->id]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(1, $result['warnings']); + $this->assertEquals('1', $result['warnings'][0]['warningcode']); + $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); + } + + /** + * Create a scenario to use into the tests. + * + * @param array $activities list of H5P activities. + * @param array $result list of H5P activities by WS. + * @return void + */ + protected function assert_activities(array $activities, array $result): void { + + $total = count($result); + for ($i = 0; $i < $total; $i++) { + $this->assertEquals($activities[$i]->id, $result['h5pactivities'][$i]['id']); + $this->assertEquals($activities[$i]->course, $result['h5pactivities'][$i]['course']); + $this->assertEquals($activities[$i]->name, $result['h5pactivities'][$i]['name']); + $this->assertEquals($activities[$i]->timecreated, $result['h5pactivities'][$i]['timecreated']); + $this->assertEquals($activities[$i]->timemodified, $result['h5pactivities'][$i]['timemodified']); + $this->assertEquals($activities[$i]->intro, $result['h5pactivities'][$i]['intro']); + $this->assertEquals($activities[$i]->introformat, $result['h5pactivities'][$i]['introformat']); + $this->assertEquals([], $result['h5pactivities'][$i]['introfiles']); + $this->assertEquals($activities[$i]->grade, $result['h5pactivities'][$i]['grade']); + $this->assertEquals($activities[$i]->displayoptions, $result['h5pactivities'][$i]['displayoptions']); + $this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']); + $this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']); + $this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']); + $this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']); + } + } +} -- 2.43.0