Merge branch 'MDL-63116-master-1' of git://github.com/mihailges/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 18 Sep 2018 21:26:51 +0000 (23:26 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 18 Sep 2018 21:26:51 +0000 (23:26 +0200)
17 files changed:
admin/tool/dataprivacy/amd/build/events.min.js
admin/tool/dataprivacy/amd/build/requestactions.min.js
admin/tool/dataprivacy/amd/src/events.js
admin/tool/dataprivacy/amd/src/requestactions.js
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_data_requests.feature
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/version.php

index 0ecae4c..6c94ab3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/events.min.js and b/admin/tool/dataprivacy/amd/build/events.min.js differ
index c405d17..2260ee3 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/requestactions.min.js and b/admin/tool/dataprivacy/amd/build/requestactions.min.js differ
index 4e7ff77..cb1d9af 100644 (file)
@@ -25,7 +25,9 @@
 define([], function() {
     return {
         approve: 'tool_dataprivacy-data_request:approve',
+        bulkApprove: 'tool_dataprivacy-data_request:bulk_approve',
         deny: 'tool_dataprivacy-data_request:deny',
+        bulkDeny: 'tool_dataprivacy-data_request:bulk_deny',
         complete: 'tool_dataprivacy-data_request:complete'
     };
 });
index 4f3c406..37c4d92 100644 (file)
@@ -40,12 +40,38 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
      * @type {{DENY_REQUEST: string}}
      * @type {{VIEW_REQUEST: string}}
      * @type {{MARK_COMPLETE: string}}
+     * @type {{CHANGE_BULK_ACTION: string}}
+     * @type {{CONFIRM_BULK_ACTION: string}}
+     * @type {{SELECT_ALL: string}}
      */
     var ACTIONS = {
         APPROVE_REQUEST: '[data-action="approve"]',
         DENY_REQUEST: '[data-action="deny"]',
         VIEW_REQUEST: '[data-action="view"]',
-        MARK_COMPLETE: '[data-action="complete"]'
+        MARK_COMPLETE: '[data-action="complete"]',
+        CHANGE_BULK_ACTION: '[id="bulk-action"]',
+        CONFIRM_BULK_ACTION: '[id="confirm-bulk-action"]',
+        SELECT_ALL: '[data-action="selectall"]'
+    };
+
+    /**
+     * List of available bulk actions.
+     *
+     * @type {{APPROVE: number}}
+     * @type {{DENY: number}}
+     */
+    var BULK_ACTIONS = {
+        APPROVE: 1,
+        DENY: 2
+    };
+
+    /**
+     * List of selectors.
+     *
+     * @type {{SELECT_REQUEST: string}}
+     */
+    var SELECTORS = {
+        SELECT_REQUEST: '.selectrequests'
     };
 
     /**
@@ -103,12 +129,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             }).then(function(modal) {
                 // Handle approve event.
                 modal.getRoot().on(DataPrivacyEvents.approve, function() {
-                    showConfirmation(DataPrivacyEvents.approve, requestId);
+                    showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
                 });
 
                 // Handle deny event.
                 modal.getRoot().on(DataPrivacyEvents.deny, function() {
-                    showConfirmation(DataPrivacyEvents.deny, requestId);
+                    showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
                 });
 
                 // Handle send event.
@@ -137,34 +163,158 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             e.preventDefault();
 
             var requestId = $(this).data('requestid');
-            showConfirmation(DataPrivacyEvents.approve, requestId);
+            showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));
         });
 
         $(ACTIONS.DENY_REQUEST).click(function(e) {
             e.preventDefault();
 
             var requestId = $(this).data('requestid');
-            showConfirmation(DataPrivacyEvents.deny, requestId);
+            showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));
         });
 
         $(ACTIONS.MARK_COMPLETE).click(function(e) {
             e.preventDefault();
-            showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
+
+            var requestId = $(this).data('requestid');
+            showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId));
+        });
+
+        $(ACTIONS.CONFIRM_BULK_ACTION).click(function() {
+            var requestIds = [];
+            var actionEvent = '';
+            var wsdata = {};
+            var bulkActionKeys = [
+                {
+                    key: 'selectbulkaction',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'selectdatarequests',
+                    component: 'tool_dataprivacy'
+                },
+                {
+                    key: 'ok'
+                }
+            ];
+
+            var bulkaction = parseInt($('#bulk-action').val());
+
+            if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) {
+                Str.get_strings(bulkActionKeys).done(function(langStrings) {
+                    Notification.alert('', langStrings[0], langStrings[2]);
+                }).fail(Notification.exception);
+
+                return;
+            }
+
+            $(".selectrequests:checked").each(function() {
+                requestIds.push($(this).val());
+            });
+
+            if (requestIds.length < 1) {
+                Str.get_strings(bulkActionKeys).done(function(langStrings) {
+                    Notification.alert('', langStrings[1], langStrings[2]);
+                }).fail(Notification.exception);
+
+                return;
+            }
+
+            switch (bulkaction) {
+                case BULK_ACTIONS.APPROVE:
+                    actionEvent = DataPrivacyEvents.bulkApprove;
+                    wsdata = bulkApproveEventWsData(requestIds);
+                    break;
+                case BULK_ACTIONS.DENY:
+                    actionEvent = DataPrivacyEvents.bulkDeny;
+                    wsdata = bulkDenyEventWsData(requestIds);
+            }
+
+            showConfirmation(actionEvent, wsdata);
+        });
+
+        $(ACTIONS.SELECT_ALL).change(function(e) {
+            e.preventDefault();
+
+            var selectAll = $(this).is(':checked');
+            $(SELECTORS.SELECT_REQUEST).prop('checked', selectAll);
         });
     };
 
+    /**
+     * Return the webservice data for the approve request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function approveEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_approve_data_request',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
+    /**
+     * Return the webservice data for the bulk approve request action.
+     *
+     * @param {Array} requestIds The array of request ID's.
+     * @return {Object}
+     */
+    function bulkApproveEventWsData(requestIds) {
+        return {
+            'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests',
+            'wsparams': {'requestids': requestIds}
+        };
+    }
+
+    /**
+     * Return the webservice data for the deny request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function denyEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_deny_data_request',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
+    /**
+     * Return the webservice data for the bulk deny request action.
+     *
+     * @param {Array} requestIds The array of request ID's.
+     * @return {Object}
+     */
+    function bulkDenyEventWsData(requestIds) {
+        return {
+            'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests',
+            'wsparams': {'requestids': requestIds}
+        };
+    }
+
+    /**
+     * Return the webservice data for the complete request action.
+     *
+     * @param {Number} requestId The ID of the request.
+     * @return {Object}
+     */
+    function completeEventWsData(requestId) {
+        return {
+            'wsfunction': 'tool_dataprivacy_mark_complete',
+            'wsparams': {'requestid': requestId}
+        };
+    }
+
     /**
      * Show the confirmation dialogue.
      *
      * @param {String} action The action name.
-     * @param {Number} requestId The request ID.
+     * @param {Object} wsdata Object containing ws data.
      */
