MDL-62211 tool_dataprivacy: Request filtering and pagination
authorJun Pataleta <jun@moodle.com>
Wed, 2 May 2018 14:51:17 +0000 (22:51 +0800)
committerJun Pataleta <jun@moodle.com>
Fri, 29 Jun 2018 02:32:04 +0000 (10:32 +0800)
* Make data requests page use table_sql to make it sortable
* Also, add an autocomplete element that enables filtering by
  status and request type.

14 files changed:
admin/tool/dataprivacy/amd/build/request_filter.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/request_filter.js [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/data_deletion_page.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/request_filter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/data_requests.mustache
admin/tool/dataprivacy/templates/request_filter.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php

diff --git a/admin/tool/dataprivacy/amd/build/request_filter.min.js b/admin/tool/dataprivacy/amd/build/request_filter.min.js
new file mode 100644 (file)
index 0000000..61344eb
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/request_filter.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/request_filter.js b/admin/tool/dataprivacy/amd/src/request_filter.js
new file mode 100644 (file)
index 0000000..6b915c8
--- /dev/null
@@ -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();
+        }
+    };
+});
index 275044a..42acb42 100644 (file)
@@ -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.
      *
index c68c994..f98362d 100644 (file)
@@ -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;
+    }
 }
index c1a2202..444569e 100644 (file)
@@ -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;
index c1b2861..7ea4bf8 100644 (file)
 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);
-
-            // 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);
+        $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);
 
-            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);
+        ob_start();
+        $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+        $requests = ob_get_contents();
+        ob_end_clean();
 
-                // 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;
     }
 }
diff --git a/admin/tool/dataprivacy/classes/output/data_requests_table.php b/admin/tool/dataprivacy/classes/output/data_requests_table.php
new file mode 100644 (file)
index 0000000..97918d9
--- /dev/null
@@ -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 '';
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/output/request_filter.php b/admin/tool/dataprivacy/classes/output/request_filter.php
new file mode 100644 (file)
index 0000000..ff3108d
--- /dev/null
@@ -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;
+    }
+}
index 59ca8c8..4c2d8c4 100644 (file)
@@ -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);
+        }
+    }
 }
index a3e613a..0887134 100644 (file)
@@ -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);
 
index b8622d8..e3e8268 100644 (file)
@@ -110,6 +110,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))';
@@ -160,6 +161,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';
@@ -174,6 +176,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';
index 45cedc6..594aaab 100644 (file)
     * 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": "#"
-                },
-                "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": "#"
-                }
-            },
-            {
-                "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": "#"
+                {
+                    "value": "2",
+                    "label": "Option 2",
+                    "selected": true
                 },
-                "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": "#"
+                {
+                    "value": "3",
+                    "label": "Option 3",
+                    "selected": true
                 }
-            },
-            {
-                "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": "#"
-                }
-            }
-        ]
+            ]
+        }
     }
 }}
 
 
 <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}}
diff --git a/admin/tool/dataprivacy/templates/request_filter.mustache b/admin/tool/dataprivacy/templates/request_filter.mustache
new file mode 100644 (file)
index 0000000..ea4f15d
--- /dev/null
@@ -0,0 +1,67 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @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}}
index 6e06478..f4a7a66 100644 (file)
@@ -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);
-
-        // 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());
-
-        $requests = api::get_data_requests($user2->id);
-        $this->assertCount(1, $requests);
-        $datarequest = reset($requests);
-        $this->assertEquals($d2->to_record(), $datarequest->to_record());
-
-        // Fetching data requests of all users.
-        // As guest.
-        $this->setGuestUser();
-        $requests = api::get_data_requests();
-        $this->assertEmpty($requests);
-
-        // 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);
+        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],
+        ];
+    }
+
+    /**
+     * 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));
+        }
+
+        // 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);
+
+        // 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);
+
+            $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);
+                }
+            }
+        }
     }
 
     /**