Merge branch 'MDL-63806-master' of git://github.com/jleyva/moodle into master
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 13 Oct 2020 20:26:27 +0000 (22:26 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 13 Oct 2020 20:26:27 +0000 (22:26 +0200)
mod/glossary/classes/external.php
mod/glossary/classes/external/delete_entry.php [new file with mode: 0644]
mod/glossary/db/services.php
mod/glossary/deleteentry.php
mod/glossary/lib.php
mod/glossary/tests/external/delete_entry.php [new file with mode: 0644]
mod/glossary/tests/external_test.php
mod/glossary/tests/lib_test.php
mod/glossary/upgrade.txt
mod/glossary/version.php

index a06d115..352c702 100644 (file)
@@ -162,7 +162,7 @@ class mod_glossary_external extends external_api {
      * @param  int $id The glossary ID.
      * @return array Contains glossary, context, course and cm.
      */
-    protected static function validate_glossary($id) {
+    public static function validate_glossary($id) {
         global $DB;
         $glossary = $DB->get_record('glossary', array('id' => $id), '*', MUST_EXIST);
         list($course, $cm) = get_course_and_cm_from_instance($glossary, 'glossary');
@@ -1406,10 +1406,16 @@ class mod_glossary_external extends external_api {
         $entry = glossary_get_entry_by_id($id);
         self::fill_entry_details($entry, $context);
 
+        // Permissions (for entry edition).
+        $permissions = [
+            'candelete' => mod_glossary_can_delete_entry($entry, $glossary, $context),
+        ];
+
         return array(
             'entry' => $entry,
             'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry',
                 array($entry)),
+            'permissions' => $permissions,
             'warnings' => $warnings
         );
     }
@@ -1424,6 +1430,12 @@ class mod_glossary_external extends external_api {
         return new external_single_structure(array(
             'entry' => self::get_entry_return_structure(),
             'ratinginfo' => \core_rating\external\util::external_ratings_structure(),
+            'permissions' => new external_single_structure(
+                [
+                    'candelete' => new external_value(PARAM_BOOL, 'Whether the user can delete the entry.'),
+                ],
+                'User permissions for the managing the entry.', VALUE_OPTIONAL
+            ),
             'warnings' => new external_warnings()
         ));
     }
diff --git a/mod/glossary/classes/external/delete_entry.php b/mod/glossary/classes/external/delete_entry.php
new file mode 100644 (file)
index 0000000..7551c43
--- /dev/null
@@ -0,0 +1,97 @@
+<?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 deleting a content.
+ *
+ * @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_warnings;
+
+/**
+ * This is the external method for deleting a content.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_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 delete'),
+        ]);
+    }
+
+    /**
+     * Delete the indicated entry from the glossary.
+     *
+     * @param  int $entryid The entry to delete
+     * @return array with result and warnings
+     * @throws moodle_exception
+     */
+    public static function execute(int $entryid): array {
+        global $DB;
+
+        $params = self::validate_parameters(self::execute_parameters(), compact('entryid'));
+        $id = $params['entryid'];
+
+        // Get and validate the glossary.
+        $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST);
+        list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid);
+
+        // Check and delete.
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+        mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course);
+
+        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 processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+}
index 942434f..74c4f5d 100644 (file)
@@ -162,4 +162,12 @@ $functions = array(
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
+    'mod_glossary_delete_entry' => [
+        'classname'     => 'mod_glossary\external\delete_entry',
+        'methodname'    => 'execute',
+        'classpath'     => '',
+        'description'   => 'Delete the given entry from the glossary.',
+        'type'          => 'write',
+        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
 );
index 624130d..ea174b7 100644 (file)
@@ -46,107 +46,23 @@ if ($cm->instance != $entry->glossaryid) {
 
 require_login($course, false, $cm);
 $context = context_module::instance($cm->id);
-$manageentries = has_capability('mod/glossary:manageentries', $context);
 
 if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) {
     print_error('invalidid', 'glossary');
 }
 
-
-$strareyousuredelete = get_string("areyousuredelete","glossary");
-
-if (($entry->userid != $USER->id) and !$manageentries) { // guest id is never matched, no need for special check here
-    print_error('nopermissiontodelentry');
-}
-$ineditperiod = ((time() - $entry->timecreated <  $CFG->maxeditingtime) || $glossary->editalways);
-if (!$ineditperiod and !$manageentries) {
-    print_error('errdeltimeexpired', 'glossary');
-}
+// Throws an exception if the user cannot delete the entry.
+mod_glossary_can_delete_entry($entry, $glossary, $context, false);
 
 /// If data submitted, then process and store.
 
 if ($confirm and confirm_sesskey()) { // the operation was confirmed.
-    // if it is an imported entry, just delete the relation
-
-    $origentry = fullclone($entry);
-    if ($entry->sourceglossaryid) {
-        if (!$newcm = get_coursemodule_from_instance('glossary', $entry->sourceglossaryid)) {
-            print_error('invalidcoursemodule');
-        }
-        $newcontext = context_module::instance($newcm->id);
-
-        $entry->glossaryid       = $entry->sourceglossaryid;
-        $entry->sourceglossaryid = 0;
-        $DB->update_record('glossary_entries', $entry);
-
-        // move attachments too
-        $fs = get_file_storage();
-
-        if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) {
-            foreach ($oldfiles as $oldfile) {
-                $file_record = new stdClass();
-                $file_record->contextid = $newcontext->id;
-                $fs->create_file_from_storedfile($file_record, $oldfile);
-            }
-            $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
-            $entry->attachment = '1';
-        } else {
-            $entry->attachment = '0';
-        }
-        $DB->update_record('glossary_entries', $entry);
-
-    } else {
-        $fs = get_file_storage();
-        $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
-        $DB->delete_records("comments", array('itemid'=>$entry->id, 'commentarea'=>'glossary_entry', 'contextid'=>$context->id));
-        $DB->delete_records("glossary_alias", array("entryid"=>$entry->id));
-        $DB->delete_records("glossary_entries", array("id"=>$entry->id));
-
-        // Update completion state
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) {
-            $completion->update_state($cm, COMPLETION_INCOMPLETE, $entry->userid);
-        }
-
-        //delete glossary entry ratings
-        require_once($CFG->dirroot.'/rating/lib.php');
-        $delopt = new stdClass;
-        $delopt->contextid = $context->id;
-        $delopt->component = 'mod_glossary';
-        $delopt->ratingarea = 'entry';
-        $delopt->itemid = $entry->id;
-        $rm = new rating_manager();
-        $rm->delete_ratings($delopt);
-    }
-
-    // Delete cached RSS feeds.
-    if (!empty($CFG->enablerssfeeds)) {
-        require_once($CFG->dirroot.'/mod/glossary/rsslib.php');
-        glossary_rss_delete_file($glossary);
-    }
-
-    core_tag_tag::remove_all_item_tags('mod_glossary', 'glossary_entries', $origentry->id);
-
-    $event = \mod_glossary\event\entry_deleted::create(array(
-        'context' => $context,
-        'objectid' => $origentry->id,
-        'other' => array(
-            'mode' => $prevmode,
-            'hook' => $hook,
-            'concept' => $origentry->concept
-        )
-    ));
-    $event->add_record_snapshot('glossary_entries', $origentry);
-    $event->trigger();
-
-    // Reset caches.
-    if ($entry->usedynalink and $entry->approved) {
-        \mod_glossary\local\concept_cache::reset_glossary($glossary);
-    }
 