-    function showConfirmation(action, requestId) {
+    function showConfirmation(action, wsdata) {
         var keys = [];
-        var wsfunction = '';
-        var params = {
-            'requestid': requestId
-        };
+
         switch (action) {
             case DataPrivacyEvents.approve:
                 keys = [
@@ -177,7 +327,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_approve_data_request';
+                break;
+            case DataPrivacyEvents.bulkApprove:
+                keys = [
+                    {
+                        key: 'bulkapproverequests',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmbulkapproval',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
                 break;
             case DataPrivacyEvents.deny:
                 keys = [
@@ -190,7 +351,18 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_deny_data_request';
+                break;
+            case DataPrivacyEvents.bulkDeny:
+                keys = [
+                    {
+                        key: 'bulkdenyrequests',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmbulkdenial',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
                 break;
             case DataPrivacyEvents.complete:
                 keys = [
@@ -203,7 +375,6 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                         component: 'tool_dataprivacy'
                     }
                 ];
-                wsfunction = 'tool_dataprivacy_mark_complete';
                 break;
         }
 
@@ -221,7 +392,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
 
             // Handle save event.
             modal.getRoot().on(ModalEvents.save, function() {
-                handleSave(wsfunction, params);
+                handleSave(wsdata.wsfunction, wsdata.wsparams);
             });
 
             // Handle hidden event.
index 212f91a..7e5f350 100644 (file)
@@ -94,6 +94,12 @@ class api {
     /** Data delete request completed, account is removed. */
     const DATAREQUEST_STATUS_DELETED = 10;
 
+    /** Approve data request. */
+    const DATAREQUEST_ACTION_APPROVE = 1;
+
+    /** Reject data request. */
+    const DATAREQUEST_ACTION_REJECT = 2;
+
     /**
      * Determines whether the user can contact the site's Data Protection Officer via Moodle.
      *
index 692e072..c3f9f65 100644 (file)
@@ -421,6 +421,85 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameter description for bulk_approve_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_function_parameters
+     */
+    public static function bulk_approve_data_requests_parameters() {
+        return new external_function_parameters([
+            'requestids' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
+            )
+        ]);
+    }
+
+    /**
+     * Bulk approve bulk data request.
+     *
+     * @since Moodle 3.5
+     * @param array $requestids Array consisting the request ID's.
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws restricted_context_exception
+     * @throws moodle_exception
+     */
+    public static function bulk_approve_data_requests($requestids) {
+        $warnings = [];
+        $result = false;
+        $params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [
+            'requestids' => $requestids
+        ]);
+        $requestids = $params['requestids'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+        require_capability('tool/dataprivacy:managedatarequests', $context);
+
+        foreach ($requestids as $requestid) {
+            // Ensure the request exists.
+            $requestexists = data_request::record_exists($requestid);
+
+            if ($requestexists) {
+                api::approve_data_request($requestid);
+            } else {
+                $warnings[] = [
+                    'item' => $requestid,
+                    'warningcode' => 'errorrequestnotfound',
+                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
+                ];
+            }
+        }
+
+        if (empty($warnings)) {
+            $result = true;
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestsapproved', 'tool_dataprivacy'));
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for bulk_approve_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_description
+     */
+    public static function bulk_approve_data_requests_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Parameter description for deny_data_request().
      *
@@ -493,6 +572,85 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameter description for bulk_deny_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_function_parameters
+     */
+    public static function bulk_deny_data_requests_parameters() {
+        return new external_function_parameters([
+            'requestids' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
+            )
+        ]);
+    }
+
+    /**
+     * Bulk deny data requests.
+     *
+     * @since Moodle 3.5
+     * @param array $requestids Array consisting of request ID's.
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws restricted_context_exception
+     * @throws moodle_exception
+     */
+    public static function bulk_deny_data_requests($requestids) {
+        $warnings = [];
+        $result = false;
+        $params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [
+            'requestids' => $requestids
+        ]);
+        $requestids = $params['requestids'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+        require_capability('tool/dataprivacy:managedatarequests', $context);
+
+        foreach ($requestids as $requestid) {
+            // Ensure the request exists.
+            $requestexists = data_request::record_exists($requestid);
+
+            if ($requestexists) {
+                api::deny_data_request($requestid);
+            } else {
+                $warnings[] = [
+                    'item' => $requestid,
+                    'warningcode' => 'errorrequestnotfound',
+                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
+                ];
+            }
+        }
+
+        if (empty($warnings)) {
+            $result = true;
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestsdenied', 'tool_dataprivacy'));
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for bulk_deny_data_requests().
+     *
+     * @since Moodle 3.5
+     * @return external_description
+     */
+    public static function bulk_deny_data_requests_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Parameter description for get_data_request().
      *
index 36dd93a..2609ef1 100644 (file)
@@ -47,6 +47,9 @@ class helper {
     /** The request filters preference key. */
     const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
 
+    /** The number of data request records per page preference key. */
+    const PREF_REQUEST_PERPAGE = 'tool_dataprivacy_request-perpage';
+
     /**
      * Retrieves the human-readable text value of a data request type.
      *
index 7ea4bf8..402a22d 100644 (file)
@@ -86,7 +86,7 @@ class data_requests_page implements renderable, templatable {
         $data->filter = $filter->export_for_template($output);
 
         ob_start();
-        $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+        $this->table->out($this->table->get_requests_per_page(), true);
         $requests = ob_get_contents();
         ob_end_clean();
 
index 477e503..b6fa957 100644 (file)
@@ -62,6 +62,12 @@ class data_requests_table extends table_sql {
     /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
     protected $datarequests = [];
 
+    /** @var int The number of data request to be displayed per page. */
+    protected $perpage;
+
+    /** @var int[] The available options for the number of data request to be displayed per page. */
+    protected $perpageoptions = [25, 50, 100, 250];
+
     /**
      * data_requests_table constructor.
      *
@@ -79,7 +85,13 @@ class data_requests_table extends table_sql {
         $this->types = $types;
         $this->manage = $manage;
 
+        $checkboxattrs = [
+            'title' => get_string('selectall'),
+            'data-action' => 'selectall'
+        ];
+
         $columnheaders = [
+            'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs),
             'type' => get_string('requesttype', 'tool_dataprivacy'),
             'userid' => get_string('user', 'tool_dataprivacy'),
             'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
@@ -91,7 +103,26 @@ class data_requests_table extends table_sql {
 
         $this->define_columns(array_keys($columnheaders));
         $this->define_headers(array_values($columnheaders));
-        $this->no_sorting('actions');
+        $this->no_sorting('select', 'actions');
+    }
+
+    /**
+     * The select column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_select($data) {
+        if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            $stringdata = [
+                'username' => $data->foruser->fullname,
+                'requesttype' => \core_text::strtolower($data->typenameshort)
+            ];
+
+            return \html_writer::checkbox('requestids[]', $data->id, false, '',
+                    ['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest',
+                    'tool_dataprivacy', $stringdata)]);
+        }
     }
 
     /**
@@ -290,4 +321,72 @@ class data_requests_table extends table_sql {
     protected function show_hide_link($column, $index) {
         return '';
     }
+
+    /**
+     * Override the table's wrap_html_finish method in order to render the bulk actions and
+     * records per page options.
+     */
+    public function wrap_html_finish() {
+        global $OUTPUT;
+
+        $data = new stdClass();
+        $data->options = [
+            [
+                'value' => 0,
+                'name' => ''
+            ],
+            [
+                'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE,
+                'name' => get_string('approve', 'tool_dataprivacy')
+            ],
+            [
+                'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT,
+                'name' => get_string('deny', 'tool_dataprivacy')
+            ]
+        ];
+
+        $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions);
+        $perpageselect = new \single_select(new moodle_url(''), 'perpage',
+                $perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup');
+        $perpageselect->label = get_string('perpage', 'moodle');
+        $data->perpage = $OUTPUT->render($perpageselect);
+
+        echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data);
+    }
+
+    /**
+     * Set the number of data request records to be displayed per page.
+     *
+     * @param int $perpage The number of data request records.
+     */
+    public function set_requests_per_page(int $perpage) {
+        $this->perpage = $perpage;
+    }
+
+    /**
+     * Get the number of data request records to be displayed per page.
+     *
+     * @return int The number of data request records.
+     */
+    public function get_requests_per_page() : int {
+        return $this->perpage;
+    }
+
+    /**
+     * Set the available options for the number of data request to be displayed per page.
+     *
+     * @param array $perpageoptions The available options for the number of data request to be displayed per page.
+     */
+    public function set_requests_per_page_options(array $perpageoptions) {
+        $this->$perpageoptions = $perpageoptions;
+    }
+
+    /**
+     * Get the available options for the number of data request to be displayed per page.
+     *
+     * @return array The available options for the number of data request to be displayed per page.
+     */
+    public function get_requests_per_page_options() : array {
+        return $this->perpageoptions;
+    }
 }
index 4c2d8c4..4ddd411 100644 (file)
@@ -76,6 +76,8 @@ class provider implements
 
         $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
             'privacy:metadata:preference:tool_dataprivacy_request-filters');
+        $collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE,
+            'privacy:metadata:preference:tool_dataprivacy_request-perpage');
 
         return $collection;
     }
@@ -200,5 +202,11 @@ class provider implements
             $descriptionstext = implode(', ', $descriptions);
             writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
         }
+
+        $prefperpage = get_user_preferences(tool_helper::PREF_REQUEST_PERPAGE, null, $userid);
+        if ($prefperpage !== null) {
+            writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_PERPAGE, $prefperpage,
+                get_string('privacy:metadata:preference:tool_dataprivacy_request-perpage', 'tool_dataprivacy'));
+        }
     }
 }
index 6a8140d..8b2b16f 100644 (file)
@@ -27,6 +27,8 @@ require_once('lib.php');
 
 require_login(null, false);
 
+$perpage = optional_param('perpage', 0, PARAM_INT);
+
 $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
 
 $title = get_string('datarequests', 'tool_dataprivacy');
@@ -66,6 +68,13 @@ if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
     }
 
     $table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+    if (!empty($perpage)) {
+        set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE, $perpage);
+    } else {
+        $prefperpage = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE);
+        $perpage = ($prefperpage) ? $prefperpage : $table->get_requests_per_page_options()[0];
+    }
+    $table->set_requests_per_page($perpage);
     $table->baseurl = $url;
 
     $requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
