MDL-62589 dataprivacy: Add ability to resubmit a request
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 6 Nov 2018 23:56:31 +0000 (07:56 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 8 Nov 2018 03:45:36 +0000 (11:45 +0800)
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_request.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/resubmitrequest.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/data_request_test.php [new file with mode: 0644]

index a17d388..024c2b4 100644 (file)
@@ -447,6 +447,48 @@ class api {
         return data_request::record_exists_select($select, $params);
     }
 
+    /**
+     * Find whether any ongoing requests exist for a set of users.
+     *
+     * @param   array   $userids
+     * @return  array
+     */
+    public static function find_ongoing_request_types_for_users(array $userids) : array {
+        global $DB;
+
+        if (empty($userids)) {
+            return [];
+        }
+
+        // Check if the user already has an incomplete data request of the same type.
+        $nonpendingstatuses = [
+            self::DATAREQUEST_STATUS_COMPLETE,
+            self::DATAREQUEST_STATUS_CANCELLED,
+            self::DATAREQUEST_STATUS_REJECTED,
+            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
+            self::DATAREQUEST_STATUS_EXPIRED,
+            self::DATAREQUEST_STATUS_DELETED,
+        ];
+        list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
+        list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
+
+        $select = "userid {$userinsql} AND status {$statusinsql}";
+        $params = array_merge($statusparams, $userparams);
+
+        $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
+
+        $returnval = [];
+        foreach ($userids as $userid) {
+            $returnval[$userid] = (object) [];
+        }
+
+        foreach ($requests as $request) {
+            $returnval[$request->userid]->{$request->type} = true;
+        }
+
+        return $returnval;
+    }
+
     /**
      * Determines whether a request is active or not based on its status.
      *
index 9976422..924118d 100644 (file)
@@ -158,8 +158,6 @@ class data_request extends persistent {
         return $result;
     }
 
-
-
     /**
      * Fetch completed data requests which are due to expire.
      *
@@ -224,4 +222,66 @@ class data_request extends persistent {
             }
         }
     }
+
+    /**
+     * Whether this request is in a state appropriate for reset/resubmission.
+     *
+     * Note: This does not check whether any other completed requests exist for this user.
+     *
+     * @return  bool
+     */
+    public function is_resettable() : bool {
+        if (api::DATAREQUEST_TYPE_OTHERS == $this->get('type')) {
+            // It is not possible to reset 'other' reqeusts.
+            return false;
+        }
+
+        $resettable = [
+            api::DATAREQUEST_STATUS_APPROVED => true,
+            api::DATAREQUEST_STATUS_REJECTED => true,
+        ];
+
+        return isset($resettable[$this->get('status')]);
+    }
+
+    /**
+     * Whether this request is 'active'.
+     *
+     * @return  bool
+     */
+    public function is_active() : bool {
+        $active = [
+            api::DATAREQUEST_STATUS_APPROVED => true,
+        ];
+
+        return isset($active[$this->get('status')]);
+    }
+
+    /**
+     * Reject this request and resubmit it as a fresh request.
+     *
+     * Note: This does not check whether any other completed requests exist for this user.
+     *
+     * @return  self
+     */
+    public function resubmit_request() : data_request {
+        if ($this->is_active()) {
+            $this->set('status', api::DATAREQUEST_STATUS_REJECTED)->save();
+        }
+
+        if (!$this->is_resettable()) {
+            throw new \moodle_exception('cannotreset', 'tool_dataprivacy');
+        }
+
+        $currentdata = $this->to_record();
+        unset($currentdata->id);
+
+        $clone = api::create_data_request($this->get('userid'), $this->get('type'));
+        $clone->set('comments', $this->get('comments'));
+        $clone->set('dpo', $this->get('dpo'));
+        $clone->set('requestedby', $this->get('requestedby'));
+        $clone->save();
+
+        return $clone;
+    }
 }
index b6fa957..5149368 100644 (file)
@@ -62,6 +62,9 @@ class data_requests_table extends table_sql {
     /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */
     protected $datarequests = [];
 
+    /** @var \stdClass[] List of userids and whether they have any ongoing active requests. */
+    protected $ongoingrequests = [];
+
     /** @var int The number of data request to be displayed per page. */
     protected $perpage;
 
@@ -247,6 +250,20 @@ class data_requests_table extends table_sql {
                 break;
         }
 
+        if ($this->manage) {
+            $persistent = $this->datarequests[$requestid];
+            $canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type});
+            $canreset = $canreset && $persistent->is_resettable();
+            if ($canreset) {
+                $reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [
+                        'requestid' => $requestid,
+                    ]);
+                $actiondata = ['data-action' => 'reset', 'data-requestid' => $requestid];
+                $actiontext = get_string('resubmitrequestasnew', 'tool_dataprivacy');
+                $actions[] = new action_menu_link_secondary($reseturl, null, $actiontext, $actiondata);
+            }
+        }
+
         $actionsmenu = new action_menu($actions);
         $actionsmenu->set_menu_trigger(get_string('actions'));
         $actionsmenu->set_owner_selector('request-actions-' . $requestid);
