Merge branch 'MDL-71090' of git://github.com/paulholden/moodle
authorSara Arjona <sara@moodle.com>
Thu, 15 Apr 2021 09:56:40 +0000 (11:56 +0200)
committerSara Arjona <sara@moodle.com>
Thu, 15 Apr 2021 09:56:40 +0000 (11:56 +0200)
14 files changed:
admin/tool/dataprivacy/amd/build/contactdpo.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/contactdpo.min.js.map [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/myrequestactions.min.js
admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map
admin/tool/dataprivacy/amd/src/contactdpo.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/myrequestactions.js
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/form/contactdpo.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/templates/contact_dpo.mustache [deleted file]
admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature
admin/tool/dataprivacy/tests/behat/my_data_requests.feature [new file with mode: 0644]

diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js b/admin/tool/dataprivacy/amd/build/contactdpo.min.js
new file mode 100644 (file)
index 0000000..7351478
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map
new file mode 100644 (file)
index 0000000..6445af7
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map differ
index a6ba759..b1b7317 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js differ
index c9f78ed..4dfdc98 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map differ
diff --git a/admin/tool/dataprivacy/amd/src/contactdpo.js b/admin/tool/dataprivacy/amd/src/contactdpo.js
new file mode 100644 (file)
index 0000000..50417a8
--- /dev/null
@@ -0,0 +1,67 @@
+// 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/>.
+
+/**
+ * Javascript module for contacting the site DPO
+ *
+ * @module      tool_dataprivacy/contactdpo
+ * @package     tool_dataprivacy
+ * @copyright   2021 Paul Holden <paulh@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import ModalForm from 'core_form/modalform';
+import Notification from 'core/notification';
+import {get_string as getString} from 'core/str';
+import {add as addToast} from 'core/toast';
+
+const SELECTORS = {
+    CONTACT_DPO: '[data-action="contactdpo"]',
+};
+
+/**
+ * Initialize module
+ */
+export const init = () => {
+    const triggerElement = document.querySelector(SELECTORS.CONTACT_DPO);
+
+    triggerElement.addEventListener('click', event => {
+        event.preventDefault();
+
+        const modalForm = new ModalForm({
+            modalConfig: {
+                title: getString('contactdataprotectionofficer', 'tool_dataprivacy'),
+            },
+            formClass: 'tool_dataprivacy\\form\\contactdpo',
+            saveButtonText: getString('send', 'tool_dataprivacy'),
+            returnFocus: triggerElement,
+        });
+
+        // Show a toast notification when the form is submitted.
+        modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {
+            if (event.detail.result) {
+                getString('requestsubmitted', 'tool_dataprivacy').then(addToast).catch();
+            } else {
+                const warningMessages = event.detail.warnings.map(warning => warning.message);
+                Notification.addNotification({
+                    type: 'error',
+                    message: warningMessages.join('<br>')
+                });
+            }
+        });
+
+        modalForm.show();
+    });
+};
index 54f94fb..b75b386 100644 (file)
  * @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',
-    'core/pending'],
-function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
 
-    /**
-     * List of action selectors.
-     *
-     * @type {{CANCEL_REQUEST: string}}
-     * @type {{CONTACT_DPO: string}}
-     */
-    var ACTIONS = {
-        CANCEL_REQUEST: '[data-action="cancel"]',
-        CONTACT_DPO: '[data-action="contactdpo"]',
-    };
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
+import Pending from 'core/pending';
+import {get_strings as getStrings} from 'core/str';
 
-    /**
-     * MyRequestActions class.
-     */
-    var MyRequestActions = function() {
-        this.registerEvents();
-    };
+const SELECTORS = {
+    CANCEL_REQUEST: '[data-action="cancel"][data-requestid]',
+};
 
