--- /dev/null
+// 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();
+ });
+};
* @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();
+ });
+};
$warnings[] = [
'item' => $dpo->id,
'warningcode' => 'errorsendingtodpo',
- 'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy')
+ 'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy',
+ fullname($dpo))
];
}
}
--- /dev/null
+<?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]);
+ }
+}
/**
* 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);
}
$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!';
// 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',
+++ /dev/null
-{{!
- 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>
-
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
--- /dev/null
+@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"