diff --git a/admin/tool/dataprivacy/amd/build/data_request_modal.min.js b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js index 7e9bda8ba609b..e8b22a22917f9 100644 --- a/admin/tool/dataprivacy/amd/build/data_request_modal.min.js +++ b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js @@ -1 +1 @@ -define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]'},i=function(a){d.call(this,a),this.getFooter().find(h.APPROVE_BUTTON).length||b.exception({message:"No approve button found"}),this.getFooter().find(h.DENY_BUTTON).length||b.exception({message:"No deny button found"})};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i}); \ No newline at end of file +define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]',COMPLETE_BUTTON:'[data-action="complete"]'},i=function(a){d.call(this,a)};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.COMPLETE_BUTTON,function(b,c){var d=a.Event(f.complete);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/events.min.js b/admin/tool/dataprivacy/amd/build/events.min.js index 1d4e97399b52f..0ecae4c42499e 100644 --- a/admin/tool/dataprivacy/amd/build/events.min.js +++ b/admin/tool/dataprivacy/amd/build/events.min.js @@ -1 +1 @@ -define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny"}}); \ No newline at end of file +define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny",complete:"tool_dataprivacy-data_request:complete"}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/requestactions.min.js b/admin/tool/dataprivacy/amd/build/requestactions.min.js index 586a2da288829..c405d17f21514 100644 --- a/admin/tool/dataprivacy/amd/build/requestactions.min.js +++ b/admin/tool/dataprivacy/amd/build/requestactions.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,g){var h=[],j="";switch(a){case i.approve:h=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],j="tool_dataprivacy_approve_data_request";break;case i.deny:h=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],j="tool_dataprivacy_deny_data_request"}var k="";d.get_strings(h).then(function(a){k=a[0];var b=a[1];return e.create({title:k,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){return a.setSaveButtonText(k),a.getRoot().on(f.save,function(){var a={requestid:g},d={methodname:j,args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a}).done(function(a){a.show()}).fail(c.exception)}var k={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]'},l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(k.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),l={requestid:k},m={methodname:"tool_dataprivacy_get_data_request",args:l},n=b.call([m]),o="",p=e.types.DEFAULT;a.when(n[0]).then(function(a){return a.result?(2==a.result.status&&(p=h.TYPE),o=a.result.typename,g.render("tool_dataprivacy/request_details",a.result)):(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){return e.create({title:o,body:a,type:p,large:!0}).then(function(a){return a.getRoot().on(i.approve,function(){j(i.approve,k)}),a.getRoot().on(i.deny,function(){j(i.deny,k)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(k.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(k.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)})},l}); \ No newline at end of file +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,b){var g=[],h="",j={requestid:b};switch(a){case i.approve:g=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],h="tool_dataprivacy_approve_data_request";break;case i.deny:g=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],h="tool_dataprivacy_deny_data_request";break;case i.complete:g=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}],h="tool_dataprivacy_mark_complete"}var l="";d.get_strings(g).then(function(a){l=a[0];var b=a[1];return e.create({title:l,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){a.setSaveButtonText(l),a.getRoot().on(f.save,function(){k(h,j)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}function k(a,d){var e={methodname:a,args:d};b.call([e])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}var l={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]',MARK_COMPLETE:'[data-action="complete"]'},m=function(){this.registerEvents()};return m.prototype.registerEvents=function(){a(l.VIEW_REQUEST).click(function(d){d.preventDefault();var l=a(this).data("requestid"),m={requestid:l},n={methodname:"tool_dataprivacy_get_data_request",args:m},o=b.call([n]);a.when(o[0]).then(function(a){return a.result?a.result:(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){var b=g.render("tool_dataprivacy/request_details",a),c={approvedeny:a.approvedeny,canmarkcomplete:a.canmarkcomplete};return e.create({title:a.typename,body:b,type:h.TYPE,large:!0,templateContext:c})}).then(function(a){a.getRoot().on(i.approve,function(){j(i.approve,l)}),a.getRoot().on(i.deny,function(){j(i.deny,l)}),a.getRoot().on(i.complete,function(){var a={requestid:l};k("tool_dataprivacy_mark_complete",a)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a.show()})["catch"](c.exception)}),a(l.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(l.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)}),a(l.MARK_COMPLETE).click(function(b){b.preventDefault(),j(i.complete,a(this).data("requestid"))})},m}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/src/data_request_modal.js b/admin/tool/dataprivacy/amd/src/data_request_modal.js index abf717b526b1d..5f841c5cfd451 100644 --- a/admin/tool/dataprivacy/amd/src/data_request_modal.js +++ b/admin/tool/dataprivacy/amd/src/data_request_modal.js @@ -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 diff --git a/admin/tool/dataprivacy/amd/src/events.js b/admin/tool/dataprivacy/amd/src/events.js index 9398dc45398a2..4e7ff77030a15 100644 --- a/admin/tool/dataprivacy/amd/src/events.js +++ b/admin/tool/dataprivacy/amd/src/events.js @@ -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' }; }); diff --git a/admin/tool/dataprivacy/amd/src/requestactions.js b/admin/tool/dataprivacy/amd/src/requestactions.js index c0941f8f93cb2..4f3c40607ba89 100644 --- a/admin/tool/dataprivacy/amd/src/requestactions.js +++ b/admin/tool/dataprivacy/amd/src/requestactions.js @@ -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); } diff --git a/admin/tool/dataprivacy/classes/api.php b/admin/tool/dataprivacy/classes/api.php index 42acb4259e74c..9aaea2865c239 100644 --- a/admin/tool/dataprivacy/classes/api.php +++ b/admin/tool/dataprivacy/classes/api.php @@ -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 @@ public static function update_request_status($requestid, $status, $dpoid = 0, $c 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 @@ public static function deny_data_request($requestid) { * @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) { diff --git a/admin/tool/dataprivacy/classes/external.php b/admin/tool/dataprivacy/classes/external.php index 6587c40b2624a..e14e07269008f 100644 --- a/admin/tool/dataprivacy/classes/external.php +++ b/admin/tool/dataprivacy/classes/external.php @@ -31,6 +31,7 @@ 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 @@ public static function contact_dpo_parameters() { } /** - * 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 @@ public static function contact_dpo($message) { } /** - * Parameter description for deny_data_request(). + * Parameter description for contact_dpo(). * * @since Moodle 3.5 * @return external_description @@ -222,6 +223,70 @@ public static function contact_dpo_returns() { ]); } + /** + * 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 @@ public static function get_data_request($requestid) { // 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 @@ public static function approve_data_request($requestid) { $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 @@ public static function deny_data_request($requestid) { $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, diff --git a/admin/tool/dataprivacy/classes/external/data_request_exporter.php b/admin/tool/dataprivacy/classes/external/data_request_exporter.php index 999634933170e..93b33e3af4749 100644 --- a/admin/tool/dataprivacy/classes/external/data_request_exporter.php +++ b/admin/tool/dataprivacy/classes/external/data_request_exporter.php @@ -102,6 +102,16 @@ protected static function define_other_properties() { 'optional' => true, 'default' => false ], + 'approvedeny' => [ + 'type' => PARAM_BOOL, + 'optional' => true, + 'default' => false + ], + 'canmarkcomplete' => [ + 'type' => PARAM_BOOL, + 'optional' => true, + 'default' => false + ], ]; } @@ -140,14 +150,19 @@ protected function get_other_values(renderer_base $output) { $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 @@ protected function get_other_values(renderer_base $output) { $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'; diff --git a/admin/tool/dataprivacy/classes/output/data_requests_table.php b/admin/tool/dataprivacy/classes/output/data_requests_table.php index 97918d9b86a4f..d8b0644a0db3d 100644 --- a/admin/tool/dataprivacy/classes/output/data_requests_table.php +++ b/admin/tool/dataprivacy/classes/output/data_requests_table.php @@ -180,16 +180,32 @@ public function col_actions($data) { $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); diff --git a/admin/tool/dataprivacy/classes/output/my_data_requests_page.php b/admin/tool/dataprivacy/classes/output/my_data_requests_page.php index 25229f008ac47..c5e18a135086d 100644 --- a/admin/tool/dataprivacy/classes/output/my_data_requests_page.php +++ b/admin/tool/dataprivacy/classes/output/my_data_requests_page.php @@ -82,6 +82,7 @@ public function export_for_template(renderer_base $output) { $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 @@ public function export_for_template(renderer_base $output) { $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: diff --git a/admin/tool/dataprivacy/db/services.php b/admin/tool/dataprivacy/db/services.php index b72b2010dc4a2..9c71e8c4dc2ff 100644 --- a/admin/tool/dataprivacy/db/services.php +++ b/admin/tool/dataprivacy/db/services.php @@ -43,6 +43,16 @@ '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', diff --git a/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php index aa640478bae1a..b01a0b6b0dc80 100644 --- a/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php +++ b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php @@ -46,6 +46,7 @@ $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['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['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 GDPR Art. 6.1'; +$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['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'; diff --git a/admin/tool/dataprivacy/templates/data_request_modal.mustache b/admin/tool/dataprivacy/templates/data_request_modal.mustache index cae5022f947c9..e5a1ff90a10f7 100644 --- a/admin/tool/dataprivacy/templates/data_request_modal.mustache +++ b/admin/tool/dataprivacy/templates/data_request_modal.mustache @@ -35,7 +35,12 @@ }} {{< core/modal }} {{$footer}} - - + {{#approvedeny}} + + + {{/approvedeny}} + {{#canmarkcomplete}} + + {{/canmarkcomplete}} {{/footer}} {{/ core/modal }} diff --git a/admin/tool/dataprivacy/version.php b/admin/tool/dataprivacy/version.php index 7b7bde4be6e9f..01b8b2b6a6bd5 100644 --- a/admin/tool/dataprivacy/version.php +++ b/admin/tool/dataprivacy/version.php @@ -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';