Merge branch 'MDL-62026-master' of git://github.com/junpataleta/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 11 Jul 2018 00:13:11 +0000 (08:13 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 11 Jul 2018 00:13:11 +0000 (08:13 +0800)
17 files changed:
admin/tool/dataprivacy/amd/build/data_request_modal.min.js
admin/tool/dataprivacy/amd/build/events.min.js
admin/tool/dataprivacy/amd/build/requestactions.min.js
admin/tool/dataprivacy/amd/src/data_request_modal.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/external/data_request_exporter.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/my_data_requests_page.php
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/data_request_modal.mustache
admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_data_requests.feature [new file with mode: 0644]
admin/tool/dataprivacy/version.php

index 7e9bda8..e8b22a2 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/data_request_modal.min.js and b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js differ
index 1d4e973..0ecae4c 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 586a2da..c405d17 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 abf717b..5f841c5 100644 (file)
@@ -29,6 +29,7 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
         var SELECTORS = {
             APPROVE_BUTTON: '[data-action="approve"]',
             DENY_BUTTON: '[data-action="deny"]',
+            COMPLETE_BUTTON: '[data-action="complete"]'
         };
 
         /**
@@ -38,14 +39,6 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
          */
         var ModalDataRequest = function(root) {
             Modal.call(this, root);
-
-            if (!this.getFooter().find(SELECTORS.APPROVE_BUTTON).length) {
-                Notification.exception({message: 'No approve button found'});
-            }
-
-            if (!this.getFooter().find(SELECTORS.DENY_BUTTON).length) {
-                Notification.exception({message: 'No deny button found'});
-            }
         };
 
         ModalDataRequest.TYPE = 'tool_dataprivacy-data_request';
@@ -80,6 +73,16 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
                     data.originalEvent.preventDefault();
                 }
             }.bind(this));
+
+            this.getModal().on(CustomEvents.events.activate, SELECTORS.COMPLETE_BUTTON, function(e, data) {
+                var completeEvent = $.Event(DataPrivacyEvents.complete);
+                this.getRoot().trigger(completeEvent, this);
+
+                if (!completeEvent.isDefaultPrevented()) {
+                    this.hide();
+                    data.originalEvent.preventDefault();
+                }
+            }.bind(this));
         };
 
         // Automatically register with the modal registry the first time this module is imported so that you can create modals
index 9398dc4..4e7ff77 100644 (file)
@@ -26,5 +26,6 @@ define([], function() {
     return {
         approve: 'tool_dataprivacy-data_request:approve',
         deny: 'tool_dataprivacy-data_request:deny',
+        complete: 'tool_dataprivacy-data_request:complete'
     };
 });
index c0941f8..4f3c406 100644 (file)
@@ -39,11 +39,13 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
      * @type {{APPROVE_REQUEST: string}}
      * @type {{DENY_REQUEST: string}}
      * @type {{VIEW_REQUEST: string}}
