From a7a1f0d48cdb3cfc15d79ba55a11925d901c131e Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Fri, 25 Sep 2020 14:01:59 +0200 Subject: [PATCH 1/3] MDL-63806 glossary: Move delete code to API function --- mod/glossary/deleteentry.php | 92 +----------------- mod/glossary/lib.php | 137 ++++++++++++++++++++++++++ mod/glossary/tests/lib_test.php | 167 ++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 88 deletions(-) diff --git a/mod/glossary/deleteentry.php b/mod/glossary/deleteentry.php index 624130da9168f..ea174b729203a 100644 --- a/mod/glossary/deleteentry.php +++ b/mod/glossary/deleteentry.php @@ -46,107 +46,23 @@ 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&mode=$prevmode&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); diff --git a/mod/glossary/lib.php b/mod/glossary/lib.php index 596660c4734f5..a89b01dbce8e6 100644 --- a/mod/glossary/lib.php +++ b/mod/glossary/lib.php @@ -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/lib_test.php b/mod/glossary/tests/lib_test.php index 58de9ee2b9561..74353ebd3585a 100644 --- a/mod/glossary/tests/lib_test.php +++ b/mod/glossary/tests/lib_test.php @@ -503,4 +503,171 @@ public function test_glossary_get_entries_search() { $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')); + } } From f9b56649e710d522f4121fa4033307ef76c3be5b Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Fri, 25 Sep 2020 14:02:26 +0200 Subject: [PATCH 2/3] MDL-63806 glossary: Return user permissions for entries in WS --- mod/glossary/classes/external.php | 12 ++++++++++++ mod/glossary/tests/external_test.php | 17 +++++++++++++++-- mod/glossary/upgrade.txt | 4 ++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mod/glossary/classes/external.php b/mod/glossary/classes/external.php index a06d115cc0207..683b49239478c 100644 --- a/mod/glossary/classes/external.php +++ b/mod/glossary/classes/external.php @@ -1406,10 +1406,16 @@ public static function get_entry_by_id($id) { $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 @@ public static function get_entry_by_id_returns() { 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/tests/external_test.php b/mod/glossary/tests/external_test.php index 18c3a4100420e..4f6a849f7cd46 100644 --- a/mod/glossary/tests/external_test.php +++ b/mod/glossary/tests/external_test.php @@ -1077,11 +1077,14 @@ public function test_get_entry_by_id() { $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 @@ public function test_get_entry_by_id() { $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 @@ public function test_get_entry_by_id() { // 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() { diff --git a/mod/glossary/upgrade.txt b/mod/glossary/upgrade.txt index 7738fe567a30c..07650ac3ef33f 100644 --- a/mod/glossary/upgrade.txt +++ b/mod/glossary/upgrade.txt @@ -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() From 8441d551eaccb6d2e51ba14270142d6a07275d85 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Fri, 25 Sep 2020 17:25:34 +0200 Subject: [PATCH 3/3] MDL-63806 glossary: New WS mod_glossary_delete_entry --- mod/glossary/classes/external.php | 2 +- .../classes/external/delete_entry.php | 97 +++++++++++++++++++ mod/glossary/db/services.php | 8 ++ mod/glossary/tests/external/delete_entry.php | 83 ++++++++++++++++ mod/glossary/version.php | 2 +- 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 mod/glossary/classes/external/delete_entry.php create mode 100644 mod/glossary/tests/external/delete_entry.php diff --git a/mod/glossary/classes/external.php b/mod/glossary/classes/external.php index 683b49239478c..352c702959881 100644 --- a/mod/glossary/classes/external.php +++ b/mod/glossary/classes/external.php @@ -162,7 +162,7 @@ protected static function fill_entry_details($entry, $context) { * @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'); diff --git a/mod/glossary/classes/external/delete_entry.php b/mod/glossary/classes/external/delete_entry.php new file mode 100644 index 0000000000000..7551c43c86ab9 --- /dev/null +++ b/mod/glossary/classes/external/delete_entry.php @@ -0,0 +1,97 @@ +. + +/** + * This is the external method for deleting a content. + * + * @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_warnings; + +/** + * This is the external method for deleting a content. + * + * @copyright 2020 Juan Leyva + * @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() + ]); + } +} diff --git a/mod/glossary/db/services.php b/mod/glossary/db/services.php index 942434f9847ee..74c4f5d50afcd 100644 --- a/mod/glossary/db/services.php +++ b/mod/glossary/db/services.php @@ -162,4 +162,12 @@ '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] + ], ); diff --git a/mod/glossary/tests/external/delete_entry.php b/mod/glossary/tests/external/delete_entry.php new file mode 100644 index 0000000000000..df56e078d4739 --- /dev/null +++ b/mod/glossary/tests/external/delete_entry.php @@ -0,0 +1,83 @@ +. + +/** + * External function test for delete_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; + +/** + * External function test for delete_entry. + * + * @package mod_glossary + * @copyright 2020 Juan Leyva + * @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); + } +} diff --git a/mod/glossary/version.php b/mod/glossary/version.php index 6a66d3eb72662..6e2b84371dd9e 100644 --- a/mod/glossary/version.php +++ b/mod/glossary/version.php @@ -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;