-    /**
-     * Register event listeners.
-     */
-    MyRequestActions.prototype.registerEvents = function() {
-        $(ACTIONS.CANCEL_REQUEST).click(function(e) {
-            e.preventDefault();
-
-            var requestId = $(this).data('requestid');
-            var stringkeys = [
-                {
-                    key: 'cancelrequest',
-                    component: 'tool_dataprivacy'
-                },
-                {
-                    key: 'cancelrequestconfirmation',
-                    component: 'tool_dataprivacy'
-                }
-            ];
-
-            Str.get_strings(stringkeys).then(function(langStrings) {
-                var title = langStrings[0];
-                var confirmMessage = langStrings[1];
-                return ModalFactory.create({
-                    title: title,
-                    body: confirmMessage,
-                    type: ModalFactory.types.SAVE_CANCEL
-                }).then(function(modal) {
-                    modal.setSaveButtonText(title);
-
-                    // Handle save event.
-                    modal.getRoot().on(ModalEvents.save, function() {
-                        // Cancel the request.
-                        var params = {
-                            'requestid': requestId
-                        };
-
-                        var request = {
-                            methodname: 'tool_dataprivacy_cancel_data_request',
-                            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);
-                    });
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-
-                    return modal;
-                });
-            }).done(function(modal) {
-                // Show the modal!
-                modal.show();
-
-            }).fail(Notification.exception);
-        });
-
-        $(ACTIONS.CONTACT_DPO).click(function(e) {
-            var pendingPromise = new Pending('dataprivacy/crud:initModal:contactdpo');
-            e.preventDefault();
+/**
+ * Initialize module
+ */
+export const init = () => {
+    document.addEventListener('click', event => {
+        const triggerElement = event.target.closest(SELECTORS.CANCEL_REQUEST);
+        if (triggerElement === null) {
+            return;
+        }
 
-            var replyToEmail = $(this).data('replytoemail');
+        event.preventDefault();
 
-            var keys = [
-                {
-                    key: 'contactdataprotectionofficer',
-                    component: 'tool_dataprivacy'
-                },
-                {
-                    key: 'send',
-                    component: 'tool_dataprivacy'
-                },
-            ];
+        const requiredStrings = [
+            {key: 'cancelrequest', component: 'tool_dataprivacy'},
+            {key: 'cancelrequestconfirmation', component: 'tool_dataprivacy'},
+        ];
 
-            var sendButtonText = '';
-            Str.get_strings(keys).then(function(langStrings) {
-                var modalTitle = langStrings[0];
-                sendButtonText = langStrings[1];
-                var context = {
-                    'replytoemail': replyToEmail
+        getStrings(requiredStrings).then(([cancelRequest, cancelConfirm]) => {
+            return Notification.confirm(cancelRequest, cancelConfirm, cancelRequest, null, () => {
+                const pendingPromise = new Pending('tool/dataprivacy:cancelRequest');
+                const request = {
+                    methodname: 'tool_dataprivacy_cancel_data_request',
+                    args: {requestid: triggerElement.dataset.requestid}
                 };
-                return ModalFactory.create({
-                    title: modalTitle,
-                    body: Templates.render('tool_dataprivacy/contact_dpo', context),
-                    type: ModalFactory.types.SAVE_CANCEL,
-                    large: true
-                });
-            }).then(function(modal) {
-                modal.setSaveButtonText(sendButtonText);
-
-                // Show the modal!
-                modal.show();
 
-                // Handle send event.
-                modal.getRoot().on(ModalEvents.save, function(e) {
-                    var message = $('#message').val().trim();
-                    if (message.length === 0) {
-                        e.preventDefault();
-                        // Show validation error when the message is empty.
-                        $('[data-region="messageinput"]').addClass('has-danger notifyproblem');
-                        $('#id_error_message').removeAttr('hidden');
+                Ajax.call([request])[0].then(response => {
+                    if (response.result) {
+                        window.location.reload();
                     } else {
-                        // Send the message.
-                        sendMessageToDPO(message);
+                        Notification.addNotification({
+                            type: 'error',
+                            message: response.warnings[0].message
+                        });
                     }
-                });
-
-                // Handle hidden event.
-                modal.getRoot().on(ModalEvents.hidden, function() {
-                    // Destroy when hidden.
-                    modal.destroy();
-                });
-
-                return;
-            }).then(pendingPromise.resolve)
-            .catch(Notification.exception);
-        });
-    };
-
-    /**
-     * Send message to the Data Protection Officer.
-     *
-     * @param {String} message The message to send.
-     */
-    function sendMessageToDPO(message) {
-        var request = {
-            methodname: 'tool_dataprivacy_contact_dpo',
-            args: {
-                message: message
-            }
-        };
-
-        var requestType = 'success';
-        Ajax.call([request])[0].then(function(data) {
-            if (data.result) {
-                return Str.get_string('requestsubmitted', 'tool_dataprivacy');
-            }
-            requestType = 'error';
-            return data.warnings.join('<br>');
-
-        }).done(function(message) {
-            Notification.addNotification({
-                message: message,
-                type: requestType
+                    return pendingPromise.resolve();
+                }).catch(Notification.exception);
             });
-
-        }).fail(Notification.exception);
-    }
-
-    return /** @alias module:tool_dataprivacy/myrequestactions */ {
-        // Public variables and functions.
-
-        /**
-         * Initialise the unified user filter.
-         *
-         * @method init
-         * @return {MyRequestActions}
-         */
-        'init': function() {
-            return new MyRequestActions();
-        }
-    };
-});
+        }).catch();
+    });
+};
index 92dd398..30d2e6d 100644 (file)
@@ -212,7 +212,8 @@ class external extends external_api {
                 $warnings[] = [
                     'item' => $dpo->id,
                     'warningcode' => 'errorsendingtodpo',
-                    'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy')
+                    'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy',
+                        fullname($dpo))
                 ];
             }
         }