+     * @type {{MARK_COMPLETE: string}}
      */
     var ACTIONS = {
         APPROVE_REQUEST: '[data-action="approve"]',
         DENY_REQUEST: '[data-action="deny"]',
-        VIEW_REQUEST: '[data-action="view"]'
+        VIEW_REQUEST: '[data-action="view"]',
+        MARK_COMPLETE: '[data-action="complete"]'
     };
 
     /**
@@ -73,16 +75,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             };
 
             var promises = Ajax.call([request]);
-            var modalTitle = '';
-            var modalType = ModalFactory.types.DEFAULT;
             $.when(promises[0]).then(function(data) {
                 if (data.result) {
-                    // Check if the status is awaiting approval.
-                    if (data.result.status == 2) {
-                        modalType = ModalDataRequest.TYPE;
-                    }
-                    modalTitle = data.result.typename;
-                    return Templates.render('tool_dataprivacy/request_details', data.result);
+                    return data.result;
                 }
                 // Fail.
                 Notification.addNotification({
@@ -91,35 +86,51 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                 });
                 return false;
 
-            }).then(function(html) {
+            }).then(function(data) {
+                var body = Templates.render('tool_dataprivacy/request_details', data);
+                var templateContext = {
+                    approvedeny: data.approvedeny,
+                    canmarkcomplete: data.canmarkcomplete
+                };
                 return ModalFactory.create({
-                    title: modalTitle,
-                    body: html,
-                    type: modalType,
-                    large: true
-                }).then(function(modal) {
-                    // Handle approve event.
-                    modal.getRoot().on(DataPrivacyEvents.approve, function() {
-                        showConfirmation(DataPrivacyEvents.approve, requestId);
-                    });
-
-                    // Handle deny event.
-                    modal.getRoot().on(DataPrivacyEvents.deny, function() {
-                        showConfirmation(DataPrivacyEvents.deny, requestId);
-                    });
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-
-                    return modal;
+                    title: data.typename,
+                    body: body,
+                    type: ModalDataRequest.TYPE,
+                    large: true,
+                    templateContext: templateContext
+                });
+
+            }).then(function(modal) {
+                // Handle approve event.
+                modal.getRoot().on(DataPrivacyEvents.approve, function() {
+                    showConfirmation(DataPrivacyEvents.approve, requestId);
+                });
+
+                // Handle deny event.
+                modal.getRoot().on(DataPrivacyEvents.deny, function() {
+                    showConfirmation(DataPrivacyEvents.deny, requestId);
                 });
-            }).done(function(modal) {
+
+                // Handle send event.
+                modal.getRoot().on(DataPrivacyEvents.complete, function() {
+                    var params = {
+                        'requestid': requestId
+                    };
+                    handleSave('tool_dataprivacy_mark_complete', params);
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
                 // Show the modal!
                 modal.show();
-            }).fail(Notification.exception);
+
+                return;
+
+            }).catch(Notification.exception);
         });
 
         $(ACTIONS.APPROVE_REQUEST).click(function(e) {
@@ -135,6 +146,11 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
             var requestId = $(this).data('requestid');
             showConfirmation(DataPrivacyEvents.deny, requestId);
         });
+
+        $(ACTIONS.MARK_COMPLETE).click(function(e) {
+            e.preventDefault();
+            showConfirmation(DataPrivacyEvents.complete, $(this).data('requestid'));
+        });
     };
 
     /**
@@ -146,6 +162,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
     function showConfirmation(action, requestId) {
         var keys = [];
         var wsfunction = '';
+        var params = {
+            'requestid': requestId
+        };
         switch (action) {
             case DataPrivacyEvents.approve:
                 keys = [
@@ -173,6 +192,19 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                 ];
                 wsfunction = 'tool_dataprivacy_deny_data_request';
                 break;
+            case DataPrivacyEvents.complete:
+                keys = [
+                    {
+                        key: 'markcomplete',
+                        component: 'tool_dataprivacy'
+                    },
+                    {
+                        key: 'confirmcompletion',
+                        component: 'tool_dataprivacy'
+                    }
+                ];
+                wsfunction = 'tool_dataprivacy_mark_complete';
+                break;
         }
 
         var modalTitle = '';
@@ -189,26 +221,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
 
             // Handle save event.
             modal.getRoot().on(ModalEvents.save, function() {
-                // Confirm the request.
-                var params = {
-                    'requestid': requestId
-                };
-
-                var request = {
-                    methodname: wsfunction,
-                    args: params
-                };
-
-                Ajax.call([request])[0].done(function(data) {
-                    if (data.result) {
-                        window.location.reload();
-                    } else {
-                        Notification.addNotification({
-                            message: data.warnings[0].message,
-                            type: 'error'
-                        });
-                    }
-                }).fail(Notification.exception);
+                handleSave(wsfunction, params);
             });
 
             // Handle hidden event.
@@ -217,9 +230,39 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Modal
                 modal.destroy();
             });
 
-            return modal;
-        }).done(function(modal) {
             modal.show();
+
+            return;
+
+        }).catch(Notification.exception);
+    }
+
+    /**
+     * Calls a web service function and reloads the page on success and shows a notification.
+     * Displays an error notification, otherwise.
+     *
+     * @param {String} wsfunction The web service function to call.
+     * @param {Object} params The parameters for the web service functoon.
+     */
+    function handleSave(wsfunction, params) {
+        // Confirm the request.
+        var request = {
+            methodname: wsfunction,
+            args: params
+        };
+
+        Ajax.call([request])[0].done(function(data) {
+            if (data.result) {
+                // On success, reload the page so that the data request table will be updated.
+                // TODO: Probably in the future, better to reload the table or the target data request via AJAX.
+                window.location.reload();
+            } else {
+                // Add the notification.
+                Notification.addNotification({
+                    message: data.warnings[0].message,
+                    type: 'error'
+                });
+            }
         }).fail(Notification.exception);
     }
 
