From c49d8663adc2ddceb19bf05450cbae1257ad98b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20D=C3=A9niz=20Falc=C3=B3n?= Date: Fri, 1 May 2020 18:06:48 +0100 Subject: [PATCH] MDL-67810 core_contentbank: contenttype_h5p editor integrated --- contentbank/classes/form/edit_content.php | 90 +++++++++++ .../contenttype/h5p/classes/form/editor.php | 152 ++++++++++++++++++ contentbank/edit.php | 110 +++++++++++++ contentbank/tests/behat/edit_content.feature | 99 ++++++++++++ lang/en/contentbank.php | 1 + 5 files changed, 452 insertions(+) create mode 100644 contentbank/classes/form/edit_content.php create mode 100644 contentbank/contenttype/h5p/classes/form/editor.php create mode 100644 contentbank/edit.php create mode 100644 contentbank/tests/behat/edit_content.feature diff --git a/contentbank/classes/form/edit_content.php b/contentbank/classes/form/edit_content.php new file mode 100644 index 00000000000..e75b7316fa2 --- /dev/null +++ b/contentbank/classes/form/edit_content.php @@ -0,0 +1,90 @@ +. + +/** + * Provides {@see \core_contentbank\form\edit_content} class. + * + * @package core_contentbank + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_contentbank\form; + +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Defines the form for editing a content. + * + * @package core_contentbank + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class edit_content extends moodleform { + + /** @var int Context the content belongs to. */ + protected $contextid; + + /** @var string Content type plugin name. */ + protected $plugin; + + /** @var int Content id in the content bank. */ + protected $id; + + /** + * Constructor. + * + * @param string $action The action attribute for the form. + * @param array $customdata Data to set during instance creation. + * @param string $method Form method. + */ + public function __construct(string $action = null, array $customdata = null, string $method = 'post') { + parent::__construct($action, $customdata, $method); + $this->contextid = $customdata['contextid']; + $this->plugin = $customdata['plugin']; + $this->id = $customdata['id']; + + $mform =& $this->_form; + $mform->addElement('hidden', 'contextid', $this->contextid); + $this->_form->setType('contextid', PARAM_INT); + + $mform->addElement('hidden', 'plugin', $this->plugin); + $this->_form->setType('plugin', PARAM_PLUGIN); + + $mform->addElement('hidden', 'id', $this->id); + $this->_form->setType('id', PARAM_INT); + } + + /** + * Overrides formslib's add_action_buttons() method. + * + * + * @param bool $cancel + * @param string|null $submitlabel + * + * @return void + */ + public function add_action_buttons($cancel = true, $submitlabel = null): void { + if (is_null($submitlabel)) { + $submitlabel = get_string('save'); + } + parent::add_action_buttons($cancel, $submitlabel); + } +} diff --git a/contentbank/contenttype/h5p/classes/form/editor.php b/contentbank/contenttype/h5p/classes/form/editor.php new file mode 100644 index 00000000000..b7229b9e50e --- /dev/null +++ b/contentbank/contenttype/h5p/classes/form/editor.php @@ -0,0 +1,152 @@ +. + +/** + * Provides the class that defines the form for the H5P authoring tool. + * + * @package contenttype_h5p + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace contenttype_h5p\form; + +use contenttype_h5p\content; +use contenttype_h5p\contenttype; +use core_contentbank\form\edit_content; +use core_h5p\api; +use core_h5p\editor as h5peditor; +use core_h5p\factory; +use stdClass; + +/** + * Defines the form for editing an H5P content. + * + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editor extends edit_content { + + /** @var $h5peditor H5P editor object */ + private $h5peditor; + + /** @var $content The content being edited */ + private $content; + + /** + * Defines the form fields. + */ + protected function definition() { + global $DB; + + $mform = $this->_form; + + // Id of the content to edit. + $id = $this->_customdata['id']; + // H5P content type to create. + $library = optional_param('library', null, PARAM_TEXT); + + if (empty($id) && empty($library)) { + $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $this->_customdata['contextid']]); + print_error('invalidcontentid', 'error', $returnurl); + } + + $this->h5peditor = new h5peditor(); + + if ($id) { + // The H5P editor needs the H5P content id (h5p table). + $record = $DB->get_record('contentbank_content', ['id' => $id]); + $this->content = new content($record); + $file = $this->content->get_file(); + + $h5p = api::get_content_from_pathnamehash($file->get_pathnamehash()); + $mform->addElement('hidden', 'h5pid', $h5p->id); + $mform->setType('h5pid', PARAM_INT); + $this->h5peditor->set_content($h5p->id); + } else { + // The H5P editor needs the H5P content type library name for a new content. + $mform->addElement('hidden', 'library', $library); + $mform->setType('library', PARAM_TEXT); + $this->h5peditor->set_library($library, $this->_customdata['contextid'], 'contentbank', 'public'); + } + + $mformid = 'coolh5peditor'; + $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes()); + + $this->add_action_buttons(); + + $this->h5peditor->add_editor_to_form($mform); + + $this->add_action_buttons(); + } + + /** + * Modify or create an H5P content from the form data. + * + * @param stdClass $data Form data to create or modify an H5P content. + * + * @return int The id of the edited or created content. + */ + public function save_content(stdClass $data): int { + global $DB; + + // The H5P libraries expect data->id as the H5P content id. + // The method \H5PCore::saveContent throws an error if id is set but empty. + if (empty($data->id)) { + unset($data->id); + } else { + // The H5P libraries save in $data->id the H5P content id (h5p table), so the content id is saved in another var. + $contentid = $data->id; + } + + $h5pcontentid = $this->h5peditor->save_content($data); + + $factory = new factory(); + $h5pfs = $factory->get_framework(); + + // Needs the H5P file id to create or update the content bank record. + $h5pcontent = $h5pfs->loadContent($h5pcontentid); + $fs = get_file_storage(); + $file = $fs->get_file_by_hash($h5pcontent['pathnamehash']); + // Creating new content. + if (!isset($data->h5pid)) { + // The initial name of the content is the title of the H5P content. + $cbrecord = new stdClass(); + $cbrecord->name = json_decode($data->h5pparams)->metadata->title; + $context = \context::instance_by_id($data->contextid, MUST_EXIST); + // Create entry in content bank. + $contenttype = new contenttype($context); + $newcontent = $contenttype->create_content($cbrecord); + if ($file && $newcontent) { + $updatedfilerecord = new stdClass(); + $updatedfilerecord->id = $file->get_id(); + $updatedfilerecord->itemid = $newcontent->get_id(); + // As itemid changed, the pathnamehash has to be updated in the file table. + $pathnamehash = \file_storage::get_pathname_hash($file->get_contextid(), $file->get_component(), + $file->get_filearea(), $updatedfilerecord->itemid, $file->get_filepath(), $file->get_filename()); + $updatedfilerecord->pathnamehash = $pathnamehash; + $DB->update_record('files', $updatedfilerecord); + // The pathnamehash in the h5p table must match the file pathnamehash. + $h5pfs->updateContentFields($h5pcontentid, ['pathnamehash' => $pathnamehash]); + } + } else { + // Update content. + $this->content->update_content(); + } + + return $contentid ?? $newcontent->get_id(); + } +} diff --git a/contentbank/edit.php b/contentbank/edit.php new file mode 100644 index 00000000000..832f1c2148d --- /dev/null +++ b/contentbank/edit.php @@ -0,0 +1,110 @@ +. + +/** + * Create or update contents through the specific content type editor + * + * @package core_contentbank + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../config.php'); + +require_login(); + +$contextid = required_param('contextid', PARAM_INT); +$pluginname = required_param('plugin', PARAM_PLUGIN); +$id = optional_param('id', null, PARAM_INT); +$context = context::instance_by_id($contextid, MUST_EXIST); +require_capability('moodle/contentbank:access', $context); + +$returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]); + +if (!empty($id)) { + $record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST); + $contentclass = "$record->contenttype\\content"; + $content = new $contentclass($record); + // Set the heading title. + $heading = $content->get_name(); + // The content type of the content overwrites the pluginname param value. + $contenttypename = $content->get_content_type(); +} else { + $contenttypename = "contenttype_$pluginname"; + $heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename)); +} + +// Check plugin is enabled. +$plugin = core_plugin_manager::instance()->get_plugin_info($contenttypename); +if (!$plugin || !$plugin->is_enabled()) { + print_error('unsupported', 'core_contentbank', $returnurl); +} + +// Create content type instance. +$contenttypeclass = "$contenttypename\\contenttype"; +if (class_exists($contenttypeclass)) { + $contenttype = new $contenttypeclass($context); +} else { + print_error('unsupported', 'core_contentbank', $returnurl); +} + +// Checks the user can edit this content type. +if (!$contenttype->can_edit()) { + print_error('contenttypenoedit', 'core_contentbank', $returnurl, $contenttype->get_plugin_name()); +} + +$values = [ + 'contextid' => $contextid, + 'plugin' => $pluginname, + 'id' => $id +]; + +$title = get_string('contentbank'); +\core_contentbank\helper::get_page_ready($context, $title, true); +if ($PAGE->course) { + require_login($PAGE->course->id); +} + +$PAGE->set_url(new \moodle_url('/contentbank/edit.php', $values)); +$PAGE->set_context($context); +$PAGE->navbar->add(get_string('edit')); +$PAGE->set_title($title); + +$PAGE->set_heading($heading); + +// Instantiate the content type form. +$editorclass = "$contenttypename\\form\\editor"; +if (!class_exists($editorclass)) { + print_error('noformdesc'); +} + +$editorform = new $editorclass(null, $values); + +if ($editorform->is_cancelled()) { + if (empty($id)) { + $returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]); + } + redirect($returnurl); +} else if ($data = $editorform->get_data()) { + $id = $editorform->save_content($data); + // Just in case we've created a new content. + $returnurl->param('id', $id); + redirect($returnurl); +} + +echo $OUTPUT->header(); +$editorform->display(); +echo $OUTPUT->footer(); diff --git a/contentbank/tests/behat/edit_content.feature b/contentbank/tests/behat/edit_content.feature new file mode 100644 index 00000000000..713768c1f5a --- /dev/null +++ b/contentbank/tests/behat/edit_content.feature @@ -0,0 +1,99 @@ +@core @core_contentbank @contentbank_h5p @_file_upload @javascript +Feature: Content bank use editor feature + In order to add/edit content + As a user + I need to be able to access the edition options + + Background: + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I configure the "Navigation" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + + Scenario: Users see the Add button disabled if there is no content type available for creation + Given I click on "Site pages" "list_item" in the "Navigation" "block" + When I click on "Content bank" "link" + Then the "[data-action=Add-content]" "css_element" should be disabled + + Scenario: Users can see the Add button if there is content type available for creation + Given I follow "Dashboard" in the user menu + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" in the "Navigation" "block" + And I click on "Upload" "link" + And I click on "Choose a file..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "filltheblanks.h5p" "link" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + When I click on "Content bank" "link" + And I click on "filltheblanks.h5p" "link" + And I click on "Close" "link" + Then I click on "[data-action=Add-content]" "css_element" + And I should see "Fill in the Blanks" + + Scenario: Users can edit content if they have the required permission + Given I follow "Dashboard" in the user menu + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" in the "Navigation" "block" + And I click on "Upload" "link" + And I click on "Choose a file..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "filltheblanks.h5p" "link" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + When I click on "Content bank" "link" + And I click on "filltheblanks.h5p" "link" + Then I click on "Edit" "link" + And I switch to "h5p-editor-iframe" class iframe + And I switch to the main frame + And I click on "Cancel" "button" + And I should see "filltheblanks.h5p" in the "h1" "css_element" + + Scenario: Users can create new content if they have the required permission + Given I navigate to "H5P > Manage H5P content types" in site administration + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager + And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" + And I should see "H5P content types uploaded successfully" + And I click on "Site pages" "list_item" in the "Navigation" "block" + When I click on "Content bank" "link" in the "Navigation" "block" + And I click on "[data-action=Add-content]" "css_element" + Then I click on "Fill in the Blanks" "link" + And I switch to "h5p-editor-iframe" class iframe + And I switch to the main frame + And I click on "Cancel" "button" + + Scenario: Users can't edit content if they don't have the required permission + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | user1@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 I navigate to "H5P > Manage H5P content types" in site administration + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager + And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" + And I should see "H5P content types uploaded successfully" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" + And "[data-action=Add-content]" "css_element" should exist + When the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/contentbank:useeditor | Prohibit | editingteacher | System | | + And I reload the page + Then "[data-action=Add-content]" "css_element" should not exist diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 4d6aa44f723..ea7fcebbffb 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -32,6 +32,7 @@ $string['contentnotrenamed'] = 'An error was encountered while trying to rename $string['contentrenamed'] = 'The content has been renamed.'; $string['contentsmoved'] = 'Content bank contents moved to {$a}.'; $string['contenttypenoaccess'] = 'You can not view this {$a} instance'; +$string['contenttypenoedit'] = 'You can not edit contents of the {$a} content type'; $string['eventcontentcreated'] = 'Content created'; $string['eventcontentdeleted'] = 'Content deleted'; $string['eventcontentupdated'] = 'Content updated';