1
0
mirror of https://github.com/moodle/moodle.git synced 2025-04-07 09:23:31 +02:00

Merge branch 'MDL-62211-master' of git://github.com/junpataleta/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2018-07-03 17:29:38 +02:00 committed by David Monllao
commit af0202c5c2
14 changed files with 954 additions and 254 deletions

@ -0,0 +1 @@
define(["jquery","core/form-autocomplete","core/str","core/notification"],function(a,b,c,d){var e={REQUEST_FILTERS:"#request-filters"},f=function(){var f=[{key:"filter",component:"moodle"},{key:"nofiltersapplied",component:"moodle"}];c.get_strings(f).then(function(a){var c=a[0],d=a[1];return b.enhance(e.REQUEST_FILTERS,!1,"",c,!1,!0,d,!0)}).fail(d.exception);var g=a(e.REQUEST_FILTERS).val();a(e.REQUEST_FILTERS).on("change",function(){var b=a(this).val();g.join(",")!==b.join(",")&&(0===b.length&&a("#filters-cleared").val(1),a(this.form).submit())})};return{init:function(){f()}}});

@ -0,0 +1,84 @@
// 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/>.
/**
* JS module for the data requests filter.
*
* @module tool_dataprivacy/request_filter
* @package tool_dataprivacy
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
/**
* Selectors.
*
* @access private
* @type {{REQUEST_FILTERS: string}}
*/
var SELECTORS = {
REQUEST_FILTERS: '#request-filters'
};
/**
* Init function.
*
* @method init
* @private
*/
var init = function() {
var stringkeys = [
{
key: 'filter',
component: 'moodle'
},
{
key: 'nofiltersapplied',
component: 'moodle'
}
];
Str.get_strings(stringkeys).then(function(langstrings) {
var placeholder = langstrings[0];
var noSelectionString = langstrings[1];
return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
}).fail(Notification.exception);
var last = $(SELECTORS.REQUEST_FILTERS).val();
$(SELECTORS.REQUEST_FILTERS).on('change', function() {
var current = $(this).val();
// Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
if (last.join(',') !== current.join(',')) {
// If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
if (current.length === 0) {
$('#filters-cleared').val(1);
}
$(this.form).submit();
}
});
};
return /** @alias module:core/form-autocomplete */ {
/**
* Initialise the unified user filter.
*
* @method init
*/
init: function() {
init();
}
};
});

