Merge branch 'MDL-62589-master' of git://github.com/andrewnicols/moodle
authorJun Pataleta <jun@moodle.com>
Fri, 9 Nov 2018 02:36:52 +0000 (10:36 +0800)
committerJun Pataleta <jun@moodle.com>
Fri, 9 Nov 2018 02:36:52 +0000 (10:36 +0800)
1  2 
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

@@@ -41,7 -41,6 +41,7 @@@ use tool_dataprivacy\external\data_requ
  use tool_dataprivacy\local\helper;
  use tool_dataprivacy\task\initiate_data_request_task;
  use tool_dataprivacy\task\process_data_request_task;
 +use tool_dataprivacy\data_request;
  
  defined('MOODLE_INTERNAL') || die();
  
@@@ -254,8 -253,6 +254,8 @@@ class api 
          $datarequest->set('type', $type);
          // Set request comments.
          $datarequest->set('comments', $comments);
 +        // Set the creation method.
 +        $datarequest->set('creationmethod', $creationmethod);
  
          // Store subject access request.
          $datarequest->create();
       * @param int $userid The User ID.
       * @param int[] $statuses The status filters.
       * @param int[] $types The request type filters.
 +     * @param int[] $creationmethods The request creation method filters.
       * @param string $sort The order by clause.
       * @param int $offset Amount of records to skip.
       * @param int $limit Amount of records to fetch.
       * @throws coding_exception
       * @throws dml_exception
       */
 -    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
 +    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
 +                                             $sort = '', $offset = 0, $limit = 0) {
          global $DB, $USER;
          $results = [];
          $sqlparams = [];
              $sqlparams = array_merge($sqlparams, $typeparams);
          }
  
 +        // Set request creation method filter.
 +        if (!empty($creationmethods)) {
 +            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
 +            $sqlconditions[] = "creationmethod $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)";
  
              if (!empty($expiredrequests)) {
                  data_request::expire($expiredrequests);
 -                $results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
 +                $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
              }
          }
  
       * @param int $userid The User ID.
       * @param int[] $statuses The status filters.
       * @param int[] $types The request type filters.
 +     * @param int[] $creationmethods The request creation method filters.
       * @return int
       * @throws coding_exception
       * @throws dml_exception
       */
 -    public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
 +    public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
          global $DB, $USER;
          $count = 0;
          $sqlparams = [];
              $sqlconditions[] = "type $typeinsql";
              $sqlparams = array_merge($sqlparams, $typeparams);
          }
 +        if (!empty($creationmethods)) {
 +            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
 +            $sqlconditions[] = "creationmethod $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)";
              self::DATAREQUEST_STATUS_EXPIRED,
              self::DATAREQUEST_STATUS_DELETED,
          ];
-         list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
-         $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
+         list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
+         $select = "type = :type AND userid = :userid AND status {$insql}";
          $params = array_merge([
              'type' => $type,
              'userid' => $userid
          ], $inparams);
  
          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;
      }
  
      /**
          return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
      }
  
 +    /**
 +     * Creates an expired context record for the provided context id.
 +     *
 +     * @param int $contextid
 +     * @return \tool_dataprivacy\expired_context
 +     */
 +    public static function create_expired_context($contextid) {
 +        $record = (object)[
 +            'contextid' => $contextid,
 +            'status' => expired_context::STATUS_EXPIRED,
 +        ];
 +        $expiredctx = new expired_context(0, $record);
 +        $expiredctx->save();
 +
 +        return $expiredctx;
 +    }
 +
 +    /**
 +     * Deletes an expired context record.
 +     *
 +     * @param int $id The tool_dataprivacy_ctxexpire id.
 +     * @return bool True on success.
 +     */
 +    public static function delete_expired_context($id) {
 +        $expiredcontext = new expired_context($id);
 +        return $expiredcontext->delete();
 +    }
 +
      /**
       * Updates the status of an expired context.
       *
@@@ -21,9 -21,7 +21,9 @@@
   * @copyright  2018 Jun Pataleta
   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
   */
 +
  namespace tool_dataprivacy;
 +
  defined('MOODLE_INTERNAL') || die();
  
  use core\persistent;
