MDL-67548 core_course: Add more category deletion hooks.
authorRuslan Kabalin <ruslan.kabalin@gmail.com>
Fri, 1 May 2020 17:00:40 +0000 (18:00 +0100)
committerRuslan Kabalin <ruslan.kabalin@gmail.com>
Tue, 12 May 2020 13:30:14 +0000 (14:30 +0100)
Introduce new hooks for plugin developers:

  - <component>_can_course_category_delete($category)
  - <component>_can_course_category_delete_move($category, $newcategory)
These hooks allow plugin developers greater control over category deletion. Plugin can return false in those
functions if category deletion or deletion with content move to the new parent category is not permitted.

  - <component>_pre_course_category_delete_move($category, $newcategory)
This hook is expanding functionality of existing <component>_pre_course_category_delete hook and allow plugin developers
to execute code prior to category deletion when its content is moved to another category.

 - <component>_get_course_category_contents($category)
This hook allow plugin developers to add information that is displayed on category deletion form. Function should
return string, which will be added to the list of category contents shown on the form.

course/classes/category.php
course/classes/deletecategory_form.php
lang/en/error.php
lib/upgrade.txt

index 7963c6f..ebc2ebf 100644 (file)
@@ -171,6 +171,24 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         debugging('Can not unset core_course_category instance properties!', DEBUG_DEVELOPER);
     }
 