@ -232,16 +232,42 @@ class api {
* (e.g. Users with the Data Protection Officer roles)
*
* @param int $userid The User ID.
* @param int[] $statuses The status filters.
* @param int[] $types The request type filters.
* @param string $sort The order by clause.
* @param int $offset Amount of records to skip.
* @param int $limit Amount of records to fetch.
* @return data_request[]
* @throws coding_exception
* @throws dml_exception
*/
public static function get_data_requests($userid = 0) {
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
global $DB, $USER;
$results = [];
$sort = 'status ASC, timemodified ASC';
$sqlparams = [];
$sqlconditions = [];
// Set default sort.
if (empty($sort)) {
$sort = 'status ASC, timemodified ASC';
}
// Set status filters.
if (!empty($statuses)) {
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
$sqlconditions[] = "status $statusinsql";
}
// Set request type filter.
if (!empty($types)) {
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
$sqlconditions[] = "type $typeinsql";
$sqlparams = array_merge($sqlparams, $typeparams);
}
if ($userid) {
// Get the data requests for the user or data requests made by the user.
$select = "(userid = :userid OR requestedby = :requestedby)";
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
$params = [
'userid' => $userid,
'requestedby' => $userid
@ -256,20 +282,87 @@ class api {
$alloweduserids = array_merge($alloweduserids, array_keys($children));
}
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
$select .= " AND userid $insql";
$params = array_merge($params, $inparams);
$sqlconditions[] .= "userid $insql";
$select = implode(' AND ', $sqlconditions);
$params = array_merge($params, $inparams, $sqlparams);
$results = data_request::get_records_select($select, $params, $sort);
$results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
} else {
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
if (self::is_site_dpo($USER->id)) {
$results = data_request::get_records(null, $sort, '');
if (!empty($sqlconditions)) {
$select = implode(' AND ', $sqlconditions);
$results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
} else {
$results = data_request::get_records(null, $sort, '', $offset, $limit);
}
}
}
return $results;
}
/**
* Fetches the count of data request records based on the given parameters.
*
* @param int $userid The User ID.
* @param int[] $statuses The status filters.
* @param int[] $types The request type filters.
* @return int
* @throws coding_exception
* @throws dml_exception
*/
public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
global $DB, $USER;
$count = 0;
$sqlparams = [];
$sqlconditions = [];
if (!empty($statuses)) {
list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
$sqlconditions[] = "status $statusinsql";
}
if (!empty($types)) {
list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
$sqlconditions[] = "type $typeinsql";
$sqlparams = array_merge($sqlparams, $typeparams);
}
if ($userid) {
// Get the data requests for the user or data requests made by the user.
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
$params = [
'userid' => $userid,
'requestedby' => $userid
];
// Build a list of user IDs that the user is allowed to make data requests for.
// Of course, the user should be included in this list.
$alloweduserids = [$userid];
// Get any users that the user can make data requests for.
if ($children = helper::get_children_of_user($userid)) {
// Get the list of user IDs of the children and merge to the allowed user IDs.
$alloweduserids = array_merge($alloweduserids, array_keys($children));
}
list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
$sqlconditions[] .= "userid $insql";
$select = implode(' AND ', $sqlconditions);
$params = array_merge($params, $inparams, $sqlparams);
$count = data_request::count_records_select($select, $params);
} else {
// If the current user is one of the site's Data Protection Officers, then fetch all data requests.
if (self::is_site_dpo($USER->id)) {
if (!empty($sqlconditions)) {
$select = implode(' AND ', $sqlconditions);
$count = data_request::count_records_select($select, $sqlparams);
} else {
$count = data_request::count_records();
}
}
}
return $count;
}
/**
* Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
*

@ -35,6 +35,17 @@ use tool_dataprivacy\api;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/** The default number of results to be shown per page. */
const DEFAULT_PAGE_SIZE = 20;
/** Filter constant associated with the request type filter. */
const FILTER_TYPE = 1;
/** Filter constant associated with the request status filter. */
const FILTER_STATUS = 2;
/** The request filters preference key. */
const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
/**
* Retrieves the human-readable text value of a data request type.
@ -45,16 +56,11 @@ class helper {
* @throws moodle_exception
*/
public static function get_request_type_string($requesttype) {
switch ($requesttype) {
case api::DATAREQUEST_TYPE_EXPORT:
return get_string('requesttypeexport', 'tool_dataprivacy');
case api::DATAREQUEST_TYPE_DELETE:
return get_string('requesttypedelete', 'tool_dataprivacy');
case api::DATAREQUEST_TYPE_OTHERS:
return get_string('requesttypeothers', 'tool_dataprivacy');
default:
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
$types = self::get_request_types();
if (!isset($types[$requesttype])) {
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
}
return $types[$requesttype];
}
/**
@ -66,16 +72,37 @@ class helper {
* @throws moodle_exception
*/
public static function get_shortened_request_type_string($requesttype) {
switch ($requesttype) {
case api::DATAREQUEST_TYPE_EXPORT:
return get_string('requesttypeexportshort', 'tool_dataprivacy');
case api::DATAREQUEST_TYPE_DELETE:
return get_string('requesttypedeleteshort', 'tool_dataprivacy');
case api::DATAREQUEST_TYPE_OTHERS:
return get_string('requesttypeothersshort', 'tool_dataprivacy');
default:
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
$types = self::get_request_types_short();
if (!isset($types[$requesttype])) {
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
}
return $types[$requesttype];
}
/**
* Returns the key value-pairs of request type code and their string value.
*
* @return array
*/
public static function get_request_types() {
return [
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
];
}
/**
* Returns the key value-pairs of request type code and their shortened string value.
*
* @return array
*/
public static function get_request_types_short() {
return [
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
];
}
/**
@ -83,30 +110,32 @@ class helper {
*
* @param int $status The request status.
* @return string
* @throws coding_exception
* @throws moodle_exception
*/
public static function get_request_status_string($status) {
switch ($status) {
case api::DATAREQUEST_STATUS_PENDING:
return get_string('statuspending', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_PREPROCESSING:
return get_string('statuspreprocessing', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
return get_string('statusawaitingapproval', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_APPROVED:
return get_string('statusapproved', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_PROCESSING:
return get_string('statusprocessing', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_COMPLETE:
return get_string('statuscomplete', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_CANCELLED:
return get_string('statuscancelled', 'tool_dataprivacy');
case api::DATAREQUEST_STATUS_REJECTED:
return get_string('statusrejected', 'tool_dataprivacy');
default:
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
$statuses = self::get_request_statuses();
if (!isset($statuses[$status])) {
throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
}
return $statuses[$status];
}
/**
* Returns the key value-pairs of request status code and string value.
*
* @return array
*/
public static function get_request_statuses() {
return [
api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
];
}
/**
@ -146,4 +175,34 @@ class helper {
}
return $finalresults;
}
/**
* Get options for the data requests filter.
*
* @return array
* @throws coding_exception
*/
public static function get_request_filter_options() {
$filters = [
self::FILTER_TYPE => (object)[
'name' => get_string('requesttype', 'tool_dataprivacy'),
'options' => self::get_request_types_short()
],
self::FILTER_STATUS => (object)[
'name' => get_string('requeststatus', 'tool_dataprivacy'),
'options' => self::get_request_statuses()
],
];
$options = [];
foreach ($filters as $category => $filtercategory) {
foreach ($filtercategory->options as $key => $name) {
$option = (object)[
'category' => $filtercategory->name,
'name' => $name
];
$options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
}
}
return $options;
}
}

@ -25,7 +25,6 @@ namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
use coding_exception;
use dml_exception;
use moodle_exception;
use moodle_url;
use renderable;
@ -34,7 +33,7 @@ use single_select;
use stdClass;
use templatable;
use tool_dataprivacy\data_request;
use tool_dataprivacy\output\expired_contexts_table;
use tool_dataprivacy\local\helper;
/**
* Class containing data for a user's data requests.
@ -44,9 +43,6 @@ use tool_dataprivacy\output\expired_contexts_table;
*/
class data_deletion_page implements renderable, templatable {
/** The default number of results to be shown per page. */
const DEFAULT_PAGE_SIZE = 20;
/** @var data_request[] $requests List of data requests. */
protected $filter = null;
@ -57,7 +53,7 @@ class data_deletion_page implements renderable, templatable {
* Construct this renderable.
*
* @param \tool_dataprivacy\data_request[] $filter
* @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
* @param expired_contexts_table $expiredcontextstable
*/
public function __construct($filter, expired_contexts_table $expiredcontextstable) {
$this->filter = $filter;
@ -70,7 +66,6 @@ class data_deletion_page implements renderable, templatable {
* @param renderer_base $output
* @return stdClass
* @throws coding_exception
* @throws dml_exception
* @throws moodle_exception
*/
public function export_for_template(renderer_base $output) {
@ -87,7 +82,7 @@ class data_deletion_page implements renderable, templatable {
$data->filter = $filterselector->export_for_template($output);
ob_start();
$this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
$this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
$expiredcontexts = ob_get_contents();
ob_end_clean();
$data->expiredcontexts = $expiredcontexts;

@ -24,20 +24,17 @@
namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
use action_menu;
use action_menu_link_secondary;
use coding_exception;
use context_system;
use dml_exception;
use moodle_exception;
use moodle_url;
use renderable;
use renderer_base;
use single_select;
use stdClass;
use templatable;
use tool_dataprivacy\api;
use tool_dataprivacy\data_request;
use tool_dataprivacy\external\data_request_exporter;
use tool_dataprivacy\local\helper;
/**
* Class containing data for a user's data requests.
@ -47,16 +44,21 @@ use tool_dataprivacy\external\data_request_exporter;
*/
class data_requests_page implements renderable, templatable {
/** @var data_request[] $requests List of data requests. */
protected $requests = [];
/** @var data_requests_table $table The data requests table. */
protected $table;
/** @var int[] $filters The applied filters. */
protected $filters = [];
/**
* Construct this renderable.
*
* @param data_request[] $requests
* @param data_requests_table $table The data requests table.
* @param int[] $filters The applied filters.
*/
public function __construct($requests) {
$this->requests = $requests;
public function __construct($table, $filters) {
$this->table = $table;
$this->filters = $filters;
}
/**
@ -78,43 +80,17 @@ class data_requests_page implements renderable, templatable {
$data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
}
$requests = [];
foreach ($this->requests as $request) {
$requestid = $request->get('id');
$status = $request->get('status');
$requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
$item = $requestexporter->export($output);
$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
$filteroptions = helper::get_request_filter_options();
$filter = new request_filter($filteroptions, $this->filters, $url);
$data->filter = $filter->export_for_template($output);
// Prepare actions.
$actions = [];
ob_start();
$this->table->out(helper::DEFAULT_PAGE_SIZE, true);
$requests = ob_get_contents();
ob_end_clean();
// View action.
$actionurl = new moodle_url('#');
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
$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);
}
$actionsmenu = new action_menu($actions);
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
$item->actions = $actionsmenu->export_for_template($output);
$requests[] = $item;
}
$data->requests = $requests;
$data->datarequests = $requests;
return $data;
}
}

@ -0,0 +1,262 @@
<?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/>.
/**
* Contains the class used for the displaying the data requests table.
*
* @package tool_dataprivacy
* @copyright 2018 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\output;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/tablelib.php');
use action_menu;
use action_menu_link_secondary;
use coding_exception;
use dml_exception;
use html_writer;
use moodle_url;
use stdClass;
use table_sql;
use tool_dataprivacy\api;
use tool_dataprivacy\external\data_request_exporter;
defined('MOODLE_INTERNAL') || die;
/**
* The class for displaying the data requests table.
*
* @copyright 2018 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_requests_table extends table_sql {
/** @var int The user ID. */
protected $userid = 0;
/** @var int[] The status filters. */
protected $statuses = [];
/** @var int[] The request type filters. */
protected $types = [];
/** @var bool Whether this table is being rendered for managing data requests. */
protected $manage = false;
/** @var stdClass[] Array of data request persistents. */
protected $datarequests = [];
/**
* data_requests_table constructor.
*
* @param int $userid The user ID
* @param int[] $statuses
* @param int[] $types
* @param bool $manage
* @throws coding_exception
*/
public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
parent::__construct('data-requests-table');
$this->userid = $userid;
$this->statuses = $statuses;
$this->types = $types;
$this->manage = $manage;
$columnheaders = [
'type' => get_string('requesttype', 'tool_dataprivacy'),
'userid' => get_string('user', 'tool_dataprivacy'),
'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
'requestedby' => get_string('requestby', 'tool_dataprivacy'),
'status' => get_string('requeststatus', 'tool_dataprivacy'),
'comments' => get_string('message', 'tool_dataprivacy'),
'actions' => '',
];
$this->define_columns(array_keys($columnheaders));
$this->define_headers(array_values($columnheaders));
$this->no_sorting('actions');
}
/**
* The type column.
*
* @param stdClass $data The row data.
* @return string
*/
public function col_type($data) {
if ($this->manage) {
return $data->typenameshort;
}
return $data->typename;
}
/**
* The user column.
*
* @param stdClass $data The row data.
* @return mixed
*/
public function col_userid($data) {
$user = $data->foruser;
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
}
/**
* The context information column.
*
* @param stdClass $data The row data.
* @return string
*/
public function col_timecreated($data) {
return userdate($data->timecreated);
}
/**
* The requesting user's column.
*
* @param stdClass $data The row data.
* @return mixed
*/
public function col_requestedby($data) {
$user = $data->requestedbyuser;
return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
}
/**
* The status column.
*
* @param stdClass $data The row data.
* @return mixed
*/
public function col_status($data) {
return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
}
/**
* The comments column.
*
* @param stdClass $data The row data.
* @return string
*/
public function col_comments($data) {
return shorten_text($data->comments, 60);
}
/**
* The actions column.
*
* @param stdClass $data The row data.
* @return string
*/
public function col_actions($data) {
global $OUTPUT;
$requestid = $data->id;
$status = $data->status;
// Prepare actions.
$actions = [];
// View action.
$actionurl = new moodle_url('#');
$actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
$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);
}
$actionsmenu = new action_menu($actions);
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
return $OUTPUT->render($actionsmenu);
}
/**
* Query the database for results to display in the table.
*
* @param int $pagesize size of page for paginated displayed table.
* @param bool $useinitialsbar do you want to use the initials bar.
* @throws dml_exception
* @throws coding_exception
*/
public function query_db($pagesize, $useinitialsbar = true) {
global $PAGE;
// Count data requests from the given conditions.
$total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
$this->pagesize($pagesize, $total);
$sort = $this->get_sql_sort();
// Get data requests from the given conditions.
$datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
$this->get_page_start(), $this->get_page_size());
$this->rawdata = [];
$context = \context_system::instance();
$renderer = $PAGE->get_renderer('tool_dataprivacy');
foreach ($datarequests as $persistent) {
$exporter = new data_request_exporter($persistent, ['context' => $context]);
$this->rawdata[] = $exporter->export($renderer);
}
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
}
/**
* Override default implementation to display a more meaningful information to the user.
*/
public function print_nothing_to_display() {
global $OUTPUT;
echo $this->render_reset_button();
$this->print_initials_bar();
if (!empty($this->statuses) || !empty($this->types)) {
$message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
} else {
$message = get_string('nodatarequests', 'tool_dataprivacy');
}
echo $OUTPUT->notification($message, 'warning');
}
/**
* Override the table's show_hide_link method to prevent the show/hide links from rendering.
*
* @param string $column the column name, index into various names.
* @param int $index numerical index of the column.
* @return string HTML fragment.
*/
protected function show_hide_link($column, $index) {
return '';
}
}

@ -0,0 +1,98 @@
<?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/>.
/**
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
*
* @package tool_dataprivacy
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\output;
use moodle_url;
use renderable;
use renderer_base;
use stdClass;
use templatable;
defined('MOODLE_INTERNAL') || die();
/**
* Class containing the filter options data for rendering the autocomplete element for the data requests page.
*
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class request_filter implements renderable, templatable {
/** @var array $filteroptions The filter options. */
protected $filteroptions;
/** @var array $selectedoptions The list of selected filter option values. */
protected $selectedoptions;
/** @var moodle_url|string $baseurl The url with params needed to call up this page. */
protected $baseurl;
/**
* request_filter constructor.
*
* @param array $filteroptions The filter options.
* @param array $selectedoptions The list of selected filter option values.
* @param string|moodle_url $baseurl The url with params needed to call up this page.
*/
public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
$this->filteroptions = $filteroptions;
$this->selectedoptions = $selectedoptions;
if (!empty($baseurl)) {
$this->baseurl = new moodle_url($baseurl);
}
}
/**
* Function to export the renderer data in a format that is suitable for a mustache template.
*
* @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
* @return stdClass|array
*/
public function export_for_template(renderer_base $output) {
global $PAGE;
$data = new stdClass();
if (empty($this->baseurl)) {
$this->baseurl = $PAGE->url;
}
$data->action = $this->baseurl->out(false);
foreach ($this->selectedoptions as $option) {
if (!isset($this->filteroptions[$option])) {
$this->filteroptions[$option] = $option;
}
}
$data->filteroptions = [];
foreach ($this->filteroptions as $value => $label) {
$selected = in_array($value, $this->selectedoptions);
$filteroption = (object)[
'value' => $value,
'label' => $label
];
$filteroption->selected = $selected;
$data->filteroptions[] = $filteroption;
}
return $data;
}
}

@ -51,7 +51,10 @@ class provider implements
\core_privacy\local\metadata\provider,
// This tool may provide access to and deletion of user data.
\core_privacy\local\request\plugin\provider {
\core_privacy\local\request\plugin\provider,
// This plugin has some sitewide user preferences to export.
\core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
@ -70,6 +73,10 @@ class provider implements
],
'privacy:metadata:request'
);
$collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
'privacy:metadata:preference:tool_dataprivacy_request-filters');
return $collection;
}
@ -162,4 +169,36 @@ class provider implements
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
if ($preffilter !== null) {
$filters = json_decode($preffilter);
$descriptions = [];
foreach ($filters as $filter) {
list($category, $value) = explode(':', $filter);
$option = new stdClass();
switch($category) {
case tool_helper::FILTER_TYPE:
$option->category = get_string('requesttype', 'tool_dataprivacy');
$option->name = tool_helper::get_shortened_request_type_string($value);
break;
case tool_helper::FILTER_STATUS:
$option->category = get_string('requeststatus', 'tool_dataprivacy');
$option->name = tool_helper::get_request_status_string($value);
break;
}
$descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
}
// Export the filter preference as comma-separated values and text descriptions.
$values = implode(', ', $filters);
$descriptionstext = implode(', ', $descriptions);
writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
}
}
}

@ -36,8 +36,38 @@ $title = get_string('datarequests', 'tool_dataprivacy');
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$requests = tool_dataprivacy\api::get_data_requests();
$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
if ($filtersapplied === [-1]) {
// If there are no filters submitted, check if there is a saved filters from the user preferences.
$filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
if ($filterprefs && empty($filterscleared)) {
$filtersapplied = json_decode($filterprefs);
} else {
$filtersapplied = [];
}
}
// Save the current applied filters to the user preferences.
set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
$types = [];
$statuses = [];
foreach ($filtersapplied as $filter) {
list($category, $value) = explode(':', $filter);
switch($category) {
case \tool_dataprivacy\local\helper::FILTER_TYPE:
$types[] = $value;
break;
case \tool_dataprivacy\local\helper::FILTER_STATUS:
$statuses[] = $value;
break;
}
}
$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
$table->baseurl = $url;
$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
echo $requestlistoutput->render($requestlist);

@ -112,6 +112,7 @@ $string['expandplugintype'] = 'Expand and collapse plugin type.';
$string['explanationtitle'] = 'Icons used on this page and what they mean.';
$string['external'] = 'Additional';
$string['externalexplanation'] = 'An additional plugin installed on this site.';
$string['filteroption'] = '{$a->category}: {$a->name}';
$string['frontpagecourse'] = 'Front page course';
$string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
$string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
@ -162,6 +163,7 @@ $string['nameemail'] = '{$a->name} ({$a->email})';
$string['nchildren'] = '{$a} children';
$string['newrequest'] = 'New request';
$string['nodatarequests'] = 'There are no data requests';
$string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
$string['noactivitiestoload'] = 'No activities';
$string['noassignedroles'] = 'No assigned roles in this context';
$string['noblockstoload'] = 'No blocks';
@ -176,6 +178,7 @@ $string['notset'] = 'Not set (use the default value)';
$string['pluginregistry'] = 'Plugin privacy registry';
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
$string['privacy'] = 'Privacy';
$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
$string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
$string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
$string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';

@ -26,92 +26,32 @@
* none
Context variables required for this template:
* requests - Array of data requests.
* newdatarequesturl string The URL pointing to the data request creation page.
* datarequests string The HTML of the data requests table.
Example context (json):
{
"requests": [
{
"id": 1,
"foruser" : {
"fullname": "Oscar Olsen",
"profileurl": "#"
"newdatarequesturl": "#",
"datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>",
"filter": {
"action": "#",
"filteroptions": [
{
"value": "1",
"label": "Option 1"
},
"typenameshort" : "Export",
"comments": "I would like to download all of my daughter's personal data",
"statuslabelclass": "label-default",
"statuslabel": "Pending",
"timecreated" : 1517902435,
"requestedbyuser" : {
"fullname": "Martha Smith",
"profileurl": "#"
}
},
{
"id": 2,
"foruser" : {
"fullname": "Alexandre Denys",
"profileurl": "#"
{
"value": "2",
"label": "Option 2",
"selected": true
},
"typenameshort" : "Export",
"comments": "Please give me all of the information you have about me...",
"statuslabelclass": "label-warning",
"statuslabel": "Awaiting completion",
"timecreated" : 1517902435,
"requestedbyuser" : {
"fullname": "Martha Smith",
"profileurl": "#"
{
"value": "3",
"label": "Option 3",
"selected": true
}
},
{
"id": 3,
"foruser" : {
"fullname": "Hirondino Moura",
"profileurl": "#"
},
"typenameshort" : "Delete",
"comments": "Please delete all of my son's personal data.",
"statuslabelclass": "label-success",
"statuslabel": "Complete",
"timecreated" : 1517902435,
"requestedbyuser" : {
"fullname": "Martha Smith",
"profileurl": "#"
}
},
{
"id": 4,
"foruser" : {
"fullname": "Florian Krause",
"profileurl": "#"
},
"typenameshort" : "Delete",
"comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
"statuslabelclass": "label-danger",
"statuslabel": "Rejected",
"timecreated" : 1517902435,
"requestedbyuser" : {
"fullname": "Martha Smith",
"profileurl": "#"
}
},
{
"id": 5,
"foruser" : {
"fullname": "Nicklas Sørensen",
"profileurl": "#"
},
"typenameshort" : "Export",
"comments": "Please let me download my data",
"statuslabelclass": "label-info",
"statuslabel": "Processing",
"timecreated" : 1517902435,
"requestedbyuser" : {
"fullname": "Martha Smith",
"profileurl": "#"
}
}
]
]
}
}
}}
@ -121,53 +61,19 @@
<div data-region="datarequests">
<div class="m-t-1 m-b-1">
<a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
{{#str}}newrequest, tool_dataprivacy{{/str}}
</a>
<div class="pull-right">
<a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
{{#str}}newrequest, tool_dataprivacy{{/str}}
</a>
</div>
{{#filter}}
{{>tool_dataprivacy/request_filter}}
{{/filter}}
</div>
<div class="m-t-1 m-b-1" data-region="data-requests-table">
{{{datarequests}}}
</div>
<table class="generaltable fullwidth">
<thead>
<tr>
<th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
<th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
</tr>
</thead>
<tbody>
{{#requests}}
<tr {{!
}} data-region="request-node"{{!
}} data-id="{{id}}"{{!
}} data-type="{{type}}"{{!
}} data-status="{{status}}"{{!
}}>
<td>{{typenameshort}}</td>
<td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
<td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
<td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
<td>
<span class="label {{statuslabelclass}}">{{statuslabel}}</span>
</td>
<td>{{#shortentext}}60, {{comments}}{{/shortentext}}</td>
<td>
{{#actions}}
{{> core/action_menu}}
{{/actions}}
</td>
</tr>
{{/requests}}
{{^requests}}
<tr>
<td class="text-muted" colspan="6">
{{#str}}nodatarequests, tool_dataprivacy{{/str}}
</td>
</tr>
{{/requests}}
</tbody>
</table>
</div>
{{#js}}

@ -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/>.
}}
{{!
@template tool_dataprivacy/request_filter
Template for the request filter element.
Context variables required for this template:
* action string - The action URL for the form.
* filteroptions - Array of filter options.
* value string - The option value.
* label string - The option label.
* selected boolean - Whether the option is selected
Example context (json):
{
"action": "#",
"filteroptions": [
{
"value": "1",
"label": "Option 1"
},
{
"value": "2",
"label": "Option 2",
"selected": true
},
{
"value": "3",
"label": "Option 3",
"selected": true
},
{
"value": "4",
"label": "Option 4"
}
]
}
}}
<form method="post" action="{{action}}" class="m-b-1" role="search" id="request_filter_form">
<label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label>
<select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select">
{{#filteroptions}}
<option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option>
{{/filteroptions}}
</select>
<input type="hidden" id="filters-cleared" name="filters-cleared" value="0" />
</form>
{{#js}}
require(['tool_dataprivacy/request_filter'], function(Filter) {
Filter.init();
});
{{/js}}

@ -29,6 +29,7 @@ use tool_dataprivacy\api;
use tool_dataprivacy\data_registry;
use tool_dataprivacy\expired_context;
use tool_dataprivacy\data_request;
use tool_dataprivacy\local\helper;
use tool_dataprivacy\task\initiate_data_request_task;
use tool_dataprivacy\task\process_data_request_task;
@ -411,42 +412,128 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
}
/**
* Test for api::get_data_requests()
* Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
*
* @return array
*/
public function test_get_data_requests() {
public function get_data_requests_provider() {
$generator = new testing_data_generator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$comment = 'sample comment';
$user3 = $generator->create_user();
$user4 = $generator->create_user();
$user5 = $generator->create_user();
$users = [$user1, $user2, $user3, $user4, $user5];
$completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
$completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
// Make a data request as user 1.
$this->setUser($user1);
$d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Make a data request as user 2.
$this->setUser($user2);
$d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
return [
// Own data requests.
[$users, $user1, false, $completeonly],
// Non-DPO fetching all requets.
[$users, $user2, true, $completeonly],
// Admin fetching all completed and cancelled requests.
[$users, get_admin(), true, $completeandcancelled],
// Admin fetching all completed requests.
[$users, get_admin(), true, $completeonly],
// Guest fetching all requests.
[$users, guest_user(), true, $completeonly],
];
}
// Fetching data requests of specific users.
$requests = api::get_data_requests($user1->id);
$this->assertCount(1, $requests);
$datarequest = reset($requests);
$this->assertEquals($d1->to_record(), $datarequest->to_record());
/**
* Test for api::get_data_requests()
*
* @dataProvider get_data_requests_provider
* @param stdClass[] $users Array of users to create data requests for.
* @param stdClass $loggeduser The user logging in.
* @param boolean $fetchall Whether to fetch all records.
* @param int[] $statuses Status filters.
*/
public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
$comment = 'Data %s request comment by user %d';
$exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
$deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
// Make a data requests for the users.
foreach ($users as $user) {
$this->setUser($user);
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
}
$requests = api::get_data_requests($user2->id);
$this->assertCount(1, $requests);
$datarequest = reset($requests);
$this->assertEquals($d2->to_record(), $datarequest->to_record());
// Log in as the target user.
$this->setUser($loggeduser);
// Get records count based on the filters.
$userid = $loggeduser->id;
if ($fetchall) {
$userid = 0;
}
$count = api::get_data_requests_count($userid);
if (api::is_site_dpo($loggeduser->id)) {
// DPOs should see all the requests.
$this->assertEquals(count($users) * 2, $count);
} else {
if (empty($userid)) {
// There should be no data requests for this user available.
$this->assertEquals(0, $count);
} else {
// There should be only one (request with pending status).
$this->assertEquals(2, $count);
}
}
// Get data requests.
$requests = api::get_data_requests($userid);
// The number of requests should match the count.
$this->assertCount($count, $requests);
// Fetching data requests of all users.
// As guest.
$this->setGuestUser();
$requests = api::get_data_requests();
$this->assertEmpty($requests);
// Test filtering by status.
if ($count && !empty($statuses)) {
$filteredcount = api::get_data_requests_count($userid, $statuses);
// There should be none as they are all pending.
$this->assertEquals(0, $filteredcount);
$filteredrequests = api::get_data_requests($userid, $statuses);
$this->assertCount($filteredcount, $filteredrequests);
// As DPO (admin in this case, which is default if no site DPOs are set).
$this->setAdminUser();
$requests = api::get_data_requests();
$this->assertCount(2, $requests);
$statuscounts = [];
foreach ($statuses as $stat) {
$statuscounts[$stat] = 0;
}
$numstatus = count($statuses);
// Get all requests with status filter and update statuses, randomly.
foreach ($requests as $request) {
if (rand(0, 1)) {
continue;
}
if ($numstatus > 1) {
$index = rand(0, $numstatus - 1);
$status = $statuses[$index];
} else {
$status = reset($statuses);
}
$statuscounts[$status]++;
api::update_request_status($request->get('id'), $status);
}
$total = array_sum($statuscounts);
$filteredcount = api::get_data_requests_count($userid, $statuses);
$this->assertEquals($total, $filteredcount);
$filteredrequests = api::get_data_requests($userid, $statuses);
$this->assertCount($filteredcount, $filteredrequests);
// Confirm the filtered requests match the status filter(s).
foreach ($filteredrequests as $request) {
$this->assertContains($request->get('status'), $statuses);
}
if ($numstatus > 1) {
// Fetch by individual status to check the numbers match.
foreach ($statuses as $status) {
$filteredcount = api::get_data_requests_count($userid, [$status]);
$this->assertEquals($statuscounts[$status], $filteredcount);
$filteredrequests = api::get_data_requests($userid, [$status]);
$this->assertCount($filteredcount, $filteredrequests);
}
}
}
}
/**