@@ -284,12 +301,19 @@ class data_requests_table extends table_sql {
         $context = \context_system::instance();
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
 
+        $forusers = [];
         foreach ($datarequests as $persistent) {
             $this->datarequests[$persistent->get('id')] = $persistent;
             $exporter = new data_request_exporter($persistent, ['context' => $context]);
             $this->rawdata[] = $exporter->export($renderer);
+            $forusers[] = $persistent->get('userid');
         }
 
+        // Fetch the list of all ongoing requests for the users currently shown.
+        // This is used to determine whether any non-active request can be resubmitted.
+        // There can only be one ongoing request of a type for each user.
+        $this->ongoingrequests = api::find_ongoing_request_types_for_users($forusers);
+
         // Set initial bars.
         if ($useinitialsbar) {
             $this->initialbars($total > $pagesize);
index 84ca013..4818a6d 100644 (file)
@@ -39,6 +39,7 @@ $string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data privacy t
 $string['cachedef_contextlevel'] = 'Context levels purpose and category';
 $string['cancelrequest'] = 'Cancel request';
 $string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
+$string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.';
 $string['categories'] = 'Categories';
 $string['category'] = 'Category';
 $string['category_help'] = 'A category in the data registry describes a type of data. A new category may be added, or if Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > Site.';
@@ -55,6 +56,7 @@ $string['confirmcompletion'] = 'Do you really want to mark this user enquiry as
 $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
 $string['confirmdenial'] = 'Do you really want deny this data request?';
 $string['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?';
+$string['confirmrequestresubmit'] = 'Are you sure you wish to cancel the current {$a->type} request for {$a->username} and resubmit it?';
 $string['contactdataprotectionofficer'] = 'Contact the privacy officer';
 $string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
 $string['contextlevelname10'] = 'Site';
@@ -264,6 +266,9 @@ When checking the active enrolment in a course, if the course has no end date th
 If the course has no end date, and this setting is enabled, then the user cannot be deleted.';
 $string['requiresattention'] = 'Requires attention.';
 $string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
+$string['resubmitrequestasnew'] = 'Resubmit as new request';
+$string['resubmitrequest'] = 'Resubmit {$a->type} request for {$a->username}';
+$string['resubmittedrequest'] = 'The existing {$a->type} request for {$a->username} was cancelled and resubmitted';
 $string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
 $string['resultdownloadready'] = 'Your copy of your personal data in {$a} that you recently requested is now available for download. Please click on the link below to go to the download page.';
 $string['reviewdata'] = 'Review data';
diff --git a/admin/tool/dataprivacy/resubmitrequest.php b/admin/tool/dataprivacy/resubmitrequest.php
new file mode 100644 (file)
index 0000000..ac7520e
--- /dev/null
@@ -0,0 +1,60 @@
+<?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/>.
+
+/**
+ * Display the request reject + resubmit confirmation page.
+ *
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package tool_dataprivacy
+ */
+
+require_once('../../../config.php');
+
+$requestid = required_param('requestid', PARAM_INT);
+$confirm = optional_param('confirm', null, PARAM_INT);
+
+$PAGE->set_url(new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]));
+
+require_login();
+
+$PAGE->set_context(\context_system::instance());
+require_capability('tool/dataprivacy:managedatarequests', $PAGE->context);
+
+$manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+
+$originalrequest = \tool_dataprivacy\api::get_request($requestid);
+$user = \core_user::get_user($originalrequest->get('userid'));
+$stringparams = (object) [
+        'username' => fullname($user),
+        'type' => \tool_dataprivacy\local\helper::get_shortened_request_type_string($originalrequest->get('type')),
+    ];
+
+if (null !== $confirm && confirm_sesskey()) {
+    $originalrequest->resubmit_request();
+    redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
+}
+
+$heading = get_string('resubmitrequest', 'tool_dataprivacy', $stringparams);
+$PAGE->set_title($heading);
+$PAGE->set_heading($heading);
+
+echo $OUTPUT->header();
+
+$confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams);
+$confirmurl = new \moodle_url($PAGE->url, ['confirm' => 1]);
+echo $OUTPUT->confirm($confirmstring, $confirmurl, $manageurl);
+echo $OUTPUT->footer();
index ec6c171..a1a5de9 100644 (file)
@@ -2052,4 +2052,116 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             'category' => $cat,
         ];
     }
