Merge branch 'MDL-63371' of git://github.com/timhunt/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Sep 2018 21:09:20 +0000 (23:09 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Sep 2018 21:09:20 +0000 (23:09 +0200)
27 files changed:
admin/tool/dataprivacy/amd/build/defaultsactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/defaultsactions.js [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/external/category_exporter.php
admin/tool/dataprivacy/classes/external/purpose_exporter.php
admin/tool/dataprivacy/classes/form/defaults.php [deleted file]
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/defaults_page.php [new file with mode: 0644]
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/templates/category_purpose_form.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_display.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_page.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/delete_activity_defaults.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_defaults.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/tests/generator/lib.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
blocks/rss_client/classes/output/item.php
group/assign.php
user/tests/behat/filter_participants.feature

diff --git a/admin/tool/dataprivacy/amd/build/defaultsactions.min.js b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js
new file mode 100644 (file)
index 0000000..b8f0bc3
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/defaultsactions.js b/admin/tool/dataprivacy/amd/src/defaultsactions.js
new file mode 100644 (file)
index 0000000..76cbb89
--- /dev/null
@@ -0,0 +1,315 @@
+// 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/>.
+
+/**
+ * AMD module for data registry defaults actions.
+ *
+ * @module     tool_dataprivacy/defaultsactions
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core/ajax',
+    'core/notification',
+    'core/str',
+    'core/modal_factory',
+    'core/modal_events',
+    'core/templates'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{EDIT_LEVEL_DEFAULTS: string}}
+     * @type {{NEW_ACTIVITY_DEFAULTS: string}}
+     * @type {{EDIT_ACTIVITY_DEFAULTS: string}}
+     * @type {{DELETE_ACTIVITY_DEFAULTS: string}}
+     */
+    var ACTIONS = {
+        EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
+        NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
+        EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
+        DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
+    };
+
+    /** @type {{INHERIT: Number}} **/
+    var INHERIT = -1;
+
+    /**
+     * DefaultsActions class.
+     */
+    var DefaultsActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    DefaultsActions.prototype.registerEvents = function() {
+        $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
+            $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
+                var categories = categoryResponse.options;
+                var purposes = purposeResponse.options;
+                showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
+
+                return true;
+            }).catch(Notification.exception);
+        });
+
+        $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+            var activity = button.data('activityname');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var activity = button.data('activityname');
+            var activityDisplayName = button.data('activitydisplayname');
+            // Set category and purpose to inherit (-1).
+            var category = INHERIT;
+            var purpose = INHERIT;
+
+            ModalFactory.create({
+                title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
+                body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
+                type: ModalFactory.types.SAVE_CANCEL,
+                large: true
+            }).then(function(modal) {
+                modal.setSaveButtonText(Str.get_string('delete'));
+
+                // Handle save event.
+                modal.getRoot().on(ModalEvents.save, function() {
+                    setContextDefaults(contextLevel, category, purpose, activity, false);
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                modal.show();
+
+                return true;
+            }).catch(Notification.exception);
+        });
+    };
+
+    /**
+     * Prepares and renders the modal for setting the defaults for the given context level/plugin.
+     *
+     * @param {String} title The modal's title.
+     * @param {Number} contextLevel The context level to set defaults for.
+     * @param {Number} category The current category ID.
+     * @param {Number} purpose The current purpose ID.
+     * @param {String} activity The plugin name of the activity. Optional.
+     * @param {Array} categoryOptions The list of category options.
+     * @param {Array} purposeOptions The list of purpose options.
+     * @param {Array} activityOptions The list of activity options. Optional.
+     */
+    function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
+                                   categoryOptions, purposeOptions, activityOptions) {
+
+        if (category !== null) {
+            categoryOptions.forEach(function(currentValue) {
+                if (currentValue.id === category) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        if (purpose !== null) {
+            purposeOptions.forEach(function(currentValue) {
+                if (currentValue.id === purpose) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        var templateContext = {
+            "contextlevel": contextLevel,
+            "categoryoptions": categoryOptions,
+            "purposeoptions": purposeOptions
+        };
+
+        // Check the activityOptions parameter that was passed.
+        if (activityOptions !== null && activityOptions.length) {
+            // Check the activity parameter that was passed.
+            if (activity === null) {
+                // We're setting a new defaults for a module.
+                templateContext.newactivitydefaults = true;
+
+            } else {
+                // Edit mode. Set selection.
+                activityOptions.forEach(function(currentValue) {
+                    if (activity === currentValue.name) {
+                        currentValue.selected = true;
+                    }
+                });
+            }
+
+            templateContext.modemodule = true;
+            templateContext.activityoptions = activityOptions;
+        }
+
+        ModalFactory.create({
+            title: title,
+            body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
+            type: ModalFactory.types.SAVE_CANCEL,
+            large: true
+        }).then(function(modal) {
+
+            // Handle save event.
+            modal.getRoot().on(ModalEvents.save, function() {
+                var activity = $('#activity');
+                var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
+                var override = $('#override');
+                var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
+
+                setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
+            });
+
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            modal.show();
+
+            return modal;
+        }).catch(Notification.exception);
+    }
+
+    /**
+     * Calls a the tool_dataprivacy_set_context_defaults WS function.
+     *
+     * @param {Number} contextLevel The context level.
+     * @param {Number} category The category ID.
+     * @param {Number} purpose The purpose ID.
+     * @param {String} activity The plugin name of the activity module.
+     * @param {Boolean} override Whether to override custom instances.
+     */
+    function setContextDefaults(contextLevel, category, purpose, activity, override) {
+        var request = {
+            methodname: 'tool_dataprivacy_set_context_defaults',
+            args: {
+                'contextlevel': contextLevel,
+                'category': category,
+                'purpose': purpose,
+                'override': override,
+                'activity': activity
+            }
+        };
+
+        Ajax.call([request])[0].done(function(data) {
+            if (data.result) {
+                window.location.reload();
+            }
+        });
+    }
+
+    return /** @alias module:tool_dataprivacy/defaultsactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the module.
+         *
+         * @method init
+         * @return {DefaultsActions}
+         */
+        'init': function() {
+            return new DefaultsActions();
+        }
+    };
+});
index 6ee9707..7428152 100644 (file)
@@ -24,7 +24,7 @@
 namespace tool_dataprivacy;
 
 use coding_exception;
-use context_course;
+use context_helper;
 use context_system;
 use core\invalid_persistent_exception;
 use core\message\message;
@@ -1141,4 +1141,91 @@ class api {
 
         return $approvedcollection;
     }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $categoryid The ID matching the category.
+     * @param int $purposeid The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
+     */
+    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
+        global $DB;
+
+        self::check_can_manage_data_registry();
+
+        // Get the class name associated with this context level.
+        $classname = context_helper::get_class_for_level($contextlevel);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
+
+        // Check the default category to be set.
+        if ($categoryid == context_instance::INHERIT) {
+            unset_config($categoryvar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given category ID exists first.
+            $categorypersistent = new category($categoryid);
+            $categorypersistent->read();
+
+            // Then set the new default value.
+            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
+        }
+
+        // Check the default purpose to be set.
+        if ($purposeid == context_instance::INHERIT) {
+            // If the defaults is set to inherit, just unset the config value.
+            unset_config($purposevar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given purpose ID exists first.
+            $purposepersistent = new purpose($purposeid);
+            $purposepersistent->read();
+
+            // Then set the new default value.
+            set_config($purposevar, $purposeid, 'tool_dataprivacy');
+        }
+
+        // Unset instances that have been assigned with custom purpose and category, if override was specified.
+        if ($override) {
+            // We'd like to find context IDs that we want to unset.
+            $statements = ["SELECT c.id as contextid FROM {context} c"];
+            // Based on this context level.
+            $params = ['contextlevel' => $contextlevel];
+
+            if ($contextlevel == CONTEXT_MODULE) {
+                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
+                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
+                // And that the module is listed on the modules table.
+                $statements[] = "JOIN {modules} m ON m.id = cm.module";
+
+                if ($activity) {
+                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
+                    $statements[] = "AND m.name = :modname";
+                    $params['modname'] = $activity;
+                }
+            }
+            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
+            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
+            // And that the context level of this instance matches the given context level.
+            $statements[] = "WHERE c.contextlevel = :contextlevel";
+
+            // Build our SQL query by gluing the statements.
+            $sql = implode("\n", $statements);
+
+            // Get the context records matching our query.
+            $contextids = $DB->get_fieldset_sql($sql, $params);
+
+            // Delete the matching context instances.
+            foreach ($contextids as $contextid) {
+                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
+                    self::unset_context_instance($instance);
+                }
+            }
+        }
+
+        return true;
+    }
 }
index 1fac2f5..7b46b4d 100644 (file)
 namespace tool_dataprivacy;
 
 use coding_exception;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\category;
-use tool_dataprivacy\contextlevel;
-use tool_dataprivacy\context_instance;
+use core\persistent;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -57,13 +54,22 @@ class data_registry {
     /**
      * Returns purpose and category var names from a context class name
      *
-     * @param string $classname
+     * @param string $classname The context level's class.
+     * @param string $pluginname The name of the plugin associated with the context level.
      * @return string[]
      */
-    public static function var_names_from_context($classname) {
+    public static function var_names_from_context($classname, $pluginname = '') {
+        $pluginname = trim($pluginname);
+        if (!empty($pluginname)) {
+            $categoryvar = $classname . '_' . $pluginname . '_category';
+            $purposevar = $classname . '_' . $pluginname . '_purpose';
+        } else {
+            $categoryvar = $classname . '_category';
+            $purposevar = $classname . '_purpose';
+        }
         return [
-            $classname . '_purpose',
-            $classname . '_category',
+            $purposevar,
+            $categoryvar
         ];
     }
 
@@ -72,17 +78,31 @@ class data_registry {
      *
      * The caller code is responsible of checking that $contextlevel is an integer.
      *
-     * @param int $contextlevel
-     * @return int|false[]
+     * @param int $contextlevel The context level.
+     * @param string $pluginname The name of the plugin associated with the context level.
+     * @return int[]|false[]
      */
-    public static function get_defaults($contextlevel) {
+    public static function get_defaults($contextlevel, $pluginname = '') {
 
         $classname = \context_helper::get_class_for_level($contextlevel);
-        list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+        list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
 
         $purposeid = get_config('tool_dataprivacy', $purposevar);
         $categoryid = get_config('tool_dataprivacy', $categoryvar);
 
+        if (!empty($pluginname)) {
+            list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+            // If the plugin-level doesn't have a default purpose set, try the context level.
+            if ($purposeid === false) {
+                $purposeid = get_config('tool_dataprivacy', $purposevar);
+            }
+
+            // If the plugin-level doesn't have a default category set, try the context level.
+            if ($categoryid === false) {
+                $categoryid = get_config('tool_dataprivacy', $categoryvar);
+            }
+        }
+
         if (empty($purposeid)) {
             $purposeid = false;
         }
@@ -197,9 +217,15 @@ class data_registry {
                 // Use the context level value as we don't allow people to set specific instances values.
                 return self::get_effective_contextlevel_value($context->contextlevel, $element);
             } else {
+                // Check if we need to pass the plugin name of an activity.
+                $forplugin = '';
+                if ($context->contextlevel == CONTEXT_MODULE) {
+                    list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
+                    $forplugin = $cm->modname;
+                }
                 // Use the default context level value.
                 list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
-                    $context->contextlevel
+                    $context->contextlevel, false, false, $forplugin
                 );
                 return self::get_element_instance($element, $$fieldname);
             }
@@ -283,14 +309,15 @@ class data_registry {
      * Returns the effective default purpose and category for a context level.
      *
      * @param int $contextlevel
-     * @param int $forcedpurposevalue Use this value as if this was this context level purpose.
-     * @param int $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
+     * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param string $activity The plugin name of the activity.
      * @return int[]
      */
     public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
-                                                                                   $forcedcategoryvalue = false) {
+                                                                                   $forcedcategoryvalue = false, $activity = '') {
 
-        list($purposeid, $categoryid) = self::get_defaults($contextlevel);
+        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
 
         // Honour forced values.
         if ($forcedpurposevalue) {
index e14e072..692e072 100644 (file)
@@ -1110,6 +1110,287 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameters for set_context_defaults().
+     *
+     * @return external_function_parameters
+     */
+    public static function set_context_defaults_parameters() {
+        return new external_function_parameters([
+            'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
+            'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
+            'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
+            'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
+            'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
+                false),
+        ]);
+    }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $category The ID matching the category.
+     * @param int $purpose The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return array
+     */
+    public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
+        $warnings = [];
+
+        $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
+            'contextlevel' => $contextlevel,
+            'category' => $category,
+            'purpose' => $purpose,
+            'activity' => $activity,
+            'override' => $override,
+        ]);
+        $contextlevel = $params['contextlevel'];
+        $category = $params['category'];
+        $purpose = $params['purpose'];
+        $activity = $params['activity'];
+        $override = $params['override'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Set the context defaults.
+        $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for set_context_defaults().
+     *
+     * @return external_single_structure
+     */
+    public static function set_context_defaults_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_category_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_category_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data category options containing category IDs as keys and the category name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_category_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $categories = api::get_categories();
+        $options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
+        $categoryoptions = [];
+        foreach ($options as $id => $name) {
+            $categoryoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $categoryoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_category_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The category ID'),
+                'name' => new external_value(PARAM_TEXT, 'The category name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_purpose_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_purpose_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_purpose_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $purposes = api::get_purposes();
+        $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
+        $purposeoptions = [];
+        foreach ($options as $id => $name) {
+            $purposeoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $purposeoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_purpose_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_purpose_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The purpose ID'),
+                'name' => new external_value(PARAM_TEXT, 'The purpose name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_activity_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_activity_options_parameters() {
+        return new external_function_parameters([
+            'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
+                VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of activity options for setting data registry defaults.
+     *
+     * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
+     *                            that don't have defaults yet (e.g. when adding a new activity module defaults).
+     * @return array
+     */
+    public static function get_activity_options($nodefaults) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_activity_options_parameters(), [
+            'nodefaults' => $nodefaults,
+        ]);
+        $nodefaults = $params['nodefaults'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Get activity module plugin info.
+        $pluginmanager = \core_plugin_manager::instance();
+        $modplugins = $pluginmanager->get_enabled_plugins('mod');
+        $modoptions = [];
+
+        // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
+        list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
+        foreach ($modplugins as $name) {
+            // Check if we have default purpose and category for this module if we want don't want to fetch everything.
+            if ($nodefaults) {
+                list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
+                // Compare this with the module-level defaults.
+                if ($purpose !== $levelpurpose || $category !== $levelcategory) {
+                    // If the defaults for this activity has been already set, there's no need to add this in the list of options.
+                    continue;
+                }
+            }
+
+            $displayname = $pluginmanager->plugin_name('mod_' . $name);
+            $modoptions[] = (object)[
+                'name' => $name,
+                'displayname' => $displayname
+            ];
+        }
+
+        return [
+            'options' => $modoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_activity_options_returns() {
+        $optionsdefinition = new external_single_structure(
+            [
+                'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
+                'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optionsdefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Gets the structure of a tree node (link + child branches).
      *
index c5c350c..06d2469 100644 (file)
@@ -25,6 +25,8 @@ namespace tool_dataprivacy\external;
 defined('MOODLE_INTERNAL') || die();
 
 use core\external\persistent_exporter;
+use tool_dataprivacy\category;
+use tool_dataprivacy\context_instance;
 
 /**
  * Class for exporting field data.
@@ -53,4 +55,25 @@ class category_exporter extends persistent_exporter {
             'context' => 'context',
         );
     }
+
+    /**
+     * Utility function that fetches a category name from the given ID.
+     *
+     * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($categoryid) {
+        global $PAGE;
+        if ($categoryid === false || $categoryid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($categoryid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new category($categoryid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
index f91e2f2..a6519c6 100644 (file)
@@ -29,6 +29,7 @@ use core\external\persistent_exporter;
 use DateInterval;
 use Exception;
 use renderer_base;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\purpose;
 
 /**
@@ -143,4 +144,25 @@ class purpose_exporter extends persistent_exporter {
 
         return $values;
     }
+
+    /**
+     * Utility function that fetches a purpose name from the given ID.
+     *
+     * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($purposeid) {
+        global $PAGE;
+        if ($purposeid === false || $purposeid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($purposeid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new purpose($purposeid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/form/defaults.php b/admin/tool/dataprivacy/classes/form/defaults.php
deleted file mode 100644 (file)
index 4e599c3..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?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 file contains the defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace tool_dataprivacy\form;
-defined('MOODLE_INTERNAL') || die();
-
-use \tool_dataprivacy\output\data_registry_page;
-
-require_once($CFG->libdir . '/formslib.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
-
-/**
- * Context levels defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class defaults extends \moodleform {
-
-    /**
-     * Define the form.
-     */
-    public function definition() {
-        global $OUTPUT;
-
-        $mform = $this->_form;
-        $mform->setDisableShortforms();
-
-        $notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
-            \core\output\notification::NOTIFY_INFO);
-        $mform->addElement('html', $notification);
-
-        foreach ($this->_customdata['levels'] as $level => $classname) {
-
-            $mform->addElement('header', $classname . '-header',
-                get_string('contextlevelname' . $level, 'tool_dataprivacy'));
-
-            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-            $includeinherit = true;
-            if ($level == CONTEXT_SYSTEM) {
-                $includeinherit = false;
-            }
-
-            $categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
-            $purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
-
-            $mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
-            $mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
-            $mform->setType($categoryvar, PARAM_INT);
-
-            $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
-            $mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
-            $mform->setType($purposevar, PARAM_INT);
-        }
-
-        $this->add_action_buttons(true, get_string('savechanges'));
-    }
-}
index 8d5fd35..be0e996 100644 (file)
@@ -425,7 +425,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of purpose persistents to a list of id => name purposes.
      *
-     * @param \tool_dataprivacy\purpose $purposes
+     * @param \tool_dataprivacy\purpose[] $purposes
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
@@ -442,7 +442,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of category persistents to a list of id => name categories.
      *
-     * @param \tool_dataprivacy\category $categories
+     * @param \tool_dataprivacy\category[] $categories
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
diff --git a/admin/tool/dataprivacy/classes/output/defaults_page.php b/admin/tool/dataprivacy/classes/output/defaults_page.php
new file mode 100644 (file)
index 0000000..166228f
--- /dev/null
@@ -0,0 +1,178 @@
+<?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/>.
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use action_menu_link_primary;
+use coding_exception;
+use moodle_exception;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+use tool_dataprivacy\data_registry;
+use tool_dataprivacy\external\category_exporter;
+use tool_dataprivacy\external\purpose_exporter;
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaults_page implements renderable, templatable {
+
+    /** @var int $mode The display mode. */
+    protected $mode = null;
+
+    /** @var int $category The default category for the given mode. */
+    protected $category = null;
+
+    /** @var int $purpose The default purpose for the given mode. */
+    protected $purpose = null;
+
+    /** @var stdClass[] $otherdefaults Other defaults for the given mode. */
+    protected $otherdefaults = [];
+
+    /** @var bool $canedit Whether editing is allowed. */
+    protected $canedit = false;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param int $mode The display mode.
+     * @param int $category The default category for the given mode.
+     * @param int $purpose The default purpose for the given mode.
+     * @param stdClass[] $otherdefaults Other defaults for the given mode.
+     * @param bool $canedit Whether editing is allowed.
+     */
+    public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
+        $this->mode = $mode;
+        $this->category = $category;
+        $this->purpose = $purpose;
+        $this->otherdefaults = $otherdefaults;
+        $this->canedit = $canedit;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = new stdClass();
+
+        // Set tab URLs.
+        $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
+        $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
+        $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
+        $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
+        $data->coursecaturl = $coursecaturl;
+        $data->courseurl = $courseurl;
+        $data->moduleurl = $moduleurl;
+        $data->blockurl = $blockurl;
+
+        // Set display mode.
+        switch ($this->mode) {
+            case CONTEXT_COURSECAT:
+                $data->modecoursecat = true;
+                break;
+            case CONTEXT_COURSE:
+                $data->modecourse = true;
+                break;
+            case CONTEXT_MODULE:
+                $data->modemodule = true;
+                break;
+            case CONTEXT_BLOCK:
+                $data->modeblock = true;
+                break;
+            default:
+                $data->modecoursecat = true;
+                break;
+        }
+
+        // Set config variables.
+        $configname = \context_helper::get_class_for_level($this->mode);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
+        $data->categoryvar = $categoryvar;
+        $data->purposevar = $purposevar;
+
+        // Set default category.
+        $data->categoryid = $this->category;
+        $data->category = category_exporter::get_name($this->category);
+
+        // Set default purpose.
+        $data->purposeid = $this->purpose;
+        $data->purpose = purpose_exporter::get_name($this->purpose);
+
+        // Set other defaults.
+        $otherdefaults = [];
+        $url = new moodle_url('#');
+        foreach ($this->otherdefaults as $pluginname => $values) {
+            $defaults = [
+                'name' => $values->name,
+                'category' => category_exporter::get_name($values->category),
+                'purpose' => purpose_exporter::get_name($values->purpose),
+            ];
+            if ($this->canedit) {
+                $actions = [];
+                // Edit link.
+                $editattrs = [
+                    'data-action' => 'edit-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-category' => $values->category,
+                    'data-purpose' => $values->purpose,
+                ];
+                $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
+                    get_string('edit'), $editattrs);
+                $actions[] = $editlink->export_for_template($output);
+
+                // Delete link.
+                $deleteattrs = [
+                    'data-action' => 'delete-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-activitydisplayname' => $values->name,
+                ];
+                $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
+                    get_string('delete'), $deleteattrs);
+                $actions[] = $deletelink->export_for_template($output);
+
+                $defaults['actions'] = $actions;
+            }
+            $otherdefaults[] = (object)$defaults;
+        }
+        $data->otherdefaults = $otherdefaults;
+
+        $data->canedit = $this->canedit;
+        $data->contextlevel = $this->mode;
+
+        return $data;
+    }
+}
index 9c71e8c..b9e6077 100644 (file)
@@ -173,4 +173,44 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_set_context_defaults' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'set_context_defaults',
+        'classpath'     => '',
+        'description'   => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_category_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_category_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data category options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_purpose_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_purpose_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data storage purpose options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_activity_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_activity_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of activity options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
 ];
index d936ba2..9faad87 100644 (file)
@@ -32,47 +32,39 @@ $title = get_string('setdefaults', 'tool_dataprivacy');
 
 \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
 
-$levels = \context_helper::get_all_levels();
-// They are set through the context level site and user.
-unset($levels[CONTEXT_SYSTEM]);
-unset($levels[CONTEXT_USER]);
+$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
+$classname = context_helper::get_class_for_level($mode);
+list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+$purpose = get_config('tool_dataprivacy', $purposevar);
+$category = get_config('tool_dataprivacy', $categoryvar);
 
-$customdata = [
-    'levels' => $levels,
-    'purposes' => \tool_dataprivacy\api::get_purposes(),
-    'categories' => \tool_dataprivacy\api::get_categories(),
-];
-$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
+$otherdefaults = [];
+if ($mode == CONTEXT_MODULE) {
+    // Get activity module plugin info.
+    $pluginmanager = core_plugin_manager::instance();
+    $modplugins = $pluginmanager->get_enabled_plugins('mod');
 
-$toform = new stdClass();
-foreach ($levels as $level => $classname) {
-    list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-    $toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
-    $toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
-}
-$form->set_data($toform);
-
-$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
-if ($form->is_cancelled()) {
-    redirect($returnurl);
-} else if ($data = $form->get_data()) {
-
-    foreach ($levels as $level => $classname) {
-
-        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-        if (isset($data->{$purposevar})) {
-            set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
-        }
-        if (isset($data->{$categoryvar})) {
-            set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
+    foreach ($modplugins as $name) {
+        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+        $plugincategory = get_config('tool_dataprivacy', $categoryvar);
+        $pluginpurpose = get_config('tool_dataprivacy', $purposevar);
+        if ($plugincategory === false && $pluginpurpose === false) {
+            // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+            continue;
         }
+
+        $displayname = $pluginmanager->plugin_name('mod_' . $name);
+        $otherdefaults[$name] = (object)[
+            'name' => $displayname,
+            'category' => $plugincategory,
+            'purpose' => $pluginpurpose,
+        ];
     }
-    redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
-        0, \core\output\notification::NOTIFY_SUCCESS);
 }
 
+$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
+
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
-$form->display();
+echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
 echo $output->footer();
index eb2c97f..682dc99 100644 (file)
@@ -28,6 +28,7 @@ $string['pluginname'] = 'Data privacy';
 $string['pluginname_help'] = 'Data privacy plugin';
 $string['activitiesandresources'] = 'Activities and resources';
 $string['addcategory'] = 'Add category';
+$string['addnewdefaults'] = 'Add a new module default';
 $string['addpurpose'] = 'Add purpose';
 $string['approve'] = 'Approve';
 $string['approverequest'] = 'Approve request';
@@ -76,9 +77,12 @@ $string['datarequests'] = 'Data requests';
 $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
 $string['daterequested'] = 'Date requested';
 $string['daterequesteddetail'] = 'Date requested:';
-$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
+$string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
+$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
 $string['deletecategory'] = 'Delete category';
 $string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
+$string['deletedefaults'] = 'Delete defaults: {$a}';
+$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
 $string['deleteexpiredcontextstask'] = 'Delete expired contexts';
 $string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
 $string['deletepurpose'] = 'Delete purpose';
@@ -95,6 +99,8 @@ $string['dporolemapping_desc'] = 'The privacy officer can manage data requests.
 $string['editcategories'] = 'Edit categories';
 $string['editcategory'] = 'Edit category';
 $string['editcategories'] = 'Edit categories';
+$string['editdefaults'] = 'Edit defaults: {$a}';
+$string['editmoduledefaults'] = 'Edit module defaults';
 $string['editpurpose'] = 'Edit purpose';
 $string['editpurposes'] = 'Edit purposes';
 $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
@@ -185,6 +191,7 @@ $string['nopurposes'] = 'There are no purposes yet';
 $string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
 $string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
 $string['notset'] = 'Not set (use the default value)';
+$string['overrideinstances'] = 'Reset instances with custom values';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
index e6ddf93..7a7015a 100644 (file)
@@ -28,3 +28,7 @@ dd a.contactdpo {
 [data-region="data-requests-table"] .moodle-actionmenu {
     min-width: 150px;
 }
+
+.context-level-view {
+    margin: 1em;
+}
\ No newline at end of file
diff --git a/admin/tool/dataprivacy/templates/category_purpose_form.mustache b/admin/tool/dataprivacy/templates/category_purpose_form.mustache
new file mode 100644 (file)
index 0000000..c5d0ed4
--- /dev/null
@@ -0,0 +1,99 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/category_purpose_form
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * actionurl String The action URL.
+    * contextlevel Number - The context level.
+    * modemodule Boolean - Whether to display the activity select element.
+    * activityoptions Array - An array of objects for the activity select element.
+    * categoryoptions Array - An array of objects for the category select element.
+    * purposeoptions Array - An array of objects for the purpose select element.
+
+    Example context (json):
+    {
+        "actionurl": "#",
+        "contextlevel": 70,
+        "newactivitydefaults": true,
+        "modemodule": true,
+        "activityoptions": [
+            { "name": "assign", "displayname": "Assignment" },
+            { "name": "forum", "displayname": "Forum", "selected": true },
+            { "name": "lesson", "displayname": "Lesson" },
+            { "name": "quiz", "displayname": "Quiz" }
+        ],
+        "categoryoptions": [
+            { "id": 1, "name": "Category 1" },
+            { "id": 2, "name": "Category 2", "selected": true },
+            { "id": 3, "name": "Category 3" }
+        ],
+        "purposeoptions": [
+            { "id": 1, "name": "Purpose 1" },
+            { "id": 2, "name": "Purpose 2" },
+            { "id": 3, "name": "Purpose 3", "selected": true }
+        ]
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<form method="post" action="{{actionurl}}" id="category_purpose_form">
+    <input type="hidden" value="{{contextlevel}}" id="contextlevel" />
+    {{#modemodule}}
+    <div class="form-group">
+        <label for="activity">{{#str}}activitymodule{{/str}}</label>
+        {{^newactivitydefaults}}
+            <input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
+        {{/newactivitydefaults}}
+        <select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
+            {{#activityoptions}}
+                <option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
+            {{/activityoptions}}
+        </select>
+    </div>
+    {{/modemodule}}
+    <div class="form-group">
+        <label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="category">
+        {{#categoryoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/categoryoptions}}
+        </select>
+    </div>
+    <div class="form-group">
+        <label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="purpose">
+        {{#purposeoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/purposeoptions}}
+        </select>
+    </div>
+    <div class="form-check">
+        <input class="form-check-input" type="checkbox" value="1" id="override">
+        <label class="form-check-label" for="override">
+            {{#str}}overrideinstances, tool_dataprivacy{{/str}}
+        </label>
+    </div>
+</form>
diff --git a/admin/tool/dataprivacy/templates/defaults_display.mustache b/admin/tool/dataprivacy/templates/defaults_display.mustache
new file mode 100644 (file)
index 0000000..39a29aa
--- /dev/null
@@ -0,0 +1,74 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_display
+
+    Renders the current default category and purpose.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * categoryid Number - The category ID.
+    * purposeid Number - The purpose ID.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+
+    Example context (json):
+    {
+        "category": "Awesome default category",
+        "categoryid": 1,
+        "purpose": "Awesome default purpose",
+        "purposeid": 2,
+        "canedit": true,
+        "contextlevel": 70
+    }
+}}
+<div class="row-fluid rtl-compatible m-t-1 m-b-1">
+    <div class="col-md-9 span9">
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{category}}
+            </div>
+        </div>
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{purpose}}
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 span3 align-self-center">
+    {{#canedit}}
+        <button class="btn btn-default" {{!
+            }}data-action="edit-level-defaults" {{!
+            }}data-contextlevel="{{contextlevel}}" {{!
+            }}data-category="{{categoryid}}" {{!
+            }}data-purpose="{{purposeid}}">
+            {{#str}}edit{{/str}}
+        </button>
+    {{/canedit}}
+    </div>
+</div>
diff --git a/admin/tool/dataprivacy/templates/defaults_page.mustache b/admin/tool/dataprivacy/templates/defaults_page.mustache
new file mode 100644 (file)
index 0000000..bb531da
--- /dev/null
@@ -0,0 +1,147 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_page
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * modecoursecat Boolean - Whether we're displaying defaults for course categories.
+    * modecourse Boolean - Whether we're displaying defaults for courses.
+    * modemodule Boolean - Whether we're displaying defaults for activity modules.
+    * modeblock Boolean - Whether we're displaying defaults for blocks.
+    * coursecaturl String - The URL for the course category defaults tab.
+    * courseurl String - The URL for the course defaults tab.
+    * moduleurl String - The URL for the activity module defaults tab.
+    * blockurl String - The URL for the block defaults tab.
+    * purposeid Number - The purpose ID for this context level.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+    * categoryid Number - The ID of the default category for this context level.
+    * purposeid Number - The ID of the default purpose for this context level.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * otherdefaults Array - An array containing the defaults for the activity modules.
+
+    Example context (json):
+    {
+        "contextlevel": 70,
+        "modecoursecat": false,
+        "modecourse": false,
+        "modemodule": true,
+        "modeblock": false,
+        "coursecaturl": "#",
+        "courseurl": "#",
+        "moduleurl": "#",
+        "blockurl": "#",
+        "category": "Awesome default category",
+        "purpose": "Awesome default purpose",
+        "canedit": true,
+        "otherdefaults": [
+            {
+                "name": "Assignment",
+                "category": "Category for activity modules",
+                "purpose": "Assessments"
+            },
+            {
+                "name": "Forum",
+                "category": "Category for activity modules",
+                "purpose": "Social interactions"
+            }
+        ]
+    }
+}}
+<div class="card">
+    <div class="card-header">
+        <ul class="nav nav-tabs card-header-tabs">
+            <li class="nav-item">
+                <a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
+            </li>
+        </ul>
+    </div>
+    <div class="card-body context-level-view">
+        <div class="alert alert-primary" role="alert">
+            {{#str}}defaultsinfo, tool_dataprivacy{{/str}}
+        </div>
+        <h4 class="card-title" id="defaults-header">
+            {{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
+            {{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
+            {{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
+            {{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
+        </h4>
+        <div>
+            {{> tool_dataprivacy/defaults_display}}
+            {{#canedit}}
+                {{#modemodule}}
+                <button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
+                    {{#str}}addnewdefaults, tool_dataprivacy{{/str}}
+                </button>
+                {{/modemodule}}
+            {{/canedit}}
+            {{#modemodule}}
+            <table class="m-t-1 table table-striped">
+                <thead>
+                    <tr>
+                        <th>&nbsp;</th>
+                        <th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
+                        <th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
+                        {{#canedit}}
+                            <th scope="col">{{#str}}actions{{/str}}</th>
+                        {{/canedit}}
+                    </tr>
+                </thead>
+                <tbody>
+                    {{#otherdefaults}}
+                    <tr>
+                        <th scope="row">{{name}}</th>
+                        <td>{{category}}</td>
+                        <td>{{purpose}}</td>
+                        {{#canedit}}
+                            <td>
+                                {{#actions}}
+                                    {{> core/action_menu_link}}
+                                {{/actions}}
+                            </td>
+                        {{/canedit}}
+                    </tr>
+                    {{/otherdefaults}}
+                </tbody>
+            </table>
+            {{/modemodule}}
+        </div>
+    </div>
+</div>
+
+{{#js}}
+// Initialise the JS.
+require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
+    ActionsMod.init();
+});
+{{/js}}
diff --git a/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache b/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache
new file mode 100644 (file)
index 0000000..c0220d3
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/delete_activity_defaults
+
+    Renders the confirmation.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * activityname String - The activity name.
+
+    Example context (json):
+    {
+        "activityname": "Assignment"
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<div>
+    {{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
+</div>
index 9feebcf..18915c1 100644 (file)
@@ -1321,6 +1321,164 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
     }
 
+    /**
+     * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
+     */
+    public function set_context_defaults_provider() {
+        $contextlevels = [
+            [CONTEXT_COURSECAT],
+            [CONTEXT_COURSE],
+            [CONTEXT_MODULE],
+            [CONTEXT_BLOCK],
+        ];
+        $paramsets = [
+            [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override.
+            [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override.
+            [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override.
+            [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override.
+            [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances.
+        ];
+        $data = [];
+        foreach ($contextlevels as $level) {
+            foreach ($paramsets as $set) {
+                $data[] = array_merge($level, $set);
+            }
+            if ($level == CONTEXT_MODULE) {
+                // Add a combination where defaults for activity is being set.
+                $data[] = [CONTEXT_MODULE, false, false, true, false];
+                $data[] = [CONTEXT_MODULE, false, false, true, true];
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Test for \tool_dataprivacy\api::set_context_defaults()
+     *
+     * @dataProvider set_context_defaults_provider
+     * @param int $contextlevel The context level
+     * @param bool $inheritcategory Whether to set category value as INHERIT.
+     * @param bool $inheritpurpose Whether to set purpose value as INHERIT.
+     * @param bool $foractivity Whether to set defaults for an activity.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $block = $generator->create_block('online_users');
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        $coursecatcontext = context_coursecat::instance($coursecat->id);
+        $coursecontext = context_course::instance($course->id);
+        $blockcontext = context_block::instance($block->id);
+
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
+        $assigncontext = context_module::instance($assigncm->id);
+        $forumcontext = context_module::instance($forumcm->id);
+
+        // Generate purposes and categories.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Assign purposes and categories to contexts.
+        $coursecatctxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecatcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $coursectxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $blockctxinstance = api::set_context_instance((object) [
+            'contextid' => $blockcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $forumctxinstance = api::set_context_instance((object) [
+            'contextid' => $forumcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id');
+        $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id');
+        $activity = '';
+        if ($contextlevel == CONTEXT_MODULE && $foractivity) {
+            $activity = 'assign';
+        }
+        $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override);
+        $this->assertTrue($result);
+
+        $targetctxinstance = false;
+        switch ($contextlevel) {
+            case CONTEXT_COURSECAT:
+                $targetctxinstance = $coursecatctxinstance;
+                break;
+            case CONTEXT_COURSE:
+                $targetctxinstance = $coursectxinstance;
+                break;
+            case CONTEXT_MODULE:
+                $targetctxinstance = $assignctxinstance;
+                break;
+            case CONTEXT_BLOCK:
+                $targetctxinstance = $blockctxinstance;
+                break;
+        }
+        $this->assertNotFalse($targetctxinstance);
+
+        // Check the context instances.
+        $instanceexists = context_instance::record_exists($targetctxinstance->get('id'));
+        if ($override) {
+            // If overridden, context instances on this context level would have been deleted.
+            $this->assertFalse($instanceexists);
+
+            // Check forum context instance.
+            $forumctxexists = context_instance::record_exists($forumctxinstance->get('id'));
+            if ($contextlevel != CONTEXT_MODULE || $foractivity) {
+                // The forum context instance won't be affected in this test if:
+                // - The overridden defaults are not for context modules.
+                // - Only the defaults for assign have been set.
+                $this->assertTrue($forumctxexists);
+            } else {
+                // If we're overriding for the whole course module context level,
+                // then this forum context instance will be deleted as well.
+                $this->assertFalse($forumctxexists);
+            }
+        } else {
+            // Otherwise, the context instance record remains.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check defaults.
+        list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity);
+        if (!$inheritpurpose) {
+            $this->assertEquals($purposeid, $defaultpurpose);
+        }
+        if (!$inheritcategory) {
+            $this->assertEquals($categoryid, $defaultcategory);
+        }
+    }
+
     /**
      * Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
      *
diff --git a/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php b/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php
new file mode 100644 (file)
index 0000000..857bb33
--- /dev/null
@@ -0,0 +1,284 @@
+<?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/>.
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
+use tool_dataprivacy\api;
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_dataprivacy extends behat_base {
+
+    /**
+     * Each element specifies:
+     * - The data generator suffix used.
+     * - The required fields.
+     * - The mapping between other elements references and database field names.
+     * @var array
+     */
+    protected static $elements = array(
+        'categories' => array(
+            'datagenerator' => 'category',
+            'required' => array()
+        ),
+        'purposes' => array(
+            'datagenerator' => 'purpose',
+            'required' => array()
+        ),
+    );
+
+    /**
+     * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
+     *
+     * @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
+     *
+     * @param string    $elementname The name of the entity to add
+     * @param TableNode $data
+     */
+    public function the_following_data_categories_exist($elementname, TableNode $data) {
+
+        // Now that we need them require the data generators.
+        require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
+
+        if (empty(self::$elements[$elementname])) {
+            throw new PendingException($elementname . ' data generator is not implemented');
+        }
+
+        $datagenerator = testing_util::get_data_generator();
+        $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
+
+        $elementdatagenerator = self::$elements[$elementname]['datagenerator'];
+        $requiredfields = self::$elements[$elementname]['required'];
+        if (!empty(self::$elements[$elementname]['switchids'])) {
+            $switchids = self::$elements[$elementname]['switchids'];
+        }
+
+        foreach ($data->getHash() as $elementdata) {
+
+            // Check if all the required fields are there.
+            foreach ($requiredfields as $requiredfield) {
+                if (!isset($elementdata[$requiredfield])) {
+                    throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
+                }
+            }
+
+            // Switch from human-friendly references to ids.
+            if (isset($switchids)) {
+                foreach ($switchids as $element => $field) {
+                    $methodname = 'get_' . $element . '_id';
+
+                    // Not all the switch fields are required, default vars will be assigned by data generators.
+                    if (isset($elementdata[$element])) {
+                        // Temp $id var to avoid problems when $element == $field.
+                        $id = $this->{$methodname}($elementdata[$element]);
+                        unset($elementdata[$element]);
+                        $elementdata[$field] = $id;
+                    }
+                }
+            }
+
+            // Preprocess the entities that requires a special treatment.
+            if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
+                $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
+            }
+
+            // Creates element.
+            $methodname = 'create_' . $elementdatagenerator;
+            if (method_exists($dataprivacygenerator, $methodname)) {
+                // Using data generators directly.
+                $dataprivacygenerator->{$methodname}($elementdata);
+
+            } else if (method_exists($this, 'process_' . $elementdatagenerator)) {
+                // Using an alternative to the direct data generator call.
+                $this->{'process_' . $elementdatagenerator}($elementdata);
+            } else {
+                throw new PendingException($elementname . ' data generator is not implemented');
+            }
+        }
+    }
+
+    /**
+     * Sets the data category and data storage purpose for the site.
+     *
+     * @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_site_category_and_purpose($category, $purpose) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $category]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
+        $data = (object)[
+            'contextlevel' => CONTEXT_SYSTEM,
+            'categoryid' => $category->get('id'),
+            'purposeid' => $purpose->get('id'),
+        ];
+        api::set_contextlevel($data);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course category instance.
+     *
+     * @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'name' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'name = :name OR idnumber = :idnumber';
+        $coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
+        $context = context_coursecat::instance($coursecatid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $name,
+            'fullname' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+        $context = context_course::instance($courseid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the activity.
+     * @param string $type The activity type. E.g. assign, quiz, forum, etc.
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        $cmid = null;
+        $cms = get_coursemodules_in_course($type, $courseid);
+        foreach ($cms as $cm) {
+            if ($cm->name === $name || $cm->idnumber === $name) {
+                $cmid = $cm->id;
+                break;
+            }
+        }
+        if ($cmid === null) {
+            throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
+        }
+        $context = context_module::instance($cmid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the block. (e.g. online_users)
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        // Fetch the course context.
+        $coursecontext = context_course::instance($courseid);
+
+        // Fetch the block record and context.
+        $blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
+        $context = context_block::instance($blockid);
+
+        // Set the category and purpose.
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the category and purpose for a context instance.
+     *
+     * @param int $contextid The context ID.
+     * @param int $categoryname The category name.
+     * @param int $purposename The purpose name.
+     * @throws coding_exception
+     */
+    protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
+
+        api::set_context_instance((object) [
+            'contextid' => $contextid,
+            'purposeid' => $purpose->get('id'),
+            'categoryid' => $category->get('id'),
+        ]);
+    }
+}
diff --git a/admin/tool/dataprivacy/tests/behat/manage_defaults.feature b/admin/tool/dataprivacy/tests/behat/manage_defaults.feature
new file mode 100644 (file)
index 0000000..a6287d5
--- /dev/null
@@ -0,0 +1,298 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data registry defaults
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the default data categories and data storage purposes for various context levels.
+
+  Background:
+    Given I log in as "admin"
+    And the following "categories" exist:
+      | name                   | idnumber | category |
+      | Science and technology | scitech  |          |
+      | Physics                | st-phys  | scitech  |
+    And the following "courses" exist:
+      | fullname                  | shortname   | category |
+      | Fundamentals of physics 1 | Physics 101 | st-phys  |
+    And the following "activities" exist:
+      | activity | name         | idnumber | course      |
+      | assign   | Assignment 1 | assign1  | Physics 101 |
+      | forum    | Forum 1      | forum1   | Physics 101 |
+    And the following "blocks" exist:
+      | blockname    | contextlevel | reference   | pagetypepattern | defaultregion |
+      | online_users | Course       | Physics 101 | course-view-*   | site-post     |
+    And the following data privacy "categories" exist:
+      | name          |
+      | Site category |
+      | Category 1    |
+      | Category 2    |
+    And the following data privacy "purposes" exist:
+      | name         | retentionperiod |
+      | Site purpose | P10Y           |
+      | Purpose 1    | P3Y            |
+      | Purpose 2    | P5Y            |
+    And I set the site category and purpose to "Site category" and "Site purpose"
+
+  Scenario: Set course category data registry defaults
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years"
+
+  Scenario: Set course data registry defaults
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course data registry defaults with override
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I click on "Forum 1 (Forum)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set block category data registry defaults
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
index 71d0111..5448d5f 100644 (file)
@@ -28,6 +28,7 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
 use tool_dataprivacy\api;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\external;
 
 /**
@@ -282,4 +283,281 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
         $this->expectException(dml_missing_record_exception::class);
         external::get_data_request($this->requestid + 1);
     }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_set_context_defaults_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults().
+     *
+     * We're just checking the module context level here to test the WS function.
+     * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($modulelevel, $override) {
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        $assigncontext = context_module::instance($assigncm->id);
+
+        // Generate purpose and category.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Set a custom purpose and ID for this assignment instance.
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $modulename = $modulelevel ? 'assign' : '';
+        $categoryid = $category2->get('id');
+        $purposeid = $purpose2->get('id');
+        $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
+
+        // Extract the result.
+        $return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
+        $this->assertTrue($return['result']);
+
+        // Check the assignment context instance.
+        $instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
+        if ($override) {
+            // The custom assign instance should have been deleted.
+            $this->assertFalse($instanceexists);
+        } else {
+            // The custom assign instance should still exist.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check the saved defaults.
+        list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
+        $this->assertEquals($categoryid, $savedcategory);
+        $this->assertEquals($purposeid, $savedpurpose);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_category_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
+     */
+    public function get_options_provider() {
+        return [
+            [false, false],
+            [false, true],
+            [true, false],
+            [true, true],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_category_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $category = api::create_category((object)['name' => 'Category ' . $i]);
+            $expectedoptions[] = [
+                'id' => $category->get('id'),
+                'name' => $category->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_category_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_purpose_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_purpose_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $purpose = api::create_purpose((object)[
+                'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+            ]);
+            $expectedoptions[] = [
+                'id' => $purpose->get('id'),
+                'name' => $purpose->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_purpose_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
+     */
+    public function get_activity_options_provider() {
+        return [
+            [false, false, true],
+            [false, true, true],
+            [true, false, true],
+            [true, true, true],
+            [false, false, false],
+            [false, true, false],
+            [true, false, false],
+            [true, true, false],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_activity_options().
+     *
+     * @dataProvider get_activity_options_provider
+     * @param bool $inheritcategory Whether the category would be set to "Inherit".
+     * @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
+     * @param bool $nodefaults Whether to fetch only activities that don't have defaults.
+     */
+    public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
+        $this->setAdminUser();
+
+        $category = api::create_category((object)['name' => 'Test category']);
+        $purpose = api::create_purpose((object)[
+            'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $categoryid = $category->get('id');
+        $purposeid = $purpose->get('id');
+
+        if ($inheritcategory) {
+            $categoryid = context_instance::INHERIT;
+        }
+        if ($inheritpurpose) {
+            $purposeid = context_instance::INHERIT;
+        }
+
+        // Set the context default for the assignment module.
+        api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
+
+        // Call the WS function.
+        $result = external::get_activity_options($nodefaults);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure the options list is not empty.
+        $this->assertNotEmpty($options);
+
+        $pluginwithdefaults = [
+            'name' => 'assign',
+            'displayname' => get_string('pluginname', 'assign')
+        ];
+
+        // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
+        // the assign module should be listed.
+        if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
+            $this->assertContains($pluginwithdefaults, $options);
+        } else {
+            $this->assertNotContains($pluginwithdefaults, $options);
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/tests/generator/lib.php b/admin/tool/dataprivacy/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..744e34a
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Data privacy tool data generator.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\category;
+use tool_dataprivacy\purpose;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data privacy tool data generator class.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_generator extends component_generator_base {
+
+    /** @var int Number of created categories. */
+    protected $categorycount = 0;
+
+    /** @var int Number of created purposes. */
+    protected $purposecount = 0;
+
+    /**
+     * Reset process.
+     *
+     * Do not call directly.
+     *
+     * @return void
+     */
+    public function reset() {
+        $this->categorycount = 0;
+        $this->purposecount = 0;
+    }
+
+    /**
+     * Create a new category.
+     *
+     * @param array|stdClass $record
+     * @return category
+     */
+    public function create_category($record = null) {
+        $this->categorycount++;
+        $i = $this->categorycount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} description";
+        }
+
+        $category = api::create_category($record);
+
+        return $category;
+    }
+
+    /**
+     * Create a new purpose.
+     *
+     * @param array|stdClass $record
+     * @return purpose
+     */
+    public function create_purpose($record = null) {
+        $this->purposecount++;
+        $i = $this->purposecount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} $i description";
+        }
+
+        if (!isset($record->retentionperiod)) {
+            $record->retentionperiod = 'PT1M';
+        }
+
+        if (!isset($record->lawfulbases)) {
+            $record->lawfulbases = 'gdpr_art_6_1_a';
+        }
+
+        $purpose = api::create_purpose($record);
+
+        return $purpose;
+    }
+}
index ca666e3..e8cf60c 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018082100;
+$plugin->version   = 2018091000;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index e988304..4bb603a 100644 (file)
@@ -131,7 +131,7 @@ class item implements \renderable, \templatable {
         }
 
         // Allow the renderer to format the title and description.
-        $data['title']          = $output->format_title($title);
+        $data['title']          = strip_tags($output->format_title($title));
         $data['description']    = $this->showdescription ? $output->format_description($this->description) : null;
 
         return $data;
index 806646f..fe3ec49 100644 (file)
@@ -92,7 +92,8 @@ $currentmembersoptions = '';
 $currentmemberscount = 0;
 if ($currentmembers) {
     foreach($currentmembers as $group) {
-        $currentmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+        $currentmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+                format_string($group->name) . '</option>';
         $currentmemberscount ++;
     }
 
@@ -112,7 +113,8 @@ $potentialmembersoptions = '';
 $potentialmemberscount = 0;
 if ($potentialmembers) {
     foreach($potentialmembers as $group) {
-        $potentialmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+        $potentialmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+                format_string($group->name) . '</option>';
         $potentialmemberscount ++;
     }
 } else {
index 68e8063..968fbbd 100644 (file)
@@ -166,32 +166,33 @@ Feature: Course participants can be filtered
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Teacher 1" in the "participants" "table"
     # Search by idnumber (only).
-    Given I click on ".tag[data-value='student1@example.com']" "css_element"
-    When I set the field "Filters" to "SID"
+    And I click on "student1@example.com" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "SID"
     And I press key "13" in the field "Filters"
-    Then I should see "Student 1" in the "participants" "table"
+    And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
     And I should not see "Teacher 1" in the "participants" "table"
     # Search by city (only).
-    Given I click on ".tag[data-value='SID']" "css_element"
-    When I set the field "Filters" to "SCITY"
+    And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "SCITY"
     And I press key "13" in the field "Filters"
-    Then I should see "Student 1" in the "participants" "table"
+    And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
     And I should not see "Teacher 1" in the "participants" "table"
     # Search by country text (only) - should not match.
-    Given I click on ".tag[data-value='SCITY']" "css_element"
-    When I set the field "Filters" to "GB"
+    And I click on "SCITY" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "GB"
     And I press key "13" in the field "Filters"
-    Then I should see "Nothing to display"
+    And I should see "Nothing to display"
     # Check no match.
-    Given I set the field "Filters" to "NOTHING"
-    When I press key "13" in the field "Filters"
-    Then I should see "Nothing to display"
+    And I click on "GB" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "NOTHING"
+    And I press key "13" in the field "Filters"
+    And I should see "Nothing to display"
 
   @javascript
   Scenario: Filter by user identity fields when cannot see the field data
@@ -213,19 +214,19 @@ Feature: Course participants can be filtered
     And I should not see "Student 4" in the "participants" "table"
     And I should see "Teacher 1" in the "participants" "table"
     # Search for other fields - should only see own results.
-    Given I click on ".tag[data-value='@example.com']" "css_element"
-    When I set the field "Filters" to "SID"
+    And I click on "@example.com" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "SID"
     And I press key "13" in the field "Filters"
-    Then I should see "Nothing to display"
-    Given I click on ".tag[data-value='SID']" "css_element"
-    When I set the field "Filters" to "TID"
+    And I should see "Nothing to display"
+    And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
+    And I set the field "Filters" to "TID"
     And I press key "13" in the field "Filters"
-    Then I should see "Teacher 1" in the "participants" "table"
-    Given I set the field "Filters" to "CITY"
-    When I press key "13" in the field "Filters"
-    Then I should see "Teacher 1" in the "participants" "table"
+    And I should see "Teacher 1" in the "participants" "table"
+    And I set the field "Filters" to "CITY"
+    And I press key "13" in the field "Filters"
+    And I should see "Teacher 1" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
     # Check no match.
-    Given I set the field "Filters" to "NOTHING"
-    When I press key "13" in the field "Filters"
-    Then I should see "Nothing to display"
+    And I set the field "Filters" to "NOTHING"
+    And I press key "13" in the field "Filters"
+    And I should see "Nothing to display"