diff --git a/admin/tool/dataprivacy/classes/form/contactdpo.php b/admin/tool/dataprivacy/classes/form/contactdpo.php
new file mode 100644 (file)
index 0000000..dc17291
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+
+
+namespace tool_dataprivacy\form;
+
+use context;
+use context_user;
+use moodle_exception;
+use moodle_url;
+use core_form\dynamic_form;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external;
+
+/**
+ * Contact DPO modal form
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2021 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contactdpo extends dynamic_form {
+
+    /**
+     * Form definition
+     */
+    protected function definition() {
+        global $USER;
+
+        $mform = $this->_form;
+
+        $mform->addElement('static', 'replyto', get_string('replyto', 'tool_dataprivacy'), s($USER->email));
+
+        $mform->addElement('textarea', 'message', get_string('message', 'tool_dataprivacy'), 'cols="60" rows="8"');
+        $mform->setType('message', PARAM_TEXT);
+        $mform->addRule('message', get_string('required'), 'required', null, 'client');
+    }
+
+    /**
+     * Return form context
+     *
+     * @return context
+     */
+    protected function get_context_for_dynamic_submission(): context {
+        global $USER;
+
+        return context_user::instance($USER->id);
+    }
+
+    /**
+     * Check if current user has access to this form, otherwise throw exception
+     *
+     * @throws moodle_exception
+     */
+    protected function check_access_for_dynamic_submission(): void {
+        if (!api::can_contact_dpo()) {
+            throw new moodle_exception('errorcontactdpodisabled', 'tool_dataprivacy');
+        }
+    }
+
+    /**
+     * Process the form submission, used if form was submitted via AJAX
+     *
+     * @return array
+     */
+    public function process_dynamic_submission() {
+        return external::contact_dpo($this->get_data()->message);
+    }
+
+    /**
+     * Load in existing data as form defaults (not applicable)
+     */
+    public function set_data_for_dynamic_submission(): void {
+        return;
+    }
+
+    /**
+     * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
+     *
+     * @return moodle_url
+     */
+    protected function get_page_url_for_dynamic_submission(): moodle_url {
+        global $USER;
+
+        return new moodle_url('/user/profile.php', ['id' => $USER->id]);
+    }
+}
index 8032906..5d3f4cb 100644 (file)
@@ -55,14 +55,11 @@ class renderer extends plugin_renderer_base {
     /**
      * Render the contact DPO link.
      *
-     * @param string $replytoemail The Reply-to email address
      * @return string The HTML for the link.
-     * @throws coding_exception
      */
-    public function render_contact_dpo_link($replytoemail) {
+    public function render_contact_dpo_link() {
         $params = [
             'data-action' => 'contactdpo',
-            'data-replytoemail' => $replytoemail,
         ];
         return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params);
     }
index 46d7af2..53ce860 100644 (file)
@@ -135,6 +135,7 @@ $string['effectiveretentionperioduser'] = '{$a} (since the last time the user ac
 $string['emailsalutation'] = 'Dear {$a},';
 $string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
 $string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
+$string['errorcontactdpodisabled'] = 'Contacting the privacy officer is disabled';
 $string['errorinvalidrequestcomments'] = 'The comments field may contain plain text only.';
 $string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
 $string['errorinvalidrequeststatus'] = 'Invalid request status!';
index 5b5f28a..25091fd 100644 (file)
@@ -54,10 +54,12 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
     // Contact data protection officer link.
     if (\tool_dataprivacy\api::can_contact_dpo() && $iscurrentuser) {
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
-        $content = $renderer->render_contact_dpo_link($USER->email);
+        $content = $renderer->render_contact_dpo_link();
         $node = new core_user\output\myprofile\node('privacyandpolicies', 'contactdpo', null, null, null, $content);
         $category->add_node($node);
-        $PAGE->requires->js_call_amd('tool_dataprivacy/myrequestactions', 'init');
+
+        // Require our Javascript module to handle contact DPO interaction.
+        $PAGE->requires->js_call_amd('tool_dataprivacy/contactdpo', 'init');
 
         $url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php');
         $node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests',
diff --git a/admin/tool/dataprivacy/templates/contact_dpo.mustache b/admin/tool/dataprivacy/templates/contact_dpo.mustache
deleted file mode 100644 (file)
index d3a0e86..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-{{!
-    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/contact_dpo
-
-    The purpose of this template is to enable the user to contact the site's DPO via email.
-
-    Classes required for JS:
-    * none
-
-    Data attributes required for JS:
-    * none
-
-    Context variables required for this template:
-    * userid int The user's ID.
-    * email string The user's email address.
-
-    Example context (json):
-    {
-        "userid": 1,
-        "replytoemail": "martha@example.com"
-    }
-}}
-<div class="container">
-    <div class="row mb-2">
-        <label class="col-md-3 col-form-label">{{#str}}replyto, tool_dataprivacy{{/str}}</label>
-        <div class="col-md-9 col-form-label">{{replytoemail}}</div>
-    </div>
-    <div class="row" data-region="messageinput">
-        <label for="message" class="col-md-3 col-form-label">
-            {{#str}}message, tool_dataprivacy{{/str}}
-            <span class="float-sm-right text-nowrap">
-            <abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>
-        </span>
-        </label>
-        <div class="col-md-9">
-            <textarea class="form-control" id="message" cols="60" rows="8"></textarea>
-            <div class="form-control-feedback" id="id_error_message" hidden="hidden">
-                {{#str}}required, moodle{{/str}}
-            </div>
-        </div>
-    </div>
-</div>
-
index 528d1f6..28f5de8 100644 (file)
@@ -8,19 +8,21 @@ Feature: Contact the privacy officer
     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"
+    Given the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+    When 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 click on "Send" "button" in the "Contact the privacy officer" "dialogue"
-    And I should see "Your request has been submitted to the privacy officer"
+    Then 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"
+
+  Scenario: Contacting the privacy officer when not enabled
+    When I log in as "student1"
+    And I follow "Profile" in the user menu
+    Then "Contact the privacy officer" "link" should not exist
diff --git a/admin/tool/dataprivacy/tests/behat/my_data_requests.feature b/admin/tool/dataprivacy/tests/behat/my_data_requests.feature
new file mode 100644 (file)
index 0000000..15de9cd
--- /dev/null
@@ -0,0 +1,26 @@
+@tool @tool_dataprivacy
+Feature: Manage my own data requests
+  In order to manage my own data requests
+  As a user
+  I need to be able to view and cancel all my data requests
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email          |
+      | student1 | Student   | 1        | s1@example.com |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+
+  @javascript
+  Scenario: Cancel my own data request
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I click on "Contact the privacy officer" "link"
+    And I set the field "Message" to "Hello DPO!"
+    And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
+    And I should see "Your request has been submitted to the privacy officer"
+    When I click on "Data requests" "link"
+    And I open the action menu in "Hello DPO!" "table_row"
+    And I choose "Cancel" in the open action menu
+    And I click on "Cancel request" "button" in the "Cancel request" "dialogue"
+    Then I should see "Cancelled" in the "Hello DPO!" "table_row"