MDL-63805 glossary: New WS mod_glossary_update_entry
authorJuan Leyva <juanleyvadelgado@gmail.com>
Mon, 28 Sep 2020 19:22:20 +0000 (21:22 +0200)
committerJuan Leyva <juanleyvadelgado@gmail.com>
Wed, 14 Oct 2020 10:56:52 +0000 (12:56 +0200)
mod/glossary/classes/external.php
mod/glossary/classes/external/update_entry.php [new file with mode: 0644]
mod/glossary/db/services.php
mod/glossary/tests/external/update_entry.php [new file with mode: 0644]

index 352c702..98acf59 100644 (file)
@@ -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 (file)
index 0000000..695fbb7
--- /dev/null
@@ -0,0 +1,176 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for updating a glossary entry.
+ *
+ * @package    mod_glossary
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @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 <juan@moodle.com>
+ * @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()
+        ]);
+    }
+}
index 74c4f5d..63728be 100644 (file)
@@ -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 (file)
index 0000000..ed6ffc5
--- /dev/null
@@ -0,0 +1,297 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * External function test for update_entry.
+ *
+ * @package    mod_glossary
+ * @category   external
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @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 <juan@moodle.com>
+ * @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 = '<p>A definition</p>';
+        $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 .= ' <p>Updated!</p>';
+        $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 = '<p>A definition</p>';
+        mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+
+        $concept = 'B concept';
+        $definition = '<p>B definition</p>';
+        mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+
+        $concept = 'Another concept';
+        $definition = '<p>Another definition</p>';
+        $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']);
+    }
+}