+
+    /**
+     * Ensure that the find_ongoing_request_types_for_users only returns requests which are active.
+     */
+    public function test_find_ongoing_request_types_for_users() {
+        $this->resetAfterTest();
+
+        // Create users and their requests:.
+        // - u1 has no requests of any type.
+        // - u2 has one rejected export request.
+        // - u3 has one rejected other request.
+        // - u4 has one rejected delete request.
+        // - u5 has one active and one rejected export request.
+        // - u6 has one active and one rejected other request.
+        // - u7 has one active and one rejected delete request.
+        // - u8 has one active export, and one active delete request.
+        $u1 = $this->getDataGenerator()->create_user();
+        $u1expect = (object) [];
+
+        $u2 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u2->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED);
+        $u2expect = (object) [];
+
+        $u3 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u3->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED);
+        $u3expect = (object) [];
+
+        $u4 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u4->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED);
+        $u4expect = (object) [];
+
+        $u5 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED);
+        $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED);
+        $u5expect = (object) [
+            api::DATAREQUEST_TYPE_EXPORT => true,
+        ];
+
+        $u6 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED);
+        $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_APPROVED);
+        $u6expect = (object) [
+            api::DATAREQUEST_TYPE_OTHERS => true,
+        ];
+
+        $u7 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED);
+        $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED);
+        $u7expect = (object) [
+            api::DATAREQUEST_TYPE_DELETE => true,
+        ];
+
+        $u8 = $this->getDataGenerator()->create_user();
+        $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED);
+        $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED);
+        $u8expect = (object) [
+            api::DATAREQUEST_TYPE_EXPORT => true,
+            api::DATAREQUEST_TYPE_DELETE => true,
+        ];
+
+        // Test with no users specified.
+        $result = api::find_ongoing_request_types_for_users([]);
+        $this->assertEquals([], $result);
+
+        // Fetch a subset of the users.
+        $result = api::find_ongoing_request_types_for_users([$u3->id, $u4->id, $u5->id]);
+        $this->assertEquals([
+                $u3->id => $u3expect,
+                $u4->id => $u4expect,
+                $u5->id => $u5expect,
+            ], $result);
+
+        // Fetch the empty user.
+        $result = api::find_ongoing_request_types_for_users([$u1->id]);
+        $this->assertEquals([
+                $u1->id => $u1expect,
+            ], $result);
+
+        // Fetch all.
+        $result = api::find_ongoing_request_types_for_users(
+            [$u1->id, $u2->id, $u3->id, $u4->id, $u5->id, $u6->id, $u7->id, $u8->id]);
+        $this->assertEquals([
+                $u1->id => $u1expect,
+                $u2->id => $u2expect,
+                $u3->id => $u3expect,
+                $u4->id => $u4expect,
+                $u5->id => $u5expect,
+                $u6->id => $u6expect,
+                $u7->id => $u7expect,
+                $u8->id => $u8expect,
+            ], $result);
+    }
+
+    /**
+     * Create  a new data request for the user with the type and status specified.
+     *
+     * @param   int     $userid
+     * @param   int     $type
+     * @param   int     $status
+     * @return  \tool_dataprivacy\data_request
+     */
+    protected function create_request_with_type_and_status(int $userid, int $type, int $status) : \tool_dataprivacy\data_request {
+        $request = new \tool_dataprivacy\data_request(0, (object) [
+            'userid' => $userid,
+            'type' => $type,
+            'status' => $status,
+        ]);
+
+        $request->save();
+
+        return $request;
+    }
 }