index b9e6077..dac6ba4 100644 (file)
@@ -73,6 +73,16 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_bulk_approve_data_requests' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'bulk_approve_data_requests',
+        'classpath'     => '',
+        'description'   => 'Bulk approve data requests',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedatarequests',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
     'tool_dataprivacy_deny_data_request' => [
         'classname'     => 'tool_dataprivacy\external',
         'methodname'    => 'deny_data_request',
@@ -83,6 +93,16 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_bulk_deny_data_requests' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'bulk_deny_data_requests',
+        'classpath'     => '',
+        'description'   => 'Bulk deny data requests',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedatarequests',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
     'tool_dataprivacy_get_users' => [
         'classname'     => 'tool_dataprivacy\external',
         'methodname'    => 'get_users',
index 45bf5a6..7b8aa51 100644 (file)
@@ -32,6 +32,8 @@ $string['addnewdefaults'] = 'Add a new module default';
 $string['addpurpose'] = 'Add purpose';
 $string['approve'] = 'Approve';
 $string['approverequest'] = 'Approve request';
+$string['bulkapproverequests'] = 'Approve requests';
+$string['bulkdenyrequests'] = 'Deny requests';
 $string['cachedef_purpose'] = 'Data purposes';
 $string['cachedef_contextlevel'] = 'Context levels purpose and category';
 $string['cancelrequest'] = 'Cancel request';
@@ -47,9 +49,11 @@ $string['categoryupdated'] = 'Category updated';
 $string['close'] = 'Close';
 $string['compliant'] = 'Compliant';
 $string['confirmapproval'] = 'Do you really want to approve this data request?';
+$string['confirmbulkapproval'] = 'Do you really want to bulk approve the selected data requests?';
 $string['confirmcompletion'] = 'Do you really want to mark this user enquiry as complete?';
 $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
 $string['confirmdenial'] = 'Do you really want deny this data request?';
+$string['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?';
 $string['contactdataprotectionofficer'] = 'Contact the privacy officer';
 $string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
 $string['contextlevelname10'] = 'Site';
@@ -198,6 +202,7 @@ $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
 $string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content';
 $string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
+$string['privacy:metadata:preference:tool_dataprivacy_request-perpage'] = 'The number of data requests the user prefers to see on one page';
 $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
@@ -228,6 +233,8 @@ $string['requestemailintro'] = 'You have received a data request:';
 $string['requestfor'] = 'Requesting for';
 $string['requestmarkedcomplete'] = 'The request has been marked as complete';
 $string['requestorigin'] = 'Request origin';
+$string['requestsapproved'] = 'The requests have been approved';
+$string['requestsdenied'] = 'The requests have been denied';
 $string['requeststatus'] = 'Status';
 $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
@@ -248,6 +255,9 @@ $string['retentionperiod'] = 'Retention period';
 $string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
 $string['retentionperiodnotdefined'] = 'No retention period was defined';
 $string['retentionperiodzero'] = 'No retention period';
+$string['selectbulkaction'] = 'Please select a bulk action.';
+$string['selectdatarequests'] = 'Please select data requests.';
+$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
 $string['send'] = 'Send';
 $string['sensitivedatareasons'] = 'Sensitive personal data processing reasons';
 $string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see  <a href="https://gdpr-info.eu/art-9-gdpr/" target="_blank">GDPR Art. 9.2</a>';
diff --git a/admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache b/admin/tool/dataprivacy/templates/data_requests_bulk_actions.mustache
new file mode 100644 (file)
index 0000000..2ec0473
--- /dev/null
@@ -0,0 +1,59 @@
+{{!
+    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/data_requests_bulk_actions
+
+    Moodle template for the bulk action select element in the data requests page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * options - Array of options for the select with value and name.
+    * perpage - HTML content of the records per page select element.
+
+    Example context (json):
+    {
+        "options": [
+            {
+                "value": 1,
+                "name": "Approve"
+            },
+            {
+                "value": 2,
+                "name": "Deny"
+            }
+        ],
+        "perpage" : "<div class='singleselect'></div>"
+    }
+}}
+<div class="m-t-1 d-inline-block w-100">
+    <div class="pull-left">
+        <select id="bulk-action" class="select custom-select">
+        {{#options}}
+            <option value="{{ value }}">{{ name }}</option>
+        {{/options}}
+        </select>
+        <button class="btn btn-primary" id="confirm-bulk-action">{{# str}} confirm {{/ str}}</button>
+    </div>
+    <div class="pull-right">
+        {{{ perpage }}}
+    </div>
+</div>
index bdc43e7..9eb2a8a 100644 (file)
@@ -56,3 +56,93 @@ Feature: Manage data requests
     And I click on "Actions" "link" in the "Jane Doe" "table_row"
     And I should see "View the request"
     But I should not see "Mark as complete"
+
+  @javascript
+  Scenario: Bulk accepting requests
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment1"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment2"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I trigger cron
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Comment1" in the "John Doe" "table_row"
+    And I should see "Awaiting approval" in the "John Doe" "table_row"
+    And I should see "Comment2" in the "Jane Doe" "table_row"
+    And I should see "Awaiting approval" in the "Jane Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
+    And I set the field with xpath "//select[@id='bulk-action']" to "Approve"
+    And I press "Confirm"
+    And I should see "Approve requests"
+    And I should see "Do you really want to bulk approve the selected data requests?"
+    When I press "Approve requests"
+    Then I should see "Approved" in the "John Doe" "table_row"
+    And I should see "Approved" in the "Jane Doe" "table_row"
+
+  @javascript
+  Scenario: Bulk denying requests
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment1"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    And I should see "Data requests"
+    And I click on "Data requests" "link"
+    And I should see "New request"
+    And I click on "New request" "link"
+    And I should see "Type"
+    And I should see "Comments"
+    And I set the field "Type" to "Export all of my personal data"
+    And I set the field "Comments" to "Comment2"
+    And I press "Save changes"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    And I trigger cron
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I should see "Comment1" in the "John Doe" "table_row"
+    And I should see "Awaiting approval" in the "John Doe" "table_row"
+    And I should see "Comment2" in the "Jane Doe" "table_row"
+    And I should see "Awaiting approval" in the "Jane Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
+    And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
+    And I set the field with xpath "//select[@id='bulk-action']" to "Deny"
+    And I press "Confirm"
+    And I should see "Deny requests"
+    And I should see "Do you really want to bulk deny the selected data requests?"
+    When I press "Deny requests"
+    Then I should see "Rejected" in the "John Doe" "table_row"
+    And I should see "Rejected" in the "Jane Doe" "table_row"
index 5448d5f..b768786 100644 (file)
@@ -560,4 +560,106 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
             $this->assertNotContains($pluginwithdefaults, $options);
         }
     }
+
+    /**
+     * Test for external::bulk_approve_data_requests().
+     */
+    public function test_bulk_approve_data_requests() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertTrue($return->result);
+        $this->assertEmpty($return->warnings);
+    }
+
+    /**
+     * Test for external::bulk_approve_data_requests() for a non-existent request ID.
+     */
+    public function test_bulk_approve_data_requests_non_existent() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_approve_data_requests([$requestid1 + 1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertFalse($return->result);
+        $this->assertCount(1, $return->warnings);
+        $warning = reset($return->warnings);
+        $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
+        $this->assertEquals($requestid1 + 1, $warning['item']);
+    }
+
+    /**
+     * Test for external::bulk_deny_data_requests().
+     */
+    public function test_bulk_deny_data_requests() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertTrue($return->result);
+        $this->assertEmpty($return->warnings);
+    }
+
+    /**
+     * Test for external::bulk_deny_data_requests() for a non-existent request ID.
+     */
+    public function test_bulk_deny_data_requests_non_existent() {
+        $generator = new testing_data_generator();
+        $requester1 = $generator->create_user();
+        $comment1 = 'sample comment';
+        // Login as requester2.
+        $this->setUser($requester1->id);
+        // Create delete data request.
+        $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+
+        $requestid1 = $datarequest1->get('id');
+        $requestid2 = $this->requestid;
+
+        $this->setAdminUser();
+        api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+        $result = external::bulk_deny_data_requests([$requestid1 + 1, $requestid2]);
+        $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
+        $this->assertFalse($return->result);
+        $this->assertCount(1, $return->warnings);
+        $warning = reset($return->warnings);
+        $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
+        $this->assertEquals($requestid1 + 1, $warning['item']);
+    }
 }
index e8cf60c..a65e257 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018091000;
+$plugin->version   = 2018091100;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';