index 42acb42..9aaea28 100644 (file)
@@ -24,6 +24,7 @@
 namespace tool_dataprivacy;
 
 use coding_exception;
+use context_course;
 use context_system;
 use core\invalid_persistent_exception;
 use core\message\message;
@@ -426,7 +427,22 @@ class api {
         if ($dpoid) {
             $datarequest->set('dpo', $dpoid);
         }
-        $datarequest->set('dpocomment', $comment);
+        // Update the comment if necessary.
+        if (!empty(trim($comment))) {
+            $params = [
+                'date' => userdate(time()),
+                'comment' => $comment
+            ];
+            $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
+            // Check if there's an existing DPO comment.
+            $currentcomment = trim($datarequest->get('dpocomment'));
+            if ($currentcomment) {
+                // Append the new comment to the current comment and give them 1 line space in between.
+                $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
+            }
+            $datarequest->set('dpocomment', $commenttosave);
+        }
+
         return $datarequest->update();
     }
 
@@ -521,7 +537,6 @@ class api {
      * @param data_request $request The data request
      * @return int|false
      * @throws coding_exception
-     * @throws dml_exception
      * @throws moodle_exception
      */
     public static function notify_dpo($dpo, data_request $request) {
index 6587c40..e14e072 100644 (file)
@@ -31,6 +31,7 @@ use context_helper;
 use context_system;
 use context_user;
 use core\invalid_persistent_exception;
+use core\notification;
 use core_user;
 use dml_exception;
 use external_api;
@@ -144,7 +145,7 @@ class external extends external_api {
     }
 
     /**
-     * Deny a data request.
+     * Make a general enquiry to a DPO.
      *
      * @since Moodle 3.5
      * @param string $message The message to be sent to the DPO.
@@ -210,7 +211,7 @@ class external extends external_api {
     }
 
     /**
-     * Parameter description for deny_data_request().
+     * Parameter description for contact_dpo().
      *
      * @since Moodle 3.5
      * @return external_description
@@ -222,6 +223,70 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameter description for mark_complete().
+     *
+     * @since Moodle 3.5.2
+     * @return external_function_parameters
+     */
+    public static function mark_complete_parameters() {
+        return new external_function_parameters([
+            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
+        ]);
+    }
+
+    /**
+     * Mark a user's general enquiry's status as complete.
+     *
+     * @since Moodle 3.5.2
+     * @param int $requestid The request ID of the general enquiry.
+     * @return array
+     * @throws coding_exception
+     * @throws invalid_parameter_exception
+     * @throws invalid_persistent_exception
+     * @throws restricted_context_exception
+     * @throws dml_exception
+     * @throws moodle_exception
+     */
+    public static function mark_complete($requestid) {
+        global $USER;
+
+        $warnings = [];
+        $params = external_api::validate_parameters(self::mark_complete_parameters(), [
+            'requestid' => $requestid,
+        ]);
+        $requestid = $params['requestid'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $message = get_string('markedcomplete', 'tool_dataprivacy');
+        // Update the data request record.
+        if ($result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE, $USER->id, $message)) {
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestmarkedcomplete', 'tool_dataprivacy'));
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for mark_complete().
+     *
+     * @since Moodle 3.5.2
+     * @return external_description
+     */
+    public static function mark_complete_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().
      *
@@ -258,9 +323,9 @@ class external extends external_api {
         // Validate context.
         $context = context_system::instance();
         self::validate_context($context);
+        $requestpersistent = new data_request($requestid);
         require_capability('tool/dataprivacy:managedatarequests', $context);
 
-        $requestpersistent = new data_request($requestid);
         $exporter = new data_request_exporter($requestpersistent, ['context' => $context]);
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
         $result = $exporter->export($renderer);
@@ -326,6 +391,9 @@ class external extends external_api {
         $result = false;
         if ($requestexists) {
             $result = api::approve_data_request($requestid);
+
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestapproved', 'tool_dataprivacy'));
         } else {
             $warnings[] = [
                 'item' => $requestid,
@@ -395,6 +463,9 @@ class external extends external_api {
         $result = false;
         if ($requestexists) {
             $result = api::deny_data_request($requestid);
+
+            // Add notification in the session to be shown when the page is reloaded on the JS side.
+            notification::success(get_string('requestdenied', 'tool_dataprivacy'));
         } else {
             $warnings[] = [
                 'item' => $requestid,
index 9996349..93b33e3 100644 (file)
@@ -102,6 +102,16 @@ class data_request_exporter extends persistent_exporter {
                 'optional' => true,
                 'default' => false
             ],
+            'approvedeny' => [
+                'type' => PARAM_BOOL,
+                'optional' => true,
+                'default' => false
+            ],
+            'canmarkcomplete' => [
+                'type' => PARAM_BOOL,
+                'optional' => true,
+                'default' => false
+            ],
         ];
     }
 
@@ -140,14 +150,19 @@ class data_request_exporter extends persistent_exporter {
 
         $values['messagehtml'] = text_to_html($this->persistent->get('comments'));
 
-        $values['typename'] = helper::get_request_type_string($this->persistent->get('type'));
-        $values['typenameshort'] = helper::get_shortened_request_type_string($this->persistent->get('type'));
+        $requesttype = $this->persistent->get('type');
+        $values['typename'] = helper::get_request_type_string($requesttype);
+        $values['typenameshort'] = helper::get_shortened_request_type_string($requesttype);
 
         $values['canreview'] = false;
+        $values['approvedeny'] = false;
         $values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status'));
+
         switch ($this->persistent->get('status')) {
             case api::DATAREQUEST_STATUS_PENDING:
                 $values['statuslabelclass'] = 'label-default';
+                // Request can be manually completed for general enquiry requests.
+                $values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS;
                 break;
             case api::DATAREQUEST_STATUS_PREPROCESSING:
                 $values['statuslabelclass'] = 'label-default';
@@ -156,6 +171,8 @@ class data_request_exporter extends persistent_exporter {
                 $values['statuslabelclass'] = 'label-info';
                 // DPO can review the request once it's ready.
                 $values['canreview'] = true;
+                // Whether the DPO can approve or deny the request.
+                $values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]);
                 break;
             case api::DATAREQUEST_STATUS_APPROVED:
                 $values['statuslabelclass'] = 'label-info';
index 97918d9..d8b0644 100644 (file)
@@ -180,16 +180,32 @@ class data_requests_table extends table_sql {
         $actiontext = get_string('viewrequest', 'tool_dataprivacy');
         $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
 
-        if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
-            // Approve.
-            $actiondata['data-action'] = 'approve';
-            $actiontext = get_string('approverequest', 'tool_dataprivacy');
-            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
-
-            // Deny.
-            $actiondata['data-action'] = 'deny';
-            $actiontext = get_string('denyrequest', 'tool_dataprivacy');
-            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        switch ($status) {
+            case api::DATAREQUEST_STATUS_PENDING:
+                // Add action to mark a general enquiry request as complete.
+                if ($data->type == api::DATAREQUEST_TYPE_OTHERS) {
+                    $actiondata['data-action'] = 'complete';
+                    $nameemail = (object)[
+                        'name' => $data->foruser->fullname,
+                        'email' => $data->foruser->email
+                    ];
+                    $actiondata['data-requestid'] = $data->id;
+                    $actiondata['data-replytoemail'] = get_string('nameemail', 'tool_dataprivacy', $nameemail);
+                    $actiontext = get_string('markcomplete', 'tool_dataprivacy');
+                    $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+                }
+                break;
+            case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
+                // Approve.
+                $actiondata['data-action'] = 'approve';
+                $actiontext = get_string('approverequest', 'tool_dataprivacy');
+                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+                // Deny.
+                $actiondata['data-action'] = 'deny';
+                $actiontext = get_string('denyrequest', 'tool_dataprivacy');
+                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+                break;
         }
 
         $actionsmenu = new action_menu($actions);
index 25229f0..c5e18a1 100644 (file)
@@ -82,6 +82,7 @@ class my_data_requests_page implements renderable, templatable {
             $requestid = $request->get('id');
             $status = $request->get('status');
             $userid = $request->get('userid');
+            $type = $request->get('type');
 
             $usercontext = context_user::instance($userid, IGNORE_MISSING);
             if (!$usercontext) {
@@ -107,7 +108,8 @@ class my_data_requests_page implements renderable, templatable {
                     $item->statuslabelclass = 'label-success';
                     $item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy');
                     $cancancel = false;
-                    $candownload = true;
+                    // Show download links only for export-type data requests.
+                    $candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
                     break;
                 case api::DATAREQUEST_STATUS_CANCELLED:
                 case api::DATAREQUEST_STATUS_REJECTED:
index b72b201..9c71e8c 100644 (file)
@@ -43,6 +43,16 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_mark_complete' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'mark_complete',
+        'classpath'     => '',
+        'description'   => 'Mark a user\'s general enquiry as complete',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedatarequests',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
     'tool_dataprivacy_get_data_request' => [
         'classname'     => 'tool_dataprivacy\external',
         'methodname'    => 'get_data_request',
index aa64047..b01a0b6 100644 (file)
@@ -46,6 +46,7 @@ $string['categoryupdated'] = 'Category updated';
 $string['close'] = 'Close';
 $string['compliant'] = 'Compliant';
 $string['confirmapproval'] = 'Do you really want to approve this data request?';
+$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['contactdataprotectionofficer'] = 'Contact the privacy officer';
@@ -70,6 +71,7 @@ $string['dataregistryinfo'] = 'The data registry enables categories (types of da
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
 $string['datarequestemailsubject'] = 'Data request: {$a}';
 $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.';
@@ -151,6 +153,8 @@ $string['httpwarning'] = 'Any data downloaded from this site may not be encrypte
 $string['inherit'] = 'Inherit';
 $string['lawfulbases'] = 'Lawful bases';
 $string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see <a href="https://gdpr-info.eu/art-6-gdpr/" target="_blank">GDPR Art. 6.1</a>';
+$string['markcomplete'] = 'Mark as complete';
+$string['markedcomplete'] = 'Your enquiry has been marked as complete by the privacy officer.';
 $string['messageprovider:contactdataprotectionofficer'] = 'Data requests';
 $string['messageprovider:datarequestprocessingresults'] = 'Data request processing results';
 $string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications';
@@ -197,12 +201,15 @@ $string['purposeslist'] = 'List of data purposes';
 $string['purposeupdated'] = 'Purpose updated';
 $string['replyto'] = 'Reply to';
 $string['requestactions'] = 'Actions';
+$string['requestapproved'] = 'The request has been approved';
 $string['requestby'] = 'Requested by';
 $string['requestbydetail'] = 'Requested by:';
 $string['requestcomments'] = 'Comments';
 $string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
+$string['requestdenied'] = 'The request has been denied';
 $string['requestemailintro'] = 'You have received a data request:';
 $string['requestfor'] = 'Requesting for';
+$string['requestmarkedcomplete'] = 'The request has been marked as complete';
 $string['requeststatus'] = 'Status';
 $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
index cae5022..e5a1ff9 100644 (file)
 }}
 {{< core/modal }}
     {{$footer}}
-        <button type="button" class="btn btn-primary" data-action="approve">{{#str}} approve, tool_dataprivacy {{/str}}</button>
-        <button type="button" class="btn btn-secondary" data-action="deny">{{#str}} deny, tool_dataprivacy {{/str}}</button>
+        {{#approvedeny}}
+            <button type="button" class="btn btn-primary" data-action="approve">{{#str}} approve, tool_dataprivacy {{/str}}</button>
+            <button type="button" class="btn btn-secondary" data-action="deny">{{#str}} deny, tool_dataprivacy {{/str}}</button>
+        {{/approvedeny}}
+        {{#canmarkcomplete}}
+            <button type="button" class="btn btn-primary" data-action="complete">{{#str}} markcomplete, tool_dataprivacy {{/str}}</button>
+        {{/canmarkcomplete}}
     {{/footer}}
 {{/ core/modal }}
diff --git a/admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature b/admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature
new file mode 100644 (file)
index 0000000..e015d19
--- /dev/null
@@ -0,0 +1,26 @@
+@tool @tool_dataprivacy
+Feature: Contact the privacy officer
+  As a user
+  In order to reach out to the site's privacy officer
+  I need to be able to contact the site's privacy officer in Moodle
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email          |
+      | student1 | Student   | 1        | s1@example.com |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | contactdataprotectionofficer | 1 |
+    And I log out
+
+  @javascript
+  Scenario: Contacting the privacy officer
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Contact the privacy officer"
+    And I click on "Contact the privacy officer" "link"
+    And I set the field "Message" to "Hello DPO!"
+    And I press "Send"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I click on "Data requests" "link"
+    And I should see "Hello DPO!" in the "General inquiry" "table_row"
diff --git a/admin/tool/dataprivacy/tests/behat/manage_data_requests.feature b/admin/tool/dataprivacy/tests/behat/manage_data_requests.feature
new file mode 100644 (file)
index 0000000..bdc43e7
--- /dev/null
@@ -0,0 +1,58 @@
+@tool @tool_dataprivacy
+Feature: Manage data requests
+  As the privacy officer
+  In order to address the privacy-related requests
+  I need to be able to manage the data requests of the site's users
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email          |
+      | student1 | John      | Doe      | s1@example.com |
+      | student2 | Jane      | Doe      | s2@example.com |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | contactdataprotectionofficer | 1 |
+    And I log out
+
+  @javascript
+  Scenario: Marking general enquiries as complete
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I should see "Contact the privacy officer"
+    And I click on "Contact the privacy officer" "link"
+    And I set the field "Message" to "Hi PO! Can others access my information on your site?"
+    And I press "Send"
+    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 click on "Contact the privacy officer" "link"
+    And I set the field "Message" to "Dear Mr. Privacy Officer, I'd like to know more about GDPR. Thanks!"
+    And I press "Send"
+    And I should see "Your request has been submitted to the privacy officer"
+    And I log out
+    When I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    Then I should see "Hi PO!" in the "John Doe" "table_row"
+    And I should see "Dear Mr. Privacy Officer" in the "Jane Doe" "table_row"
+    And I click on "Actions" "link" in the "John Doe" "table_row"
+    And I should see "View the request"
+    And I should see "Mark as complete"
+    And I choose "View the request" in the open action menu
+    And I should see "Hi PO! Can others access my information on your site?"
+    And I press "Mark as complete"
+    And I wait until the page is ready
+    And I should see "Complete" in the "John Doe" "table_row"
+    And I click on "Actions" "link" in the "John Doe" "table_row"
+    And I should see "View the request"
+    But I should not see "Mark as complete"
+    And I press key "27" in ".moodle-actionmenu" "css_element"
+    And I click on "Actions" "link" in the "Jane Doe" "table_row"
+    And I choose "Mark as complete" in the open action menu
+    And I should see "Do you really want to mark this user enquiry as complete?"
+    And I press "Mark as complete"
+    And I wait until the page is ready
+    And I should see "Complete" in the "Jane Doe" "table_row"
+    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"
index 7b7bde4..01b8b2b 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018051401;
+$plugin->version   = 2018051402;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';