diff --git a/admin/tool/dataprivacy/tests/data_request_test.php b/admin/tool/dataprivacy/tests/data_request_test.php
new file mode 100644 (file)
index 0000000..e10c12d
--- /dev/null
@@ -0,0 +1,241 @@
+<?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/>.
+
+/**
+ * Tests for the data_request persistent.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once('data_privacy_testcase.php');
+
+use tool_dataprivacy\api;
+
+/**
+ * Tests for the data_request persistent.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_data_request_testcase extends data_privacy_testcase {
+
+    /**
+     * Data provider for testing is_resettable, and is_active.
+     *
+     * @return  array
+     */
+    public function status_state_provider() : array {
+        return [
+            [
+                'state' => api::DATAREQUEST_STATUS_PENDING,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_APPROVED,
+                'resettable' => true,
+                'active' => true,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_PROCESSING,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_COMPLETE,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_CANCELLED,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_REJECTED,
+                'resettable' => true,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
+                'resettable' => false,
+                'active' => false,
+            ],
+            [
+                'state' => api::DATAREQUEST_STATUS_EXPIRED,
+                'resettable' => false,
+                'active' => false,
+            ],
+        ];
+    }
+
+    /**
+     * Test the pseudo states of a data request with an export request.
+     *
+     * @dataProvider        status_state_provider
+     * @param       int     $status
+     * @param       bool    $resettable
+     * @param       bool    $active
+     */
+    public function test_pseudo_states_export(int $status, bool $resettable, bool $active) {
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', $status);
+        $uut->set('type', api::DATAREQUEST_TYPE_EXPORT);
+
+        $this->assertEquals($resettable, $uut->is_resettable());
+        $this->assertEquals($active, $uut->is_active());
+    }
+
+    /**
+     * Test the pseudo states of a data request with a delete request.
+     *
+     * @dataProvider        status_state_provider
+     * @param       int     $status
+     * @param       bool    $resettable
+     * @param       bool    $active
+     */
+    public function test_pseudo_states_delete(int $status, bool $resettable, bool $active) {
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', $status);
+        $uut->set('type', api::DATAREQUEST_TYPE_DELETE);
+
+        $this->assertEquals($resettable, $uut->is_resettable());
+        $this->assertEquals($active, $uut->is_active());
+    }
+
+    /**
+     * Test the pseudo states of a data request.
+     *
+     * @dataProvider        status_state_provider
+     * @param       int     $status
+     */
+    public function test_can_reset_others($status) {
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', $status);
+        $uut->set('type', api::DATAREQUEST_TYPE_OTHERS);
+
+        $this->assertFalse($uut->is_resettable());
+    }
+
+    /**
+     * Data provider for states which are not resettable.
+     *
+     * @return      array
+     */
+    public function non_resettable_provider() : array {
+        $states = [];
+        foreach ($this->status_state_provider() as $thisstatus) {
+            if (!$thisstatus['resettable']) {
+                $states[] = $thisstatus;
+            }
+        }
+
+        return $states;
+    }
+
+    /**
+     * Ensure that requests which are not resettable cause an exception to be thrown.
+     *
+     * @dataProvider        non_resettable_provider
+     * @param       int     $status
+     */
+    public function test_non_resubmit_request($status) {
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', $status);
+
+        $this->expectException(\moodle_exception::class);
+        $this->expectExceptionMessage(get_string('cannotreset', 'tool_dataprivacy'));
+
+        $uut->resubmit_request();
+    }
+
+    /**
+     * Ensure that a rejected request can be reset.
+     */
+    public function test_resubmit_request() {
+        $this->resetAfterTest();
+
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', api::DATAREQUEST_STATUS_REJECTED);
+        $uut->set('type', api::DATAREQUEST_TYPE_DELETE);
+        $uut->set('comments', 'Foo');
+        $uut->set('requestedby', 42);
+        $uut->set('dpo', 98);
+
+        $newrequest = $uut->resubmit_request();
+
+        $this->assertEquals('Foo', $newrequest->get('comments'));
+        $this->assertEquals(42, $newrequest->get('requestedby'));
+        $this->assertEquals(98, $newrequest->get('dpo'));
+        $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
+        $this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
+
+        $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
+    }
+
+    /**
+     * Ensure that an active request can be reset.
+     */
+    public function test_resubmit_active_request() {
+        $this->resetAfterTest();
+
+        $uut = new \tool_dataprivacy\data_request();
+        $uut->set('status', api::DATAREQUEST_STATUS_APPROVED);
+        $uut->set('type', api::DATAREQUEST_TYPE_DELETE);
+        $uut->set('comments', 'Foo');
+        $uut->set('requestedby', 42);
+        $uut->set('dpo', 98);
+
+        $newrequest = $uut->resubmit_request();
+
+        $this->assertEquals('Foo', $newrequest->get('comments'));
+        $this->assertEquals(42, $newrequest->get('requestedby'));
+        $this->assertEquals(98, $newrequest->get('dpo'));
+        $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $newrequest->get('status'));
+        $this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
+
+        $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
+    }
+
+    /**
+     * Create a data request for the user.
+     *
+     * @param   int     $userid
+     * @param   int     $type
+     * @param   int     $status
+     * @return  data_request
+     */
+    public function create_request_for_user_with_status(int $userid, int $type, int $status) : data_request {
+        $request = new data_request(0, (object) [
+                'userid' => $userid,
+                'type' => $type,
+                'status' => $status,
+            ]);
+
+        $request->save();
+
+        return $request;
+    }
+}