From 1e7e255d720d4a330b884ffd9c264380cdc5a86f Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 5 Feb 2020 16:57:11 +0100 Subject: [PATCH 1/5] MDL-67707 course: add support for PARAM_ALPHANUM module names --- course/mod.php | 2 +- course/modedit.php | 2 +- course/moodleform_mod.php | 2 +- course/upgrade.txt | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/course/mod.php b/course/mod.php index 54f6f9cb5a2..67719a7aae2 100644 --- a/course/mod.php +++ b/course/mod.php @@ -27,7 +27,7 @@ require("../config.php"); require_once("lib.php"); $sectionreturn = optional_param('sr', null, PARAM_INT); -$add = optional_param('add', '', PARAM_ALPHA); +$add = optional_param('add', '', PARAM_ALPHANUM); $type = optional_param('type', '', PARAM_ALPHA); $indent = optional_param('indent', 0, PARAM_INT); $update = optional_param('update', 0, PARAM_INT); diff --git a/course/modedit.php b/course/modedit.php index f048bcd25e4..0388708c9a0 100644 --- a/course/modedit.php +++ b/course/modedit.php @@ -31,7 +31,7 @@ require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/plagiarismlib.php'); require_once($CFG->dirroot . '/course/modlib.php'); -$add = optional_param('add', '', PARAM_ALPHA); // module name +$add = optional_param('add', '', PARAM_ALPHANUM); // Module name. $update = optional_param('update', 0, PARAM_INT); $return = optional_param('return', 0, PARAM_BOOL); //return to course/view.php if false or mod/modname/view.php if true $type = optional_param('type', '', PARAM_ALPHANUM); //TODO: hopefully will be removed in 2.0 diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index 34c25f04735..2bbdac012dc 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -942,7 +942,7 @@ abstract class moodleform_mod extends moodleform { $mform->setType('instance', PARAM_INT); $mform->addElement('hidden', 'add', 0); - $mform->setType('add', PARAM_ALPHA); + $mform->setType('add', PARAM_ALPHANUM); $mform->addElement('hidden', 'update', 0); $mform->setType('update', PARAM_INT); diff --git a/course/upgrade.txt b/course/upgrade.txt index f88a58bb4f7..195a6ebf7fc 100644 --- a/course/upgrade.txt +++ b/course/upgrade.txt @@ -4,6 +4,7 @@ information provided here is intended especially for developers. === 3.9 === * The function get_module_metadata is now deprecated. Please use \core_course\local\service\content_item_service instead. +* Activity module names are now PARAM_ALPHANUM instead of PARAM_ALPHA so integers can be used in activity module names === 3.8 === From f3c7e00f135dfa775570560b274df57a72aa5b27 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 12 Feb 2020 12:24:14 +0100 Subject: [PATCH 2/5] MDL-67707 core_h5p: add public H5P player methods --- h5p/classes/helper.php | 20 ++++++++++++++++++ h5p/classes/player.php | 36 +++++++++++++++++++++++++++++++-- h5p/templates/h5pembed.mustache | 6 ++++-- h5p/tests/helper_test.php | 15 ++++++++++---- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/h5p/classes/helper.php b/h5p/classes/helper.php index d4137453aa1..1aaadb3f4b2 100644 --- a/h5p/classes/helper.php +++ b/h5p/classes/helper.php @@ -107,6 +107,26 @@ class helper { return $core->getStorableDisplayOptions($disableoptions, 0); } + /** + * Convert the int representation of display options into stdClass + * + * @param core $core The \core_h5p\core object + * @param int $displayint integer value representing display options + * + * @return int The representation of display options as int + */ + public static function decode_display_options(core $core, int $displayint = null): \stdClass { + $config = new \stdClass(); + if ($displayint === null) { + $displayint = self::get_display_options($core, $config); + } + $displayarray = $core->getDisplayOptionsForEdit($displayint); + $config->export = $displayarray[core::DISPLAY_OPTION_DOWNLOAD] ?? 0; + $config->embed = $displayarray[core::DISPLAY_OPTION_EMBED] ?? 0; + $config->copyright = $displayarray[core::DISPLAY_OPTION_COPYRIGHT] ?? 0; + return $config; + } + /** * Checks if the author of the .h5p file is "trustable". If the file hasn't been uploaded by a user with the * required capability, the content won't be deployed. diff --git a/h5p/classes/player.php b/h5p/classes/player.php index b6c355e6422..9a86b449676 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -123,6 +123,38 @@ class player { } } + /** + * Get the encoded URL for embeding this H5P content. + * + * @param string $url Local URL of the H5P file to display. + * @param stdClass $config Configuration for H5P buttons. + * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * + * @return string The embedable code to display a H5P file. + */ + public static function display(string $url, \stdClass $config, bool $preventredirect = true): string { + global $OUTPUT; + $params = [ + 'url' => $url, + 'preventredirect' => $preventredirect, + ]; + + $optparams = ['frame', 'export', 'embed', 'copyright']; + foreach ($optparams as $optparam) { + if (!empty($config->$optparam)) { + $params[$optparam] = $config->$optparam; + } + } + $fileurl = new \moodle_url('/h5p/embed.php', $params); + + $template = new \stdClass(); + $template->embedurl = $fileurl->out(false); + + $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template); + $result .= self::get_resize_code(); + return $result; + } + /** * Get the error messages stored in our H5P framework. * @@ -167,7 +199,7 @@ class player { 'exportUrl' => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '', 'embedCode' => $this->get_embed_code($this->url->out(), $displayoptions[ core::DISPLAY_OPTION_EMBED ]), - 'resizeCode' => $this->get_resize_code(), + 'resizeCode' => self::get_resize_code(), 'title' => $this->content['slug'], 'displayOptions' => $displayoptions, 'url' => self::get_embed_url($this->url->out())->out(), @@ -715,7 +747,7 @@ class player { * * @return string The HTML code with the resize script. */ - private function get_resize_code(): string { + private static function get_resize_code(): string { global $OUTPUT; $template = new \stdClass(); diff --git a/h5p/templates/h5pembed.mustache b/h5p/templates/h5pembed.mustache index 9f261265bb2..a0808404fcb 100644 --- a/h5p/templates/h5pembed.mustache +++ b/h5p/templates/h5pembed.mustache @@ -28,5 +28,7 @@ } }} - - \ No newline at end of file + diff --git a/h5p/tests/helper_test.php b/h5p/tests/helper_test.php index 6158c532d66..c216a2ce126 100644 --- a/h5p/tests/helper_test.php +++ b/h5p/tests/helper_test.php @@ -41,14 +41,14 @@ class helper_testcase extends \advanced_testcase { /** * Test the behaviour of get_display_options(). * - * @dataProvider get_display_options_provider + * @dataProvider display_options_provider * @param bool $frame Whether the frame should be displayed or not * @param bool $export Whether the export action button should be displayed or not * @param bool $embed Whether the embed action button should be displayed or not * @param bool $copyright Whether the copyright action button should be displayed or not * @param int $expected The expectation with the displayoptions value */ - public function test_get_display_options(bool $frame, bool $export, bool $embed, bool $copyright, int $expected): void { + public function test_display_options(bool $frame, bool $export, bool $embed, bool $copyright, int $expected): void { $this->setRunTestInSeparateProcess(true); $this->resetAfterTest(); @@ -60,9 +60,16 @@ class helper_testcase extends \advanced_testcase { 'embed' => $embed, 'copyright' => $copyright, ]; - $displayoptions = helper::get_display_options($core, $config); + // Test getting display options. + $displayoptions = helper::get_display_options($core, $config); $this->assertEquals($expected, $displayoptions); + + // Test decoding display options. + $decoded = helper::decode_display_options($core, $expected); + $this->assertEquals($decoded->export, $config->export); + $this->assertEquals($decoded->embed, $config->embed); + $this->assertEquals($decoded->copyright, $config->copyright); } /** @@ -70,7 +77,7 @@ class helper_testcase extends \advanced_testcase { * * @return array */ - public function get_display_options_provider(): array { + public function display_options_provider(): array { return [ 'All display options disabled' => [ false, From 40cef8afe7bd28656fd7e7338e21a5c2dcf0b67a Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 12 Feb 2020 12:25:13 +0100 Subject: [PATCH 3/5] MDL-67707 core_h5p: move shared h5p files to core fixtures --- filter/displayh5p/tests/behat/h5p_filter.feature | 8 ++++---- .../tests/fixtures/guess-the-answer.h5p | Bin .../displayh5p => h5p}/tests/fixtures/ipsums.h5p | Bin lib/editor/atto/plugins/h5p/tests/behat/h5p.feature | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) rename {lib/editor/atto => h5p}/tests/fixtures/guess-the-answer.h5p (100%) rename {filter/displayh5p => h5p}/tests/fixtures/ipsums.h5p (100%) diff --git a/filter/displayh5p/tests/behat/h5p_filter.feature b/filter/displayh5p/tests/behat/h5p_filter.feature index 4bad0907e61..df8f2207ed7 100644 --- a/filter/displayh5p/tests/behat/h5p_filter.feature +++ b/filter/displayh5p/tests/behat/h5p_filter.feature @@ -65,7 +65,7 @@ Feature: Render H5P content using filters And I add a "File" to section "1" And I set the following fields to these values: | Name | ipsumFile | - And I upload "filter/displayh5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager And I press "Save and return to course" And I follow "PageName1" And I navigate to "Edit settings" in current page administration @@ -102,7 +102,7 @@ Feature: Render H5P content using filters And I add a "File" to section "1" And I set the following fields to these values: | Name | ipsumFile | - And I upload "filter/displayh5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager And I press "Save and return to course" And I follow "PageName1" And I navigate to "Edit settings" in current page administration @@ -127,7 +127,7 @@ Feature: Render H5P content using filters And I add a "File" to section "1" And I set the following fields to these values: | Name | ipsumFileTeacher | - And I upload "filter/displayh5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager And I press "Save and return to course" And I follow "PageName1" And I navigate to "Edit settings" in current page administration @@ -151,7 +151,7 @@ Feature: Render H5P content using filters And I add a "File" to section "1" And I set the following fields to these values: | Name | ipsumFile | - And I upload "filter/displayh5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Select files" filemanager And I press "Save and return to course" And I follow "PageName2" And I navigate to "Edit settings" in current page administration diff --git a/lib/editor/atto/tests/fixtures/guess-the-answer.h5p b/h5p/tests/fixtures/guess-the-answer.h5p similarity index 100% rename from lib/editor/atto/tests/fixtures/guess-the-answer.h5p rename to h5p/tests/fixtures/guess-the-answer.h5p diff --git a/filter/displayh5p/tests/fixtures/ipsums.h5p b/h5p/tests/fixtures/ipsums.h5p similarity index 100% rename from filter/displayh5p/tests/fixtures/ipsums.h5p rename to h5p/tests/fixtures/ipsums.h5p diff --git a/lib/editor/atto/plugins/h5p/tests/behat/h5p.feature b/lib/editor/atto/plugins/h5p/tests/behat/h5p.feature index 4ea2e13b538..fc51cf4639f 100644 --- a/lib/editor/atto/plugins/h5p/tests/behat/h5p.feature +++ b/lib/editor/atto/plugins/h5p/tests/behat/h5p.feature @@ -40,7 +40,7 @@ Feature: Add h5ps to Atto Scenario: Insert an h5p file Given I log in as "admin" And I follow "Manage private files..." - And I upload "lib/editor/atto/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager And I click on "Save changes" "button" And I am on "Course 1" course homepage And I follow "PageName1" @@ -145,7 +145,7 @@ Feature: Add h5ps to Atto Scenario: Enable/disable H5P options Given I log in as "admin" And I follow "Manage private files..." - And I upload "lib/editor/atto/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager And I click on "Save changes" "button" And I am on "Course 1" course homepage And I follow "PageName1" @@ -209,7 +209,7 @@ Feature: Add h5ps to Atto | student1 | C1 | student | And I log in as "admin" And I follow "Manage private files..." - And I upload "lib/editor/atto/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager And I click on "Save changes" "button" And I am on "Course 1" course homepage And I follow "PageName1" From b7a416817ffc1e9277a242d066e94d711608aa72 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 5 Feb 2020 18:04:57 +0100 Subject: [PATCH 4/5] MDL-67707 mod_h5pactivity: plugin skeleton AMOS BEGIN CPY [h5poptions,atto_h5p],[h5pdisplay,mod_h5pactivity] CPY [downloadbutton,atto_h5p],[displayexport,mod_h5pactivity] CPY [embedbutton,atto_h5p],[displayembed,mod_h5pactivity] CPY [copyrightbutton,atto_h5p],[displaycopyright,mod_h5pactivity] AMOS END --- ...backup_h5pactivity_activity_task.class.php | 70 +++ .../moodle2/backup_h5pactivity_stepslib.php | 58 +++ ...estore_h5pactivity_activity_task.class.php | 93 ++++ .../moodle2/restore_h5pactivity_stepslib.php | 67 +++ .../course_module_instance_list_viewed.php | 37 ++ .../classes/event/course_module_viewed.php | 59 +++ mod/h5pactivity/classes/privacy/provider.php | 46 ++ mod/h5pactivity/db/access.php | 51 +++ mod/h5pactivity/db/install.xml | 25 + mod/h5pactivity/grade.php | 42 ++ mod/h5pactivity/index.php | 89 ++++ mod/h5pactivity/lang/en/h5pactivity.php | 47 ++ mod/h5pactivity/lib.php | 408 +++++++++++++++++ mod/h5pactivity/mod_form.php | 182 ++++++++ mod/h5pactivity/pix/icon.png | Bin 0 -> 797 bytes mod/h5pactivity/pix/icon.svg | 431 ++++++++++++++++++ .../tests/behat/add_h5pactivity.feature | 126 +++++ mod/h5pactivity/tests/events_test.php | 107 +++++ mod/h5pactivity/tests/generator/lib.php | 91 ++++ mod/h5pactivity/tests/generator_test.php | 106 +++++ mod/h5pactivity/version.php | 29 ++ mod/h5pactivity/view.php | 74 +++ 22 files changed, 2238 insertions(+) create mode 100644 mod/h5pactivity/backup/moodle2/backup_h5pactivity_activity_task.class.php create mode 100644 mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php create mode 100644 mod/h5pactivity/backup/moodle2/restore_h5pactivity_activity_task.class.php create mode 100644 mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php create mode 100644 mod/h5pactivity/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/h5pactivity/classes/event/course_module_viewed.php create mode 100644 mod/h5pactivity/classes/privacy/provider.php create mode 100644 mod/h5pactivity/db/access.php create mode 100644 mod/h5pactivity/db/install.xml create mode 100644 mod/h5pactivity/grade.php create mode 100644 mod/h5pactivity/index.php create mode 100644 mod/h5pactivity/lang/en/h5pactivity.php create mode 100644 mod/h5pactivity/lib.php create mode 100644 mod/h5pactivity/mod_form.php create mode 100644 mod/h5pactivity/pix/icon.png create mode 100644 mod/h5pactivity/pix/icon.svg create mode 100644 mod/h5pactivity/tests/behat/add_h5pactivity.feature create mode 100644 mod/h5pactivity/tests/events_test.php create mode 100644 mod/h5pactivity/tests/generator/lib.php create mode 100644 mod/h5pactivity/tests/generator_test.php create mode 100644 mod/h5pactivity/version.php create mode 100644 mod/h5pactivity/view.php diff --git a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_activity_task.class.php b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_activity_task.class.php new file mode 100644 index 00000000000..199c2359439 --- /dev/null +++ b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_activity_task.class.php @@ -0,0 +1,70 @@ +. + +/** + * The task that provides all the steps to perform a complete backup is defined here. + * + * @package mod_h5pactivity + * @category backup + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php'); + +/** + * The class provides all the settings and steps to perform one complete backup of mod_h5pactivity. + */ +class backup_h5pactivity_activity_task extends backup_activity_task { + + /** + * Defines particular settings for the plugin. + */ + protected function define_my_settings() { + return; + } + + /** + * Defines particular steps for the backup process. + */ + protected function define_my_steps() { + $this->add_step(new backup_h5pactivity_activity_structure_step('h5pactivity_structure', 'h5pactivity.xml')); + } + + /** + * Codes the transformations to perform in the activity in order to get transportable (encoded) links. + * + * @param string $content content to encode. + * @return string encoded string + */ + static public function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to the list of choices. + $search = "/(".$base."\/mod\/h5pactivity\/index.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@H5PACTIVITYINDEX*$2@$', $content); + + // Link to choice view by moduleid. + $search = "/(".$base."\/mod\/h5pactivity\/view.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@H5PACTIVITYVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php new file mode 100644 index 00000000000..8f40e330387 --- /dev/null +++ b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php @@ -0,0 +1,58 @@ +. + +/** + * Backup steps for mod_h5pactivity are defined here. + * + * @package mod_h5pactivity + * @category backup + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define the complete structure for backup, with file and id annotations. + */ +class backup_h5pactivity_activity_structure_step extends backup_activity_structure_step { + + /** + * Defines the structure of the resulting xml file. + * + * @return backup_nested_element The structure wrapped by the common 'activity' element. + */ + protected function define_structure() { + $userinfo = $this->get_setting_value('userinfo'); + + // Replace with the attributes and final elements that the element will handle. + $attributes = ['id']; + $finalelements = ['name', 'timecreated', 'timemodified', 'intro', + 'introformat', 'grade', 'displayoptions']; + $root = new backup_nested_element('h5pactivity', $attributes, $finalelements); + + // Define the source tables for the elements. + $root->set_source_table('h5pactivity', ['id' => backup::VAR_ACTIVITYID]); + + // Define id annotations. + + // Define file annotations. + $root->annotate_files('mod_h5pactivity', 'intro', null); // This file area hasn't itemid. + $root->annotate_files('mod_h5pactivity', 'package', null); // This file area hasn't itemid. + + return $this->prepare_activity_structure($root); + } +} diff --git a/mod/h5pactivity/backup/moodle2/restore_h5pactivity_activity_task.class.php b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_activity_task.class.php new file mode 100644 index 00000000000..cca1c2c1f80 --- /dev/null +++ b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_activity_task.class.php @@ -0,0 +1,93 @@ +. + +/** + * The task that provides a complete restore of mod_h5pactivity is defined here. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php'); + +/** + * Restore task for mod_h5pactivity. + */ +class restore_h5pactivity_activity_task extends restore_activity_task { + + /** + * Defines particular settings that this activity can have. + */ + protected function define_my_settings(): void { + return; + } + + /** + * Defines particular steps that this activity can have. + * + * @return base_step. + */ + protected function define_my_steps(): void { + $this->add_step(new restore_h5pactivity_activity_structure_step('h5pactivity_structure', 'h5pactivity.xml')); + } + + /** + * Defines the contents in the activity that must be processed by the link decoder. + * + * @return array. + */ + static public function define_decode_contents(): array { + $contents = []; + + // Define the contents. + $contents[] = new restore_decode_content('h5pactivity', ['intro'], 'h5pactivity'); + + return $contents; + } + + /** + * Defines the decoding rules for links belonging to the activity to be executed by the link decoder. + * + * @return restore_decode_rule[]. + */ + static public function define_decode_rules(): array { + $rules = []; + + $rules[] = new restore_decode_rule('H5PACTIVITYVIEWBYID', '/mod/h5pactivity/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('H5PACTIVITYINDEX', '/mod/h5pactivity/index.php?id=$1', 'course'); + + return $rules; + } + + /** + * Defines the restore log rules that will be applied by the + * {@link restore_logs_processor} when restoring mod_h5pactivity logs. It + * must return one array of {@link restore_log_rule} objects. + * + * @return restore_log_rule[]. + */ + static public function define_restore_log_rules(): array { + $rules = []; + + // Define the rules. + $rules[] = new restore_log_rule('h5pactivity', 'view all', 'index.php?id={course}', null); + + return $rules; + } +} diff --git a/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php new file mode 100644 index 00000000000..4c4ec6a3dad --- /dev/null +++ b/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php @@ -0,0 +1,67 @@ +. + +/** + * All the steps to restore mod_h5pactivity are defined here. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Defines the structure step to restore one mod_h5pactivity activity. + */ +class restore_h5pactivity_activity_structure_step extends restore_activity_structure_step { + + /** + * Defines the structure to be restored. + * + * @return restore_path_element[]. + */ + protected function define_structure(): array { + $paths = []; + $userinfo = $this->get_setting_value('userinfo'); + $paths[] = new restore_path_element('h5pactivity', '/activity/h5pactivity'); + return $this->prepare_activity_structure($paths); + } + + /** + * Processes the elt restore data. + * + * @param array $data Parsed element data. + */ + protected function process_h5pactivity(array $data): void { + global $DB; + $data = (object) $data; + $data->course = $this->get_courseid(); + // Insert the record. + $newitemid = $DB->insert_record('h5pactivity', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Defines post-execution actions. + */ + protected function after_execute(): void { + // Add related files, no need to match by itemname (just internally handled context). + $this->add_related_files('mod_h5pactivity', 'intro', null); + $this->add_related_files('mod_h5pactivity', 'package', null); + } +} diff --git a/mod/h5pactivity/classes/event/course_module_instance_list_viewed.php b/mod/h5pactivity/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 00000000000..f0a720495d9 --- /dev/null +++ b/mod/h5pactivity/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,37 @@ +. + +/** + * Plugin event classes are defined here. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The course_module_instance_list_viewed event class. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { +} diff --git a/mod/h5pactivity/classes/event/course_module_viewed.php b/mod/h5pactivity/classes/event/course_module_viewed.php new file mode 100644 index 00000000000..4dabab3e9d5 --- /dev/null +++ b/mod/h5pactivity/classes/event/course_module_viewed.php @@ -0,0 +1,59 @@ +. + +/** + * Plugin event classes are defined here. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The course_module_viewed event class. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_viewed extends \core\event\course_module_viewed { + + /** + * Init method. + * + * @return void + */ + protected function init(): void { + $this->data['objecttable'] = 'h5pactivity'; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * This is used when restoring course logs where it is required that we + * map the objectid to it's new value in the new course. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'h5pactivity', 'restore' => 'h5pactivity']; + } + +} diff --git a/mod/h5pactivity/classes/privacy/provider.php b/mod/h5pactivity/classes/privacy/provider.php new file mode 100644 index 00000000000..afb38de01ec --- /dev/null +++ b/mod/h5pactivity/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Defines {@link \mod_h5pactivity\privacy\provider} class. + * + * @package mod_h5pactivity + * @category privacy + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the H5P activity plugin. + * + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/h5pactivity/db/access.php b/mod/h5pactivity/db/access.php new file mode 100644 index 00000000000..26b21b33059 --- /dev/null +++ b/mod/h5pactivity/db/access.php @@ -0,0 +1,51 @@ +. + +/** + * Plugin capabilities are defined here. + * + * @package mod_h5pactivity + * @category access + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + 'mod/h5pactivity:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'guest' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + ], + + 'mod/h5pactivity:addinstance' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], +]; diff --git a/mod/h5pactivity/db/install.xml b/mod/h5pactivity/db/install.xml new file mode 100644 index 00000000000..a2461ef18fc --- /dev/null +++ b/mod/h5pactivity/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/mod/h5pactivity/grade.php b/mod/h5pactivity/grade.php new file mode 100644 index 00000000000..ccf6c4af1ec --- /dev/null +++ b/mod/h5pactivity/grade.php @@ -0,0 +1,42 @@ +. + +/** + * Redirect the user to the appropiate submission related page. + * + * @package mod_h5pactivity + * @category grade + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../config.php'); + +// Course module ID. +$id = required_param('id', PARAM_INT); + +// Item number may be != 0 for activities that allow more than one grade per user. +$itemnumber = optional_param('itemnumber', 0, PARAM_INT); + +// Graded user ID (optional). +$userid = optional_param('userid', 0, PARAM_INT); + +require_login(); + +// TODO: in the near future this file will redirect to a specific user H5P attempts page. + +// In the simplest case just redirect to the view page. +redirect('view.php?id='.$id); diff --git a/mod/h5pactivity/index.php b/mod/h5pactivity/index.php new file mode 100644 index 00000000000..f72fb4b7cdf --- /dev/null +++ b/mod/h5pactivity/index.php @@ -0,0 +1,89 @@ +. + +/** + * Display information about all the mod_h5pactivity modules in the requested course. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); + +$id = required_param('id', PARAM_INT); + +$course = $DB->get_record('course', ['id' => $id], '*', MUST_EXIST); +require_course_login($course); + +$coursecontext = context_course::instance($course->id); + +$event = \mod_h5pactivity\event\course_module_instance_list_viewed::create(['context' => $coursecontext]); +$event->add_record_snapshot('course', $course); +$event->trigger(); + +$PAGE->set_url('/mod/h5pactivity/index.php', ['id' => $id]); +$PAGE->set_title(format_string($course->fullname)); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($coursecontext); + +echo $OUTPUT->header(); + +$modulenameplural = get_string('modulenameplural', 'mod_h5pactivity'); +echo $OUTPUT->heading($modulenameplural); + +$h5pactivities = get_all_instances_in_course('h5pactivity', $course); + +if (empty($h5pactivities)) { + notice(get_string('thereareno', 'moodle'), new moodle_url('/course/view.php', ['id' => $course->id])); + exit; +} + +$table = new html_table(); +$table->attributes['class'] = 'generaltable mod_index'; + +$align = ['center', 'left']; +if ($course->format == 'weeks') { + $table->head = [get_string('week'), get_string('name')]; + $table->align = ['center', 'left']; +} else if ($course->format == 'topics') { + $table->head = [get_string('topic'), get_string('name')]; + $table->align = ['center', 'left']; +} else { + $table->head = [get_string('name')]; + $table->align = ['left']; +} + +foreach ($h5pactivities as $h5pactivity) { + $attributes = []; + if (!$h5pactivity->visible) { + $attributes['class'] = 'dimmed'; + } + $link = html_writer::link( + new moodle_url('/mod/h5pactivity/view.php', ['id' => $h5pactivity->coursemodule]), + format_string($h5pactivity->name, true), + $attributes); + + if ($course->format == 'weeks' or $course->format == 'topics') { + $table->data[] = [$h5pactivity->section, $link]; + } else { + $table->data[] = [$link]; + } +} + +echo html_writer::table($table); +echo $OUTPUT->footer(); diff --git a/mod/h5pactivity/lang/en/h5pactivity.php b/mod/h5pactivity/lang/en/h5pactivity.php new file mode 100644 index 00000000000..73e54893287 --- /dev/null +++ b/mod/h5pactivity/lang/en/h5pactivity.php @@ -0,0 +1,47 @@ +. + +/** + * Plugin strings are defined here. + * + * @package mod_h5pactivity + * @category string + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['areapackage'] = 'Package file'; +$string['displayexport'] = 'Allow download'; +$string['displayembed'] = 'Embed button'; +$string['displaycopyright'] = 'Copyright button'; +$string['h5pactivity:addinstance'] = 'Add a new H5P'; +$string['h5pactivity:view'] = 'View H5P'; +$string['h5pactivityfieldset'] = 'H5P Settings'; +$string['h5pactivityname'] = 'H5P'; +$string['h5pactivitysettings'] = 'Settings'; +$string['h5pdisplay'] = 'H5P options'; +$string['modulename'] = 'H5P activity'; +$string['modulename_help'] = 'Use this module to use a H5P compatible content as a course activity.'; +$string['modulename_link'] = 'mod/h5pactivity/view'; +$string['modulenameplural'] = 'H5P activities'; +$string['package'] = 'Package file'; +$string['package_help'] = 'The package file is a h5pfile containing H5P dynamic content.'; +$string['page-mod-h5pactivity-x'] = 'Any H5P module page'; +$string['pluginadministration'] = 'H5P administration'; +$string['pluginname'] = 'H5P activity'; +$string['privacy:metadata'] = 'The H5P activity plugin does not store any personal data.'; diff --git a/mod/h5pactivity/lib.php b/mod/h5pactivity/lib.php new file mode 100644 index 00000000000..1ebf7ba57c9 --- /dev/null +++ b/mod/h5pactivity/lib.php @@ -0,0 +1,408 @@ +. + +/** + * Library of interface functions and constants. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Checks if H5P activity supports a specific feature. + * + * @uses FEATURE_GROUPS + * @uses FEATURE_GROUPINGS + * @uses FEATURE_MOD_INTRO + * @uses FEATURE_SHOW_DESCRIPTION + * @uses FEATURE_COMPLETION_TRACKS_VIEWS + * @uses FEATURE_COMPLETION_HAS_RULES + * @uses FEATURE_MODEDIT_DEFAULT_COMPLETION + * @uses FEATURE_GRADE_HAS_GRADE + * @uses FEATURE_GRADE_OUTCOMES + * @uses FEATURE_BACKUP_MOODLE2 + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed True if module supports feature, false if not, null if doesn't know + */ +function h5pactivity_supports(string $feature): ?bool { + switch($feature) { + case FEATURE_GROUPS: + return true; + case FEATURE_GROUPINGS: + return true; + case FEATURE_MOD_INTRO: + return true; + case FEATURE_SHOW_DESCRIPTION: + return true; + case FEATURE_COMPLETION_TRACKS_VIEWS: + return true; + case FEATURE_MODEDIT_DEFAULT_COMPLETION: + return true; + case FEATURE_GRADE_HAS_GRADE: + return true; + case FEATURE_GRADE_OUTCOMES: + return true; + case FEATURE_BACKUP_MOODLE2: + return true; + default: + return null; + } +} + +/** + * Saves a new instance of the mod_h5pactivity into the database. + * + * Given an object containing all the necessary data, (defined by the form + * in mod_form.php) this function will create a new instance and return the id + * number of the instance. + * + * @param stdClass $data An object from the form. + * @param mod_h5pactivity_mod_form $mform The form. + * @return int The id of the newly inserted record. + */ +function h5pactivity_add_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): int { + global $DB; + + $data->timecreated = time(); + $cmid = $data->coursemodule; + + $data->id = $DB->insert_record('h5pactivity', $data); + + // We need to use context now, so we need to make sure all needed info is already in db. + $DB->set_field('course_modules', 'instance', $data->id, ['id' => $cmid]); + h5pactivity_set_mainfile($data); + + // Extra fields required in grade related functions. + $data->cmid = $data->coursemodule; + h5pactivity_grade_item_update($data); + return $data->id; +} + +/** + * Updates an instance of the mod_h5pactivity in the database. + * + * Given an object containing all the necessary data (defined in mod_form.php), + * this function will update an existing instance with new data. + * + * @param stdClass $data An object from the form in mod_form.php. + * @param mod_h5pactivity_mod_form $mform The form. + * @return bool True if successful, false otherwise. + */ +function h5pactivity_update_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): bool { + global $DB; + + $data->timemodified = time(); + $data->id = $data->instance; + + h5pactivity_set_mainfile($data); + + // Extra fields required in grade related functions. + $data->cmid = $data->coursemodule; + h5pactivity_grade_item_update($data); + h5pactivity_update_grades($data); + + return $DB->update_record('h5pactivity', $data); +} + +/** + * Removes an instance of the mod_h5pactivity from the database. + * + * @param int $id Id of the module instance. + * @return bool True if successful, false on failure. + */ +function h5pactivity_delete_instance(int $id): bool { + global $DB; + + $activity = $DB->get_record('h5pactivity', ['id' => $id]); + if (!$activity) { + return false; + } + + $DB->delete_records('h5pactivity', ['id' => $id]); + + h5pactivity_grade_item_delete($activity); + + return true; +} + +/** + * Checks if scale is being used by any instance of mod_h5pactivity. + * + * This is used to find out if scale used anywhere. + * + * @param int $scaleid ID of the scale. + * @return bool True if the scale is used by any mod_h5pactivity instance. + */ +function h5pactivity_scale_used_anywhere(int $scaleid): bool { + global $DB; + + if ($scaleid and $DB->record_exists('h5pactivity', ['grade' => -$scaleid])) { + return true; + } else { + return false; + } +} + +/** + * Creates or updates grade item for the given mod_h5pactivity instance. + * + * Needed by {@link grade_update_mod_grades()}. + * + * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. + * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return int int 0 if ok, error code otherwise + */ +function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades=null): int { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + $item = []; + $item['itemname'] = clean_param($moduleinstance->name, PARAM_NOTAGS); + $item['gradetype'] = GRADE_TYPE_VALUE; + if (isset($moduleinstance->cmidnumber)) { + $item['idnumber'] = $moduleinstance->cmidnumber; + } + + if ($moduleinstance->grade > 0) { + $item['gradetype'] = GRADE_TYPE_VALUE; + $item['grademax'] = $moduleinstance->grade; + $item['grademin'] = 0; + } else if ($moduleinstance->grade < 0) { + $item['gradetype'] = GRADE_TYPE_SCALE; + $item['scaleid'] = -$moduleinstance->grade; + } else { + $item['gradetype'] = GRADE_TYPE_NONE; + } + if ($grades === 'reset') { + $params['reset'] = true; + $grades = null; + } + return grade_update('mod/h5pactivity', $moduleinstance->course, 'mod', + 'h5pactivity', $moduleinstance->id, 0, null, $item); +} + +/** + * Delete grade item for given mod_h5pactivity instance. + * + * @param stdClass $moduleinstance Instance object. + * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED + */ +function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + return grade_update('mod/h5pactivity', $moduleinstance->course, 'mod', 'h5pactivity', + $moduleinstance->id, 0, null, ['deleted' => 1]); +} + +/** + * Update mod_h5pactivity grades in the gradebook. + * + * Needed by {@link grade_update_mod_grades()}. + * + * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. + * @param int $userid Update grade of specific user only, 0 means all participants. + */ +function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): void { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + // Populate array of grade objects indexed by userid. + $grades = []; + grade_update('mod/h5pactivity', $moduleinstance->course, 'mod', + 'h5pactivity', $moduleinstance->id, 0, $grades); +} + +/** + * This function is used by the reset_course_userdata function in moodlelib. + * This function will remove all assignment submissions and feedbacks in the database + * and clean up any related data. + * + * @param stdClass $data the data submitted from the reset course. + * @return array + */ +function h5pactivity_reset_userdata($data) { + global $CFG, $DB; + // TODO: When attempts are created this function will remove them. + return []; +} + +/** + * Removes all grades from gradebook + * + * @param int $courseid Coude ID + * @param string $type optional type (default '') + */ +function h5pactivity_reset_gradebook(int $courseid, string $type=''): void { + global $DB; + + $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid + FROM {h5pactivity} a, {course_modules} cm, {modules} m + WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=s.id AND s.course=?"; + + if ($activities = $DB->get_records_sql($sql, [$courseid])) { + foreach ($activities as $activity) { + h5pactivity_grade_item_update($activity, true); + } + } +} + +/** + * Return a list of page types + * + * @param string $pagetype current page type + * @param stdClass $parentcontext Block's parent context + * @param stdClass $currentcontext Current context of block + * @return array array of page types and it's names + */ +function h5pactivity_page_type_list(string $pagetype, stdClass $parentcontext, stdClass $currentcontext): array { + $modulepagetype = [ + 'mod-h5pactivity-*' => get_string('page-mod-h5pactivity-x', 'h5pactivity'), + ]; + return $modulepagetype; +} + +/** + * Check if the module has any update that affects the current user since a given time. + * + * @param cm_info $cm course module data + * @param int $from the time to check updates from + * @param array $filter if we need to check only specific updates + * @return stdClass an object with the different type of areas indicating if they were updated or not + */ +function h5pactivity_check_updates_since(cm_info $cm, int $from, array $filter = []): stdClass { + $updates = course_check_module_updates_since($cm, $from, ['package'], $filter); + return $updates; +} + +/** + * Returns the lists of all browsable file areas within the given module context. + * + * The file area 'intro' for the activity introduction field is added automatically + * by {@link file_browser::get_file_info_context_module()}. + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @return string[] array of pair file area => human file area name + */ +function h5pactivity_get_file_areas(stdClass $course, stdClass $cm, stdClass $context): array { + $areas = []; + $areas['package'] = get_string('areapackage', 'mod_h5pactivity'); + return $areas; +} + +/** + * File browsing support for data module. + * + * @param file_browser $browser + * @param array $areas + * @param stdClass $course + * @param stdClass $cm + * @param context $context + * @param string $filearea + * @param int $itemid + * @param string $filepath + * @param string $filename + * @return file_info_stored|null file_info_stored instance or null if not found + */ +function h5pactivity_get_file_info(file_browser $browser, array $areas, stdClass $course, + stdClass $cm, context $context, string $filearea, int $itemid, + string $filepath, string $filename): ?file_info_stored { + global $CFG; + + if (!has_capability('moodle/course:managefiles', $context)) { + return null; + } + + $fs = get_file_storage(); + + if ($filearea === 'package') { + $filepath = is_null($filepath) ? '/' : $filepath; + $filename = is_null($filename) ? '.' : $filename; + + $urlbase = $CFG->wwwroot.'/pluginfile.php'; + if (!$storedfile = $fs->get_file($context->id, 'mod_h5pactivity', 'package', 0, $filepath, $filename)) { + if ($filepath === '/' and $filename === '.') { + $storedfile = new virtual_root_file($context->id, 'mod_h5pactivity', 'package', 0); + } else { + // Not found. + return null; + } + } + return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false); + } + return null; +} + +/** + * Serves the files from the mod_h5pactivity file areas. + * + * @param mixed $course course or id of the course + * @param mixed $cm course module or id of the course module + * @param context $context + * @param string $filearea + * @param array $args + * @param bool $forcedownload + * @param array $options additional options affecting the file serving + * @return bool false if file not found, does not return if found - just send the file + */ +function h5pactivity_pluginfile($course, $cm, context $context, + string $filearea, array $args, bool $forcedownload, array $options = []): bool { + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + + require_login($course, true, $cm); + + $fullpath = ''; + + if ($filearea === 'package') { + $revision = (int)array_shift($args); // Prevents caching problems - ignored here. + $relativepath = implode('/', $args); + $fullpath = "/$context->id/mod_h5pactivity/package/0/$relativepath"; + } + if (empty($fullpath)) { + return false; + } + $fs = get_file_storage(); + $file = $fs->get_file_by_hash(sha1($fullpath)); + if (empty($file)) { + return false; + } + send_stored_file($file, $lifetime, 0, false, $options); +} + +/** + * Saves draft files as the activity package. + * + * @param stdClass $data an object from the form + */ +function h5pactivity_set_mainfile(stdClass $data): void { + $fs = get_file_storage(); + $cmid = $data->coursemodule; + $context = context_module::instance($cmid); + + if (!empty($data->packagefile)) { + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'mod_h5pactivity', 'package'); + file_save_draft_area_files($data->packagefile, $context->id, 'mod_h5pactivity', 'package', + 0, ['subdirs' => 0, 'maxfiles' => 1]); + } +} diff --git a/mod/h5pactivity/mod_form.php b/mod/h5pactivity/mod_form.php new file mode 100644 index 00000000000..39651747bfd --- /dev/null +++ b/mod/h5pactivity/mod_form.php @@ -0,0 +1,182 @@ +. + +/** + * The main mod_h5pactivity configuration form. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/course/moodleform_mod.php'); + +/** + * Module instance settings form. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity_mod_form extends moodleform_mod { + + /** + * Defines forms elements + */ + public function definition(): void { + global $CFG; + + $mform = $this->_form; + + // Adding the "general" fieldset, where all the common settings are shown. + $mform->addElement('header', 'general', get_string('general', 'form')); + + // Adding the standard "name" field. + $mform->addElement('text', 'name', get_string('name'), ['size' => '64']); + + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + + $this->standard_intro_elements(); + + // Adding the rest of mod_h5pactivity settings, spreading all them into this fieldset. + $options = []; + $options['accepted_types'] = ['.h5p']; + $options['maxbytes'] = 0; + $options['maxfiles'] = 1; + $options['subdirs'] = 0; + + $mform->addElement('filemanager', 'packagefile', get_string('package', 'mod_h5pactivity'), null, $options); + $mform->addHelpButton('packagefile', 'package', 'mod_h5pactivity'); + + // H5P displaying options. + $factory = new \core_h5p\factory(); + $core = $factory->get_core(); + $displayoptions = (array) \core_h5p\helper::decode_display_options($core); + $mform->addElement('header', 'h5pdisplay', get_string('h5pdisplay', 'mod_h5pactivity')); + foreach ($displayoptions as $key => $value) { + $name = get_string('display'.$key, 'mod_h5pactivity'); + $fieldname = "displayopt[$key]"; + $mform->addElement('checkbox', $fieldname, $name); + $mform->setType($fieldname, PARAM_BOOL); + } + + // Add standard grading elements. + $this->standard_grading_coursemodule_elements(); + + // Add standard elements. + $this->standard_coursemodule_elements(); + + // Add standard buttons. + $this->add_action_buttons(); + } + + /** + * Enforce validation rules here + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array + **/ + public function validation($data, $files) { + global $USER; + $errors = parent::validation($data, $files); + + if (empty($data['packagefile'])) { + $errors['packagefile'] = get_string('required'); + + } else { + $draftitemid = file_get_submitted_draft_itemid('packagefile'); + + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_h5pactivity', 'packagefilecheck', null, + ['subdirs' => 0, 'maxfiles' => 1]); + + // Get file from users draft area. + $usercontext = context_user::instance($USER->id); + $fs = get_file_storage(); + $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', false); + + if (count($files) < 1) { + $errors['packagefile'] = get_string('required'); + return $errors; + } + $file = reset($files); + if (!$file->is_external_file() && !empty($data['updatefreq'])) { + // Make sure updatefreq is not set if using normal local file. + $errors['updatefreq'] = get_string('updatefreq_error', 'mod_h5pactivity'); + } + } + + return $errors; + } + + /** + * Enforce defaults here. + * + * @param array $defaultvalues Form defaults + * @return void + **/ + public function data_preprocessing(&$defaultvalues) { + // H5P file. + $draftitemid = file_get_submitted_draft_itemid('packagefile'); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_h5pactivity', + 'package', 0, ['subdirs' => 0, 'maxfiles' => 1]); + $defaultvalues['packagefile'] = $draftitemid; + + // H5P display options. + $factory = new \core_h5p\factory(); + $core = $factory->get_core(); + if (isset($defaultvalues['displayoptions'])) { + $currentdisplay = $defaultvalues['displayoptions']; + $displayoptions = (array) \core_h5p\helper::decode_display_options($core, $currentdisplay); + } else { + $displayoptions = (array) \core_h5p\helper::decode_display_options($core); + } + foreach ($displayoptions as $key => $value) { + $fieldname = "displayopt[$key]"; + $defaultvalues[$fieldname] = $value; + } + } + + /** + * Allows modules to modify the data returned by form get_data(). + * This method is also called in the bulk activity completion form. + * + * Only available on moodleform_mod. + * + * @param stdClass $data passed by reference + */ + public function data_postprocessing($data) { + parent::data_postprocessing($data); + + $factory = new \core_h5p\factory(); + $core = $factory->get_core(); + if (isset($data->displayopt)) { + $config = (object) $data->displayopt; + } else { + $config = \core_h5p\helper::decode_display_options($core); + } + $data->displayoptions = \core_h5p\helper::get_display_options($core, $config); + } +} diff --git a/mod/h5pactivity/pix/icon.png b/mod/h5pactivity/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e77dfc0b72e054ef5a6b60bb38b8161a7429be59 GIT binary patch literal 797 zcmV+&1LFLNP)9~%@&I=b+NbW%&oTacsjwNiubViIqDjDp#Y~&D5zpoL)VD+K_L(@=wBQ` zP^_wAEsUM+ZlhKUR27>f!uiJ$ID0iMVI~rU$8&mE#`Fbl+?#?vv5`jwEpwWQ7e#Ogfp#~7yIf5#cx*(#A z4i=agws~G(9Pzs9`S{&)c_OCN8*y&fGG7;TM}o;1y^nHPK^2jJNNP*vhessfec;t{ zoA+NG<$gnKBUaXhg{ot-ndTM@cps&w%q$A3c-g(CljAXy<8i+JQWt_K+ezSHuVKF8 zcw2K^zkHCXiI}O0I1|gARu^h(sb}X$d$@L~#8wh0+K8f!xP5Pf zjb>nR#nEz>FKfbP5?H7@zHbG}gBHjeGzc0aE+W&gP;K*ME&<@o&;Zj52_IKH!{q{l zr2-obK^01I#C)aAv-u=fYmC7Q=w6Fd6}NZDn%b7(*r5WC=b9{ib9rp^sAX)VNOiTb zFIRW$&YrlmK~O~C(aQ!0iiXOnCxo=<;cSE0Mw~ig^9FHGM7hs!Z)Th2J-O+vNndC9 zMS&N1wbbHQ%zj8ZVo6JRvDg_;X%&&E(;}~ixP17xMSlwCcRytdAkI;H^R_GG8M^oT b`q%gg%TswD-x#>g00000NkvXXu0mjf*Y{@z literal 0 HcmV?d00001 diff --git a/mod/h5pactivity/pix/icon.svg b/mod/h5pactivity/pix/icon.svg new file mode 100644 index 00000000000..e363814ebd5 --- /dev/null +++ b/mod/h5pactivity/pix/icon.svg @@ -0,0 +1,431 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/h5pactivity/tests/behat/add_h5pactivity.feature b/mod/h5pactivity/tests/behat/add_h5pactivity.feature new file mode 100644 index 00000000000..b7d5c00a33a --- /dev/null +++ b/mod/h5pactivity/tests/behat/add_h5pactivity.feature @@ -0,0 +1,126 @@ +@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe +Feature: Add H5P activity + In order to let students access a H5P package + As a teacher + I need to add H5P activity to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/h5p:updatelibraries | Allow | editingteacher | System | | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + + + @javascript + Scenario: Add a h5pactivity activity to a course + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Lorum ipsum" + And I should not see "Reuse" + And I should not see "Rights of use" + And I should not see "Embed" + And I switch to the main frame + + @javascript + Scenario: Add a h5pactivity activity with download + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + | Allow download | 1 | + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Reuse" + And I should not see "Rights of use" + And I should not see "Embed" + And I switch to the main frame + + @javascript + Scenario: Add a h5pactivity activity with embed + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + | Embed button | 1 | + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Reuse" + And I should not see "Rights of use" + And I should see "Embed" + And I switch to the main frame + + @javascript + Scenario: Add a h5pactivity activity with copyright + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + | Copyright button | 1 | + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Reuse" + And I should see "Rights of use" + And I should not see "Embed" + And I switch to the main frame + + @javascript + Scenario: Add a h5pactivity activity with copyright in a content without copyright + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + | Copyright button | 1 | + And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Reuse" + And I should not see "Rights of use" + And I should not see "Embed" + And I switch to the main frame + + @javascript + Scenario: Add a h5pactivity activity to a course with all display options enabled + When I add a "H5P activity" to section "1" + And I set the following fields to these values: + | Name | Awesome H5P package | + | Description | Description | + | Allow download | 1 | + | Embed button | 1 | + | Copyright button | 1 | + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager + And I click on "Save and display" "button" + And I wait until the page is ready + Then I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Reuse" + And I should see "Rights of use" + And I should see "Embed" + And I switch to the main frame diff --git a/mod/h5pactivity/tests/events_test.php b/mod/h5pactivity/tests/events_test.php new file mode 100644 index 00000000000..43ba10a8f59 --- /dev/null +++ b/mod/h5pactivity/tests/events_test.php @@ -0,0 +1,107 @@ +. + +/** + * Events test. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * H5P activity events test cases. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity_events_testcase extends advanced_testcase { + + /** + * Setup is called before calling test case. + */ + public function setUp() { + // Must be a non-guest user to create h5pactivities. + $this->setAdminUser(); + } + + /** + * Test course_module_instance_list_viewed event. + */ + public function test_course_module_instance_list_viewed() { + // There is no proper API to call to trigger this event, so what we are + // doing here is simply making sure that the events returns the right information. + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $params = [ + 'context' => context_course::instance($course->id) + ]; + $event = \mod_h5pactivity\event\course_module_instance_list_viewed::create($params); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_h5pactivity\event\course_module_instance_list_viewed', $event); + $this->assertEquals(context_course::instance($course->id), $event->get_context()); + $expected = [$course->id, 'h5pactivity', 'view all', 'index.php?id='.$course->id, '']; + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } + + /** + * Test course_module_viewed event. + */ + public function test_course_module_viewed() { + // There is no proper API to call to trigger this event, so what we are + // doing here is simply making sure that the events returns the right information. + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); + + $params = [ + 'context' => context_module::instance($activity->cmid), + 'objectid' => $activity->id + ]; + $event = \mod_h5pactivity\event\course_module_viewed::create($params); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_h5pactivity\event\course_module_viewed', $event); + $this->assertEquals(context_module::instance($activity->cmid), $event->get_context()); + $this->assertEquals($activity->id, $event->objectid); + $expected = [$course->id, 'h5pactivity', 'view', 'view.php?id=' . $activity->cmid, $activity->id, $activity->cmid]; + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } +} diff --git a/mod/h5pactivity/tests/generator/lib.php b/mod/h5pactivity/tests/generator/lib.php new file mode 100644 index 00000000000..0bf6f9246b3 --- /dev/null +++ b/mod/h5pactivity/tests/generator/lib.php @@ -0,0 +1,91 @@ +. + +/** + * Data generator. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * h5pactivity module data generator class. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity_generator extends testing_module_generator { + + /** + * Creates new h5pactivity module instance. By default it contains a short + * text file. + * + * @param array|stdClass $record data for module being generated. Requires 'course' key + * (an id or the full object). Also can have any fields from add module form. + * @param null|array $options general options for course module. Since 2.6 it is + * possible to omit this argument by merging options into $record + * @return stdClass record from module-defined table with additional field + * cmid (corresponding id in course_modules table) + */ + public function create_instance($record = null, array $options = null) { + global $CFG, $USER; + // Ensure the record can be modified without affecting calling code. + $record = (object)(array)$record; + + // Fill in optional values if not specified. + if (!isset($record->packagefilepath)) { + $record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip'; + } + if (!isset($record->grade)) { + $record->grade = 100; + } + if (!isset($record->displayoptions)) { + $factory = new \core_h5p\factory(); + $core = $factory->get_core(); + $config = \core_h5p\helper::decode_display_options($core); + $record->displayoptions = \core_h5p\helper::get_display_options($core, $config); + } + + // The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath. + if (empty($record->packagefile)) { + if (!isloggedin() || isguestuser()) { + throw new coding_exception('Scorm generator requires a current user'); + } + if (!file_exists($record->packagefilepath)) { + throw new coding_exception("File {$record->packagefilepath} does not exist"); + } + $usercontext = context_user::instance($USER->id); + + // Pick a random context id for specified user. + $record->packagefile = file_get_unused_draft_itemid(); + + // Add actual file there. + $filerecord = ['component' => 'user', 'filearea' => 'draft', + 'contextid' => $usercontext->id, 'itemid' => $record->packagefile, + 'filename' => basename($record->packagefilepath), 'filepath' => '/']; + $fs = get_file_storage(); + $fs->create_file_from_pathname($filerecord, $record->packagefilepath); + } + + // Do work to actually add the instance. + return parent::create_instance($record, (array)$options); + } +} diff --git a/mod/h5pactivity/tests/generator_test.php b/mod/h5pactivity/tests/generator_test.php new file mode 100644 index 00000000000..1f2cad8673f --- /dev/null +++ b/mod/h5pactivity/tests/generator_test.php @@ -0,0 +1,106 @@ +. + +/** + * mod_h5pactivity generator tests + * + * @package mod_h5pactivity + * @category test + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Genarator tests class for mod_h5pactivity. + * + * @package mod_h5pactivity + * @category test + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_h5pactivity_generator_testcase extends advanced_testcase { + + /** + * Test on H5P activity creation. + */ + public function test_create_instance() { + global $DB, $CFG, $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + // Create one activity. + $this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id])); + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); + $records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id'); + $this->assertEquals(15, $activity->displayoptions); + $this->assertEquals(1, count($records)); + $this->assertTrue(array_key_exists($activity->id, $records)); + + // Create a second one with different name and dusplay options. + $params = ['course' => $course->id, 'name' => 'Another h5pactivity', 'displayoptions' => 6]; + $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); + $records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id'); + $this->assertEquals(6, $activity->displayoptions); + $this->assertEquals(2, count($records)); + $this->assertEquals('Another h5pactivity', $records[$activity->id]->name); + + // Examples of specifying the package file (do not validate anything, just check for exceptions). + // 1. As path to the file in filesystem. + $params = [ + 'course' => $course->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p' + ]; + $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); + + // 2. As file draft area id. + $fs = get_file_storage(); + $params = [ + 'course' => $course->id, + 'packagefile' => file_get_unused_draft_itemid() + ]; + $usercontext = context_user::instance($USER->id); + $filerecord = ['component' => 'user', 'filearea' => 'draft', + 'contextid' => $usercontext->id, 'itemid' => $params['packagefile'], + 'filename' => 'singlescobasic.zip', 'filepath' => '/']; + $filepath = $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p'; + $fs->create_file_from_pathname($filerecord, $filepath); + $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); + } + + /** + * Test that a new H5P activity cannot be generated without a valid file + * other user. + */ + public function test_create_file_exception() { + global $CFG; + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + // Testing generator exceptions. + $params = [ + 'course' => $course->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/wrong_file_.xxx' + ]; + $this->expectException(coding_exception::class); + $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); + } +} diff --git a/mod/h5pactivity/version.php b/mod/h5pactivity/version.php new file mode 100644 index 00000000000..3e72cfba6d8 --- /dev/null +++ b/mod/h5pactivity/version.php @@ -0,0 +1,29 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'mod_h5pactivity'; +$plugin->version = 2020022501; +$plugin->requires = 2020013000; diff --git a/mod/h5pactivity/view.php b/mod/h5pactivity/view.php new file mode 100644 index 00000000000..03365527e51 --- /dev/null +++ b/mod/h5pactivity/view.php @@ -0,0 +1,74 @@ +. + +/** + * Prints an instance of mod_h5pactivity. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +$id = required_param('id', PARAM_INT); + +list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity'); + +require_login($course, true, $cm); + +$moduleinstance = $DB->get_record('h5pactivity', ['id' => $cm->instance], '*', MUST_EXIST); + +$context = context_module::instance($cm->id); + +$event = \mod_h5pactivity\event\course_module_viewed::create([ + 'objectid' => $moduleinstance->id, + 'context' => $context +]); +$event->add_record_snapshot('course', $course); +$event->add_record_snapshot('h5pactivity', $moduleinstance); +$event->trigger(); + +// Completion. +$completion = new completion_info($course); +$completion->set_module_viewed($cm); + +// Convert display options to a valid object. +$factory = new \core_h5p\factory(); +$core = $factory->get_core(); +$config = \core_h5p\helper::decode_display_options($core, $moduleinstance->displayoptions); + +// Instantiate player. +$fs = get_file_storage(); +$files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'id', false); +$file = reset($files); +$fileurl = moodle_url::make_pluginfile_url($file->get_contextid(), $file->get_component(), + $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), + $file->get_filename(), false); + +$PAGE->set_url('/mod/h5pactivity/view.php', ['id' => $cm->id]); +$PAGE->set_title(format_string($moduleinstance->name)); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($context); + +echo $OUTPUT->header(); + +// TODO: add component to enable xAPI traking. +echo \core_h5p\player::display($fileurl, $config, true); + +echo $OUTPUT->footer(); From 252e112376c14569289b4dbc6263d2668aa61041 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 13 Feb 2020 10:28:02 +0100 Subject: [PATCH 5/5] MDL-67707 mod_h5pactivity: add plugin to standard list --- lib/classes/plugin_manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 86b2751f379..72be1712cc9 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1914,7 +1914,7 @@ class core_plugin_manager { 'mod' => array( 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', - 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page', + 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop' ),