+    /**
+     * Get list of plugin callback functions.
+     *
+     * @param string $name Callback function name.
+     * @return [callable] $pluginfunctions
+     */
+    public function get_plugins_callback_function(string $name) : array {
+        $pluginfunctions = [];
+        if ($pluginsfunction = get_plugins_with_function($name)) {
+            foreach ($pluginsfunction as $plugintype => $plugins) {
+                foreach ($plugins as $pluginfunction) {
+                    $pluginfunctions[] = $pluginfunction;
+                }
+            }
+        }
+        return $pluginfunctions;
+    }
+
     /**
      * Create an iterator because magic vars can't be seen by 'foreach'.
      *
@@ -1900,13 +1918,12 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
             return false;
         }
 
-        $context = $this->get_context();
-        if (!$this->is_uservisible() ||
-                !has_capability('moodle/category:manage', $context)) {
+        if (!$this->has_manage_capability()) {
             return false;
         }
 
         // Check all child categories (not only direct children).
+        $context = $this->get_context();
         $sql = context_helper::get_preload_record_columns_sql('ctx');
         $childcategories = $DB->get_records_sql('SELECT c.id, c.visible, '. $sql.
             ' FROM {context} ctx '.
@@ -1936,6 +1953,15 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
             }
         }
 
+        // Check if plugins permit deletion of category content.
+        $pluginfunctions = $this->get_plugins_callback_function('can_course_category_delete');
+        foreach ($pluginfunctions as $pluginfunction) {
+            // If at least one plugin does not permit deletion, stop and return false.
+            if (!$pluginfunction($this)) {
+                return false;
+            }
+        }
+
         return true;
     }
 
@@ -1961,13 +1987,9 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         $settimeout = core_php_time_limit::raise();
 
         // Allow plugins to use this category before we completely delete it.
-        if ($pluginsfunction = get_plugins_with_function('pre_course_category_delete')) {
-            $category = $this->get_db_record();
-            foreach ($pluginsfunction as $plugintype => $plugins) {
-                foreach ($plugins as $pluginfunction) {
-                    $pluginfunction($category);
-                }
-            }
+        $pluginfunctions = $this->get_plugins_callback_function('pre_course_category_delete');
+        foreach ($pluginfunctions as $pluginfunction) {
+            $pluginfunction($this->get_db_record());
         }
 
         $deletedcourses = array();
@@ -2072,25 +2094,35 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
     public function can_move_content_to($newcatid) {
         global $CFG;
         require_once($CFG->libdir . '/questionlib.php');
-        $context = $this->get_context();
-        if (!$this->is_uservisible() ||
-                !has_capability('moodle/category:manage', $context)) {
+
+        if (!$this->has_manage_capability()) {
             return false;
         }
+
         $testcaps = array();
         // If this category has courses in it, user must have 'course:create' capability in target category.
         if ($this->has_courses()) {
             $testcaps[] = 'moodle/course:create';
         }
         // If this category has subcategories or questions, user must have 'category:manage' capability in target category.
-        if ($this->has_children() || question_context_has_any_questions($context)) {
+        if ($this->has_children() || question_context_has_any_questions($this->get_context())) {
             $testcaps[] = 'moodle/category:manage';
         }
-        if (!empty($testcaps)) {
-            return has_all_capabilities($testcaps, context_coursecat::instance($newcatid));
+        if (!empty($testcaps) && !has_all_capabilities($testcaps, context_coursecat::instance($newcatid))) {
+            // No sufficient capabilities to perform this task.
+            return false;
+        }
+
+        // Check if plugins permit moving category content.
+        $pluginfunctions = $this->get_plugins_callback_function('can_course_category_delete_move');
+        $newparentcat = self::get($newcatid, MUST_EXIST, true);
+        foreach ($pluginfunctions as $pluginfunction) {
+            // If at least one plugin does not permit move on deletion, stop and return false.
+            if (!$pluginfunction($this, $newparentcat)) {
+                return false;
+            }
         }
 
-        // There is no content but still return true.
         return true;
     }
 
@@ -2120,6 +2152,12 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         $coursesids = $DB->get_fieldset_select('course', 'id', 'category = :category ORDER BY sortorder ASC', $params);
         $context = $this->get_context();
 
+        // Allow plugins to make necessary changes before we move the category content.
+        $pluginfunctions = $this->get_plugins_callback_function('pre_course_category_delete_move');
+        foreach ($pluginfunctions as $pluginfunction) {
+            $pluginfunction($this, $newparentcat);
+        }
+
         if ($children) {
             foreach ($children as $childcat) {
                 $childcat->change_parent_raw($newparentcat);
index 285047b..ddbd433 100644 (file)
@@ -76,14 +76,23 @@ class core_course_deletecategory_form extends moodleform {
         // Describe the contents of this category.
         $contents = '';
         if ($this->coursecat->has_children()) {
-            $contents .= '<li>' . get_string('subcategories') . '</li>';
+            $contents .= html_writer::tag('li', get_string('subcategories'));
         }
         if ($this->coursecat->has_courses()) {
-            $contents .= '<li>' . get_string('courses') . '</li>';
+            $contents .= html_writer::tag('li', get_string('courses'));
         }
         if (question_context_has_any_questions($categorycontext)) {
-            $contents .= '<li>' . get_string('questionsinthequestionbank') . '</li>';
+            $contents .= html_writer::tag('li', get_string('questionsinthequestionbank'));
         }
+
+        // Check if plugins can provide more info.
+        $pluginfunctions = $this->coursecat->get_plugins_callback_function('get_course_category_contents');
+        foreach ($pluginfunctions as $pluginfunction) {
+            if ($plugincontents = $pluginfunction($this->coursecat)) {
+                $contents .= html_writer::tag('li', $plugincontents);
+            }
+        }
+
         if (!empty($contents)) {
             $mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
         } else {
@@ -92,7 +101,9 @@ class core_course_deletecategory_form extends moodleform {
 
         // Give the options for what to do.
         $mform->addElement('select', 'fulldelete', get_string('whattodo'), $options);
+
         if (count($options) == 1) {
+            // Freeze selector if only one option available.
             $optionkeys = array_keys($options);
             $option = reset($optionkeys);
             $mform->hardFreeze('fulldelete');
@@ -127,6 +138,11 @@ class core_course_deletecategory_form extends moodleform {
         if (empty($data['fulldelete']) && empty($data['newparent'])) {
             // When they have chosen the move option, they must specify a destination.
             $errors['newparent'] = get_string('required');
+            return $errors;
+        }
+
+        if (!empty($data['newparent']) && !$this->coursecat->can_move_content_to($data['newparent'])) {
+            $errors['newparent'] = get_string('movecatcontentstoselected', 'error');
         }
 
         return $errors;
index cf78c4c..5a6c463 100644 (file)
@@ -405,6 +405,7 @@ $string['moduledoesnotexist'] = 'This module does not exist';
 $string['moduleinstancedoesnotexist'] = 'The instance of this module does not exist';
 $string['modulemissingcode'] = 'Module {$a} is missing the code needed to perform this function';
 $string['movecatcontentstoroot'] = 'Moving the category content to root is not allowed. You must move the contents to an existing category!';
+$string['movecatcontentstoselected'] = 'Some of the category content can not be moved into selected category.';
 $string['movecategorynotpossible'] = 'You cannot move category \'{$a}\' into the selected category.';
 $string['movecategoryownparent'] = 'You cannot make category \'{$a}\' a parent of itself.';
 $string['movecategoryparentconflict'] = 'You cannot make category \'{$a}\' a subcategory of one of its own subcategories.';
index d8ab4bc..f82110e 100644 (file)
@@ -53,6 +53,20 @@ information provided here is intended especially for developers.
   The confirmation dialogue no longer has a configurable "No" button as per similar changes in MDL-59759.
   This set of confirmation modals was unintentionally missed from that deprecation process.
 * The download_as_dataformat() method has been deprecated. Please use \core\dataformat::download_data() instead
+* Introduce new hooks for plugin developers:
+    - <component>_can_course_category_delete($category)
+    - <component>_can_course_category_delete_move($category, $newcategory)
+  These hooks allow plugin developers greater control over category deletion. Plugin can return false in those
+  functions if category deletion or deletion with content move to the new parent category is not permitted.
+  Both $category and $newcategory params are instances of core_course_category class.
+    - <component>_pre_course_category_delete_move($category, $newcategory)
+  This hook is expanding functionality of existing <component>_pre_course_category_delete hook and allow plugin developers
+  to execute code prior to category deletion when its content is moved to another category.
+  Both $category and $newcategory params are instances of core_course_category class.
+    - <component>_get_course_category_contents($category)
+  This hook allow plugin developers to add information that is displayed on category deletion form. Function should
+  return string, which will be added to the list of category contents shown on the form. $category param is an instance
+  of core_course_category class.
 
 === 3.8 ===
 * Add CLI option to notify all cron tasks to stop: admin/cli/cron.php --stop