+    mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook, $prevmode);
     redirect("view.php?id=$cm->id&amp;mode=$prevmode&amp;hook=$hook");
 
 } else {        // the operation has not been confirmed yet so ask the user to do so
+    $strareyousuredelete = get_string("areyousuredelete", "glossary");
     $PAGE->navbar->add(get_string('delete'));
     $PAGE->set_title($glossary->name);
     $PAGE->set_heading($course->fullname);
index 596660c..a89b01d 100644 (file)
@@ -4318,3 +4318,140 @@ function mod_glossary_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * Checks if the current user can delete the given glossary entry.
+ *
+ * @since Moodle 3.10
+ * @param stdClass $entry the entry database object
+ * @param stdClass $glossary the glossary database object
+ * @param stdClass $context the glossary context
+ * @param bool $return Whether to return a boolean value or stop the execution (exception)
+ * @return bool if the user can delete the entry
+ * @throws moodle_exception
+ */
+function mod_glossary_can_delete_entry($entry, $glossary, $context, $return = true) {
+    global $USER, $CFG;
+
+    $manageentries = has_capability('mod/glossary:manageentries', $context);
+
+    if ($manageentries) {   // Users with the capability will always be able to delete entries.
+        return true;
+    }
+
+    if ($entry->userid != $USER->id) { // Guest id is never matched, no need for special check here.
+        if ($return) {
+            return false;
+        }
+        throw new moodle_exception('nopermissiontodelentry');
+    }
+
+    $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways);
+
+    if (!$ineditperiod) {
+        if ($return) {
+            return false;
+        }
+        throw new moodle_exception('errdeltimeexpired', 'glossary');
+    }
+
+    return true;
+}
+
+/**
+ * Deletes the given entry, this function does not perform capabilities/permission checks.
+ *
+ * @since Moodle 3.10
+ * @param stdClass $entry the entry database object
+ * @param stdClass $glossary the glossary database object
+ * @param stdClass $cm the glossary course moduule object
+ * @param stdClass $context the glossary context
+ * @param stdClass $course the glossary course
+ * @param string $hook the hook, usually type of filtering, value
+ * @param string $prevmode the previsualisation mode
+ * @throws moodle_exception
+ */
+function mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook = '', $prevmode = '') {
+    global $CFG, $DB;
+
+    $origentry = fullclone($entry);
+
+    // If it is an imported entry, just delete the relation.
+    if ($entry->sourceglossaryid) {
+        if (!$newcm = get_coursemodule_from_instance('glossary', $entry->sourceglossaryid)) {
+            print_error('invalidcoursemodule');
+        }
+        $newcontext = context_module::instance($newcm->id);
+
+        $entry->glossaryid       = $entry->sourceglossaryid;
+        $entry->sourceglossaryid = 0;
+        $DB->update_record('glossary_entries', $entry);
+
+        // Move attachments too.
+        $fs = get_file_storage();
+
+        if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) {
+            foreach ($oldfiles as $oldfile) {
+                $filerecord = new stdClass();
+                $filerecord->contextid = $newcontext->id;
+                $fs->create_file_from_storedfile($filerecord, $oldfile);
+            }
+            $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
+            $entry->attachment = '1';
+        } else {
+            $entry->attachment = '0';
+        }
+        $DB->update_record('glossary_entries', $entry);
+
+    } else {
+        $fs = get_file_storage();
+        $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
+        $DB->delete_records("comments",
+            ['itemid' => $entry->id, 'commentarea' => 'glossary_entry', 'contextid' => $context->id]);
+        $DB->delete_records("glossary_alias", ["entryid" => $entry->id]);
+        $DB->delete_records("glossary_entries", ["id" => $entry->id]);
+
+        // Update completion state.
+        $completion = new completion_info($course);
+        if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) {
+            $completion->update_state($cm, COMPLETION_INCOMPLETE, $entry->userid);
+        }
+
+        // Delete glossary entry ratings.
+        require_once($CFG->dirroot.'/rating/lib.php');
+        $delopt = new stdClass;
+        $delopt->contextid = $context->id;
+        $delopt->component = 'mod_glossary';
+        $delopt->ratingarea = 'entry';
+        $delopt->itemid = $entry->id;
+        $rm = new rating_manager();
+        $rm->delete_ratings($delopt);
+    }
+
+    // Delete cached RSS feeds.
+    if (!empty($CFG->enablerssfeeds)) {
+        require_once($CFG->dirroot . '/mod/glossary/rsslib.php');
+        glossary_rss_delete_file($glossary);
+    }
+
+    core_tag_tag::remove_all_item_tags('mod_glossary', 'glossary_entries', $origentry->id);
+
+    $event = \mod_glossary\event\entry_deleted::create(
+        [
+            'context' => $context,
+            'objectid' => $origentry->id,
+            'other' => [
+                'mode' => $prevmode,
+                'hook' => $hook,
+                'concept' => $origentry->concept
+            ]
+        ]
+    );
+    $event->add_record_snapshot('glossary_entries', $origentry);
+    $event->trigger();
+
+    // Reset caches.
+    if ($entry->usedynalink and $entry->approved) {
+        \mod_glossary\local\concept_cache::reset_glossary($glossary);
+    }
+}
diff --git a/mod/glossary/tests/external/delete_entry.php b/mod/glossary/tests/external/delete_entry.php
new file mode 100644 (file)
index 0000000..df56e07
--- /dev/null
@@ -0,0 +1,83 @@
+<?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 delete_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;
+
+/**
+ * External function test for delete_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 delete_entry_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test the behaviour of delete_entry().
+     */
+    public function test_delete_entry() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+
+        // Test entry creator can delete.
+        $result = delete_entry::execute($entry->id);
+        $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result);
+        $this->assertTrue($result['result']);
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+
+        // Test admin can delete.
+        $this->setAdminUser();
+        $entry = $gg->create_content($glossary);
+        $result = delete_entry::execute($entry->id);
+        $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result);
+        $this->assertTrue($result['result']);
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+
+        $entry = $gg->create_content($glossary);
+        // Test a different student is not able to delete.
+        $this->setUser($anotherstudent);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        delete_entry::execute($entry->id);
+    }
+}
index 18c3a41..4f6a849 100644 (file)
@@ -1077,11 +1077,14 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
         $c1 = $this->getDataGenerator()->create_course();
         $c2 = $this->getDataGenerator()->create_course();
         $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id));