@@@ -160,8 -158,6 +160,6 @@@ class data_request extends persistent 
          return $result;
      }
  
      /**
       * Fetch completed data requests which are due to expire.
       *
              }
          }
      }
+     /**
+      * 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;
+     }
  }
@@@ -62,6 -62,9 +62,9 @@@ class data_requests_table extends table
      /** @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;
  
       * @param int $userid The user ID
       * @param int[] $statuses
       * @param int[] $types
 +     * @param int[] $creationmethods
       * @param bool $manage
       * @throws coding_exception
       */
 -    public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
 +    public function __construct($userid = 0, $statuses = [], $types = [], $creationmethods = [], $manage = false) {
          parent::__construct('data-requests-table');
  
          $this->userid = $userid;
          $this->statuses = $statuses;
          $this->types = $types;
 +        $this->creationmethods = $creationmethods;
          $this->manage = $manage;
  
          $checkboxattrs = [
                  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);
          $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());
 +        $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types,
 +                $this->creationmethods, $sort, $this->get_page_start(), $this->get_page_size());
  
          // Count data requests from the given conditions.
 -        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
 +        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types,
 +                $this->creationmethods);
          $this->pagesize($pagesize, $total);
  
          $this->rawdata = [];
          $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);
@@@ -32,8 -32,6 +32,8 @@@ $string['addnewdefaults'] = 'Add a new 
  $string['addpurpose'] = 'Add purpose';
  $string['approve'] = 'Approve';
  $string['approverequest'] = 'Approve request';
 +$string['automaticdeletionrequests'] = 'Create automatic data deletion requests';
 +$string['automaticdeletionrequests_desc'] = 'If enabled, automatic delete data request will be created upon user deletion or for each existing deleted user which data was not fully deleted.';
  $string['bulkapproverequests'] = 'Approve requests';
  $string['bulkdenyrequests'] = 'Deny requests';
  $string['cachedef_purpose'] = 'Data purposes';
@@@ -41,6 -39,7 +41,7 @@@ $string['cachedef_purpose_overrides'] 
  $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.';
@@@ -57,6 -56,7 +58,7 @@@ $string['confirmcompletion'] = 'Do you 
  $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';
@@@ -70,8 -70,6 +72,8 @@@ $string['contactdpoviaprivacypolicy'] 
  $string['createcategory'] = 'Create data category';
  $string['createnewdatarequest'] = 'Create a new data request';
  $string['createpurpose'] = 'Create data purpose';
 +$string['creationauto'] = 'Automatically';
 +$string['creationmanual'] = 'Manually';
  $string['datadeletion'] = 'Data deletion';
  $string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
  $string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
@@@ -86,7 -84,6 +88,7 @@@ $string['dataretentionsummary'] = 'Dat
  $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
  $string['datarequestcreatedfromscheduledtask'] = 'Automatically created from a scheduled task (pre-existing deleted user).';
  $string['datarequestemailsubject'] = 'Data request: {$a}';
 +$string['datarequestcreatedupondelete'] = 'Automatically created upon user deletion.';
  $string['datarequests'] = 'Data requests';
  $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
  $string['daterequested'] = 'Date requested';
@@@ -122,7 -119,6 +124,7 @@@ $string['editpurposes'] = 'Edit purpose
  $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
  $string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)';
  $string['emailsalutation'] = 'Dear {$a},';
 +$string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
  $string['errorinvalidrequeststatus'] = 'Invalid request status!';
  $string['errorinvalidrequesttype'] = 'Invalid request type!';
  $string['errornocapabilitytorequestforothers'] = 'User {$a->requestedby} doesn\'t have the capability to make a data request on behalf of user {$a->userid}';
@@@ -241,7 -237,6 +243,7 @@@ $string['requestby'] = 'Requested by'
  $string['requestbydetail'] = 'Requested by:';
  $string['requestcomments'] = 'Comments';
  $string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
 +$string['requestcreation'] = 'Creation';
  $string['requestdenied'] = 'The request has been denied';
  $string['requestemailintro'] = 'You have received a data request:';
  $string['requestfor'] = 'User';
@@@ -271,6 -266,9 +273,9 @@@ When checking the active enrolment in 
  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';