From 004be78003890985a932221f0952a77f9c3e2a1d Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 28 Sep 2020 21:22:20 +0200 Subject: [PATCH] MDL-63805 glossary: New WS mod_glossary_update_entry --- mod/glossary/classes/external.php | 4 +- .../classes/external/update_entry.php | 176 +++++++++++ mod/glossary/db/services.php | 9 + mod/glossary/tests/external/update_entry.php | 297 ++++++++++++++++++ 4 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 mod/glossary/classes/external/update_entry.php create mode 100644 mod/glossary/tests/external/update_entry.php diff --git a/mod/glossary/classes/external.php b/mod/glossary/classes/external.php index 352c7029598..98acf593dd2 100644 --- a/mod/glossary/classes/external.php +++ b/mod/glossary/classes/external.php @@ -1397,7 +1397,7 @@ class mod_glossary_external extends external_api { // Get and validate the glossary. $entry = $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST); - list($glossary, $context) = self::validate_glossary($entry->glossaryid); + list($glossary, $context, $course, $cm) = self::validate_glossary($entry->glossaryid); if (empty($entry->approved) && $entry->userid != $USER->id && !has_capability('mod/glossary:approve', $context)) { throw new invalid_parameter_exception('invalidentry'); @@ -1409,6 +1409,7 @@ class mod_glossary_external extends external_api { // Permissions (for entry edition). $permissions = [ 'candelete' => mod_glossary_can_delete_entry($entry, $glossary, $context), + 'canupdate' => mod_glossary_can_update_entry($entry, $glossary, $context, $cm), ]; return array( @@ -1433,6 +1434,7 @@ class mod_glossary_external extends external_api { 'permissions' => new external_single_structure( [ 'candelete' => new external_value(PARAM_BOOL, 'Whether the user can delete the entry.'), + 'canupdate' => new external_value(PARAM_BOOL, 'Whether the user can update the entry.'), ], 'User permissions for the managing the entry.', VALUE_OPTIONAL ), diff --git a/mod/glossary/classes/external/update_entry.php b/mod/glossary/classes/external/update_entry.php new file mode 100644 index 00000000000..695fbb72d3e --- /dev/null +++ b/mod/glossary/classes/external/update_entry.php @@ -0,0 +1,176 @@ +. + +/** + * This is the external method for updating a glossary entry. + * + * @package mod_glossary + * @since Moodle 3.10 + * @copyright 2020 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_glossary\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/glossary/lib.php'); + +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use external_format_value; +use external_warnings; +use core_text; +use moodle_exception; + +/** + * This is the external method for updating a glossary entry. + * + * @copyright 2020 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_entry extends external_api { + /** + * Parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'entryid' => new external_value(PARAM_INT, 'Glossary entry id to update'), + 'concept' => new external_value(PARAM_TEXT, 'Glossary concept'), + 'definition' => new external_value(PARAM_RAW, 'Glossary concept definition'), + 'definitionformat' => new external_format_value('definition'), + 'options' => new external_multiple_structure ( + new external_single_structure( + [ + 'name' => new external_value(PARAM_ALPHANUM, + 'The allowed keys (value format) are: + inlineattachmentsid (int); the draft file area id for inline attachments + attachmentsid (int); the draft file area id for attachments + categories (comma separated int); comma separated category ids + aliases (comma separated str); comma separated aliases + usedynalink (bool); whether the entry should be automatically linked. + casesensitive (bool); whether the entry is case sensitive. + fullmatch (bool); whether to match whole words only.'), + 'value' => new external_value(PARAM_RAW, 'the value of the option (validated inside the function)') + ] + ), 'Optional settings', VALUE_DEFAULT, [] + ) + ]); + } + + /** + * Update the indicated glossary entry. + * + * @param int $entryid The entry to update + * @param string $concept the glossary concept + * @param string $definition the concept definition + * @param int $definitionformat the concept definition format + * @param array $options additional settings + * @return array with result and warnings + * @throws moodle_exception + */ + public static function execute(int $entryid, string $concept, string $definition, int $definitionformat, + array $options = []): array { + + global $DB; + + $params = self::validate_parameters(self::execute_parameters(), compact('entryid', 'concept', 'definition', + 'definitionformat', 'options')); + $id = $params['entryid']; + + // Get and validate the glossary entry. + $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST); + list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid); + + // Check if the user can update the entry. + mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); + + // Check for duplicates if the concept changes. + if (!$glossary->allowduplicatedentries && + core_text::strtolower($entry->concept) != core_text::strtolower(trim($params['concept']))) { + + if (glossary_concept_exists($glossary, $params['concept'])) { + throw new moodle_exception('errconceptalreadyexists', 'glossary'); + } + } + + // Prepare the entry object. + $entry->aliases = ''; + $entry = mod_glossary_prepare_entry_for_edition($entry); + $entry->concept = $params['concept']; + $entry->definition_editor = [ + 'text' => $params['definition'], + 'format' => $params['definitionformat'], + ]; + // Options. + foreach ($params['options'] as $option) { + $name = trim($option['name']); + switch ($name) { + case 'inlineattachmentsid': + $entry->definition_editor['itemid'] = clean_param($option['value'], PARAM_INT); + break; + case 'attachmentsid': + $entry->attachment_filemanager = clean_param($option['value'], PARAM_INT); + break; + case 'categories': + $entry->categories = clean_param($option['value'], PARAM_SEQUENCE); + $entry->categories = explode(',', $entry->categories); + break; + case 'aliases': + $entry->aliases = clean_param($option['value'], PARAM_NOTAGS); + // Convert to the expected format. + $entry->aliases = str_replace(",", "\n", $entry->aliases); + break; + case 'usedynalink': + case 'casesensitive': + case 'fullmatch': + // Only allow if linking is enabled. + if ($glossary->usedynalink) { + $entry->{$name} = clean_param($option['value'], PARAM_BOOL); + } + break; + default: + throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); + } + } + + $entry = glossary_edit_entry($entry, $course, $cm, $glossary, $context); + + return [ + 'result' => true, + 'warnings' => [], + ]; + } + + /** + * Return. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The update result'), + 'warnings' => new external_warnings() + ]); + } +} diff --git a/mod/glossary/db/services.php b/mod/glossary/db/services.php index 74c4f5d50af..63728bef0e6 100644 --- a/mod/glossary/db/services.php +++ b/mod/glossary/db/services.php @@ -170,4 +170,13 @@ $functions = array( 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ], + + 'mod_glossary_update_entry' => [ + 'classname' => 'mod_glossary\external\update_entry', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Updates the given glossary entry.', + 'type' => 'write', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] + ], ); diff --git a/mod/glossary/tests/external/update_entry.php b/mod/glossary/tests/external/update_entry.php new file mode 100644 index 00000000000..ed6ffc52e66 --- /dev/null +++ b/mod/glossary/tests/external/update_entry.php @@ -0,0 +1,297 @@ +. + +/** + * External function test for update_entry. + * + * @package mod_glossary + * @category external + * @since Moodle 3.10 + * @copyright 2020 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_glossary\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use external_api; +use externallib_advanced_testcase; +use mod_glossary_external; +use context_module; +use context_user; +use external_util; + +/** + * External function test for update_entry. + * + * @package mod_glossary + * @copyright 2020 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_entry_testcase extends externallib_advanced_testcase { + + /** + * test_update_entry_without_optional_settings + */ + public function test_update_entry_without_optional_settings() { + global $CFG, $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); + + $this->setAdminUser(); + $concept = 'A concept'; + $definition = '

A definition

'; + $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); + $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); + $entryid = $return['entryid']; + + // Updates the entry. + $concept .= ' Updated!'; + $definition .= '

Updated!

'; + $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); + $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); + + // Get entry from DB. + $entry = $DB->get_record('glossary_entries', ['id' => $entryid]); + + $this->assertEquals($concept, $entry->concept); + $this->assertEquals($definition, $entry->definition); + $this->assertEquals($CFG->glossary_linkentries, $entry->usedynalink); + $this->assertEquals($CFG->glossary_casesensitive, $entry->casesensitive); + $this->assertEquals($CFG->glossary_fullmatch, $entry->fullmatch); + $this->assertEmpty($DB->get_records('glossary_alias', ['entryid' => $entryid])); + $this->assertEmpty($DB->get_records('glossary_entries_categories', ['entryid' => $entryid])); + } + + /** + * test_update_entry_duplicated + */ + public function test_update_entry_duplicated() { + global $CFG, $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'allowduplicatedentries' => 1]); + + // Create three entries. + $this->setAdminUser(); + $concept = 'A concept'; + $definition = '

A definition

'; + mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); + + $concept = 'B concept'; + $definition = '

B definition

'; + mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); + + $concept = 'Another concept'; + $definition = '

Another definition

'; + $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); + $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); + $entryid = $return['entryid']; + + // Updates the entry using an existing entry name when duplicateds are allowed. + $concept = 'A concept'; + update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); + + // Updates the entry using an existing entry name when duplicateds are NOT allowed. + $DB->set_field('glossary', 'allowduplicatedentries', 0, ['id' => $glossary->id]); + $concept = 'B concept'; + $this->expectExceptionMessage(get_string('errconceptalreadyexists', 'glossary')); + update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); + } + + /** + * test_update_entry_with_aliases + */ + public function test_update_entry_with_aliases() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); + + $this->setAdminUser(); + $concept = 'A concept'; + $definition = 'A definition'; + $paramaliases = 'abc, def, gez'; + $options = [ + [ + 'name' => 'aliases', + 'value' => $paramaliases, + ] + ]; + $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); + $entryid = $return['entryid']; + + // Updates the entry. + $newaliases = 'abz, xyz'; + $options[0]['value'] = $newaliases; + $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); + + $aliases = $DB->get_records('glossary_alias', ['entryid' => $entryid]); + $this->assertCount(2, $aliases); + foreach ($aliases as $alias) { + $this->assertContains($alias->alias, $newaliases); + } + } + + /** + * test_update_entry_in_categories + */ + public function test_update_entry_in_categories() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); + $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); + $cat1 = $gg->create_category($glossary); + $cat2 = $gg->create_category($glossary); + $cat3 = $gg->create_category($glossary); + + $this->setAdminUser(); + $concept = 'A concept'; + $definition = 'A definition'; + $paramcategories = "$cat1->id, $cat2->id"; + $options = [ + [ + 'name' => 'categories', + 'value' => $paramcategories, + ] + ]; + $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); + $entryid = $return['entryid']; + + // Updates the entry. + $newcategories = "$cat1->id, $cat3->id"; + $options[0]['value'] = $newcategories; + $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); + + $categories = $DB->get_records('glossary_entries_categories', ['entryid' => $entryid]); + $this->assertCount(2, $categories); + foreach ($categories as $category) { + $this->assertContains($category->categoryid, $newcategories); + } + } + + /** + * test_update_entry_with_attachments + */ + public function test_update_entry_with_attachments() { + global $DB, $USER; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); + $context = context_module::instance($glossary->cmid); + + $this->setAdminUser(); + $concept = 'A concept'; + $definition = 'A definition'; + + // Draft files. + $draftidinlineattach = file_get_unused_draft_itemid(); + $draftidattach = file_get_unused_draft_itemid(); + $usercontext = context_user::instance($USER->id); + $filerecordinline = [ + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftidinlineattach, + 'filepath' => '/', + 'filename' => 'shouldbeanimage.png', + ]; + $fs = get_file_storage(); + + // Create a file in a draft area for regular attachments. + $filerecordattach = $filerecordinline; + $attachfilename = 'attachment.txt'; + $filerecordattach['filename'] = $attachfilename; + $filerecordattach['itemid'] = $draftidattach; + $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); + $fs->create_file_from_string($filerecordattach, 'simple text attachment'); + + $options = [ + [ + 'name' => 'inlineattachmentsid', + 'value' => $draftidinlineattach, + ], + [ + 'name' => 'attachmentsid', + 'value' => $draftidattach, + ] + ]; + $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); + $entryid = $return['entryid']; + $entry = $DB->get_record('glossary_entries', ['id' => $entryid]); + + list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry); + + $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry', + $entry->id); + $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment', + $entry->id); + + $inlineattachmentsid = $entry->definition_editor['itemid']; + $attachmentsid = $entry->attachment_filemanager; + + // Change the file areas. + + // Delete one inline editor file. + $selectedfile = (object)[ + 'filename' => $filerecordinline['filename'], + 'filepath' => $filerecordinline['filepath'], + ]; + $return = repository_delete_selected_files($usercontext, 'user', 'draft', $inlineattachmentsid, [$selectedfile]); + + // Add more files. + $filerecordinline['filename'] = 'newvideo.mp4'; + $filerecordinline['itemid'] = $inlineattachmentsid; + + $filerecordattach['filename'] = 'newattach.txt'; + $filerecordattach['itemid'] = $attachmentsid; + + $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); + $fs->create_file_from_string($filerecordattach, 'simple text attachment'); + + // Updates the entry. + $options[0]['value'] = $inlineattachmentsid; + $options[1]['value'] = $attachmentsid; + $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); + $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); + + $editorfiles = external_util::get_area_files($context->id, 'mod_glossary', 'entry', $entryid); + $attachmentfiles = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $entryid); + + $this->assertCount(1, $editorfiles); + $this->assertCount(2, $attachmentfiles); + + $this->assertEquals('newvideo.mp4', $editorfiles[0]['filename']); + $this->assertEquals('attachment.txt', $attachmentfiles[0]['filename']); + $this->assertEquals('newattach.txt', $attachmentfiles[1]['filename']); + } +} -- 2.43.0