-        $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'visible' => 0));
+        $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'visible' => 0));
         $u1 = $this->getDataGenerator()->create_user();
         $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
         $ctx = context_module::instance($g1->cmid);
         $this->getDataGenerator()->enrol_user($u1->id, $c1->id);
+        $this->getDataGenerator()->enrol_user($u2->id, $c1->id);
+        $this->getDataGenerator()->enrol_user($u3->id, $c1->id);
 
         $e1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs')));
         // Add a fake inline image to the entry.
@@ -1108,10 +1111,12 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('Cats', $return['entry']['tags'][0]['rawname']);
         $this->assertEquals('Dogs', $return['entry']['tags'][1]['rawname']);
         $this->assertEquals($filename, $return['entry']['definitioninlinefiles'][0]['filename']);
+        $this->assertTrue($return['permissions']['candelete']);
 
         $return = mod_glossary_external::get_entry_by_id($e2->id);
         $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
         $this->assertEquals($e2->id, $return['entry']['id']);
+        $this->assertTrue($return['permissions']['candelete']);
 
         try {
             $return = mod_glossary_external::get_entry_by_id($e3->id);
@@ -1127,11 +1132,19 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
             // All good.
         }
 
-        // An admin can be other's entries to be approved.
+        // An admin can see other's entries to be approved.
         $this->setAdminUser();
         $return = mod_glossary_external::get_entry_by_id($e3->id);
         $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
         $this->assertEquals($e3->id, $return['entry']['id']);
+        $this->assertTrue($return['permissions']['candelete']);
+
+        // Students can see other students approved entries but they will not be able to delete them.
+        $this->setUser($u3);
+        $return = mod_glossary_external::get_entry_by_id($e1->id);
+        $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
+        $this->assertEquals($e1->id, $return['entry']['id']);
+        $this->assertFalse($return['permissions']['candelete']);
     }
 
     public function test_add_entry_without_optional_settings() {
index 58de9ee..74353eb 100644 (file)
@@ -503,4 +503,171 @@ class mod_glossary_lib_testcase extends advanced_testcase {
         $search = glossary_get_entries_search($concept, $course->id);
         $this->assertCount(0, $search);
     }
+
+    public function test_mod_glossary_can_delete_entry_users() {
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+
+        // Test student can delete.
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test teacher can delete.
+        $this->setUser($teacher);
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test admin can delete.
+        $this->setAdminUser();
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test a different student is not able to delete.
+        $this->setUser($anotherstudent);
+        $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test exception.
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+    }
+
+    public function test_mod_glossary_can_delete_entry_edit_period() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'editalways' => 1]);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+
+        // Test student can always delete when edit always is set to 1.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test student cannot delete old entries when edit always is set to 0.
+        $glossary->editalways = 0;
+        $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test student can delete recent entries when edit always is set to 0.
+        $entry->timecreated = time();
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Check exception.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->expectExceptionMessage(get_string('errdeltimeexpired', 'glossary'));
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+    }
+
+    public function test_mod_glossary_delete_entry() {
+        global $DB, $CFG;
+        $this->resetAfterTest();
+        require_once($CFG->dirroot . '/rating/lib.php');
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->assessed = RATING_AGGREGATE_AVERAGE;
+        $scale = $this->getDataGenerator()->create_scale(['scale' => 'A,B,C,D']);
+        $record->scale = "-$scale->id";
+        $glossary = $this->getDataGenerator()->create_module('glossary', $record);
+        $context = context_module::instance($glossary->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary->id);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student1);
+
+        // Create entry with tags and rating.
+        $entry = $gg->create_content(
+            $glossary,
+            ['approved' => 1, 'userid' => $student1->id, 'tags' => ['Cats', 'Dogs']],
+            ['alias1', 'alias2']
+        );
+
+        // Rate the entry as user2.
+        $rating1 = new stdClass();
+        $rating1->contextid = $context->id;
+        $rating1->component = 'mod_glossary';
+        $rating1->ratingarea = 'entry';
+        $rating1->itemid = $entry->id;
+        $rating1->rating = 1; // 1 is A.
+        $rating1->scaleid = "-$scale->id";
+        $rating1->userid = $student2->id;
+        $rating1->timecreated = time();
+        $rating1->timemodified = time();
+        $rating1->id = $DB->insert_record('rating', $rating1);
+
+        $sink = $this->redirectEvents();
+        mod_glossary_delete_entry(fullclone($entry), $glossary, $cm, $context, $course);
+        $events = $sink->get_events();
+        $event = array_pop($events);
+
+        // Check events.
+        $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname);
+        $this->assertEquals($entry->id, $event->objectid);
+        $sink->close();
+
+        // No entry, no alias, no ratings, no tags.
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+        $this->assertEquals(0, $DB->count_records('glossary_alias', ['entryid' => $entry->id]));
+        $this->assertEquals(0, $DB->count_records('rating', ['component' => 'mod_glossary', 'itemid' => $entry->id]));
+        $this->assertEmpty(core_tag_tag::get_by_name(0, 'Cats'));
+    }
+
+    public function test_mod_glossary_delete_entry_imported() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $glossary2 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $context = context_module::instance($glossary2->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary2->id);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+
+        $entry1 = $gg->create_content($glossary1);
+        $entry2 = $gg->create_content(
+            $glossary2,
+            ['approved' => 1, 'userid' => $student->id, 'sourceglossaryid' => $glossary1->id, 'tags' => ['Cats', 'Dogs']]
+        );
+
+        $sink = $this->redirectEvents();
+        mod_glossary_delete_entry(fullclone($entry2), $glossary2, $cm, $context, $course);
+        $events = $sink->get_events();
+        $event = array_pop($events);
+
+        // Check events.
+        $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname);
+        $this->assertEquals($entry2->id, $event->objectid);
+        $sink->close();
+
+        // Check source.
+        $this->assertEquals(0, $DB->get_field('glossary_entries', 'sourceglossaryid', ['id' => $entry2->id]));
+        $this->assertEquals($glossary1->id, $DB->get_field('glossary_entries', 'glossaryid', ['id' => $entry2->id]));
+
+        // Tags.
+        $this->assertEmpty(core_tag_tag::get_by_name(0, 'Cats'));
+    }
 }
index 7738fe5..07650ac 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/glossary/*,
 information provided here is intended especially for developers.
 
+=== 3.10 ===
+* External function get_entries_by_id now returns and additional "permissions" field indicating the user permissions for managing
+  the entry.
+
 === 3.8 ===
 * The following functions have been finally deprecated and can not be used anymore:
     * glossary_scale_used()
index 6a66d3e..6e2b843 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2021052500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2021052501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2021052500;    // Requires this Moodle version
 $plugin->component = 'mod_glossary';   // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;