Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Defines {@link \mod_workshop\privacy\provider} class.
  19   *
  20   * @package     mod_workshop
  21   * @category    privacy
  22   * @copyright   2018 David Mudrák <david@moodle.com>
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_workshop\privacy;
  27  
  28  use core_privacy\local\metadata\collection;
  29  use core_privacy\local\request\approved_contextlist;
  30  use core_privacy\local\request\approved_userlist;
  31  use core_privacy\local\request\contextlist;
  32  use core_privacy\local\request\deletion_criteria;
  33  use core_privacy\local\request\helper;
  34  use core_privacy\local\request\transform;
  35  use core_privacy\local\request\userlist;
  36  use core_privacy\local\request\writer;
  37  
  38  defined('MOODLE_INTERNAL') || die();
  39  
  40  require_once($CFG->dirroot.'/mod/workshop/locallib.php');
  41  
  42  /**
  43   * Privacy API implementation for the Workshop activity module.
  44   *
  45   * @copyright 2018 David Mudrák <david@moodle.com>
  46   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class provider implements
  49          \core_privacy\local\metadata\provider,
  50          \core_privacy\local\request\core_userlist_provider,
  51          \core_privacy\local\request\user_preference_provider,
  52          \core_privacy\local\request\plugin\provider {
  53  
  54      /**
  55       * Describe all the places where the Workshop module stores some personal data.
  56       *
  57       * @param collection $collection Collection of items to add metadata to.
  58       * @return collection Collection with our added items.
  59       */
  60      public static function get_metadata(collection $collection) : collection {
  61  
  62          $collection->add_database_table('workshop_submissions', [
  63              'workshopid' => 'privacy:metadata:workshopid',
  64              'authorid' => 'privacy:metadata:authorid',
  65              'example' => 'privacy:metadata:example',
  66              'timecreated' => 'privacy:metadata:timecreated',
  67              'timemodified' => 'privacy:metadata:timemodified',
  68              'title' => 'privacy:metadata:submissiontitle',
  69              'content' => 'privacy:metadata:submissioncontent',
  70              'contentformat' => 'privacy:metadata:submissioncontentformat',
  71              'grade' => 'privacy:metadata:submissiongrade',
  72              'gradeover' => 'privacy:metadata:submissiongradeover',
  73              'feedbackauthor' => 'privacy:metadata:feedbackauthor',
  74              'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat',
  75              'published' => 'privacy:metadata:published',
  76              'late' => 'privacy:metadata:late',
  77          ], 'privacy:metadata:workshopsubmissions');
  78  
  79          $collection->add_database_table('workshop_assessments', [
  80              'submissionid' => 'privacy:metadata:submissionid',
  81              'reviewerid' => 'privacy:metadata:reviewerid',
  82              'weight' => 'privacy:metadata:weight',
  83              'timecreated' => 'privacy:metadata:timecreated',
  84              'timemodified' => 'privacy:metadata:timemodified',
  85              'grade' => 'privacy:metadata:assessmentgrade',
  86              'gradinggrade' => 'privacy:metadata:assessmentgradinggrade',
  87              'gradinggradeover' => 'privacy:metadata:assessmentgradinggradeover',
  88              'feedbackauthor' => 'privacy:metadata:feedbackauthor',
  89              'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat',
  90              'feedbackreviewer' => 'privacy:metadata:feedbackreviewer',
  91              'feedbackreviewerformat' => 'privacy:metadata:feedbackreviewerformat',
  92          ], 'privacy:metadata:workshopassessments');
  93  
  94          $collection->add_database_table('workshop_grades', [
  95              'assessmentid' => 'privacy:metadata:assessmentid',
  96              'strategy' => 'privacy:metadata:strategy',
  97              'dimensionid' => 'privacy:metadata:dimensionid',
  98              'grade' => 'privacy:metadata:dimensiongrade',
  99              'peercomment' => 'privacy:metadata:peercomment',
 100              'peercommentformat' => 'privacy:metadata:peercommentformat',
 101          ], 'privacy:metadata:workshopgrades');
 102  
 103          $collection->add_database_table('workshop_aggregations', [
 104              'workshopid' => 'privacy:metadata:workshopid',
 105              'userid' => 'privacy:metadata:userid',
 106              'gradinggrade' => 'privacy:metadata:aggregatedgradinggrade',
 107              'timegraded' => 'privacy:metadata:timeaggregated',
 108          ], 'privacy:metadata:workshopaggregations');
 109  
 110          $collection->add_subsystem_link('core_files', [], 'privacy:metadata:subsystem:corefiles');
 111          $collection->add_subsystem_link('core_plagiarism', [], 'privacy:metadata:subsystem:coreplagiarism');
 112  
 113          $userprefs = self::get_user_prefs();
 114          foreach ($userprefs as $userpref) {
 115              if ($userpref === 'workshop_perpage') {
 116                  $collection->add_user_preference('workshop_perpage', 'privacy:metadata:preference:perpage');
 117              } else {
 118                  $summary = str_replace('workshop-', '', $userpref);
 119                  $collection->add_user_preference($userpref, "privacy:metadata:preference:$summary");
 120              }
 121          }
 122  
 123          return $collection;
 124      }
 125  
 126      /**
 127       * Get the list of contexts that contain personal data for the specified user.
 128       *
 129       * User has personal data in the workshop if any of the following cases happens:
 130       *
 131       * - the user has submitted in the workshop
 132       * - the user has overridden a submission grade
 133       * - the user has been assigned as a reviewer of a submission
 134       * - the user has overridden a grading grade
 135       * - the user has a grading grade (existing or to be calculated)
 136       *
 137       * @param int $userid ID of the user.
 138       * @return contextlist List of contexts containing the user's personal data.
 139       */
 140      public static function get_contexts_for_userid(int $userid) : contextlist {
 141  
 142          $contextlist = new contextlist();
 143          $sql = "SELECT ctx.id
 144                    FROM {course_modules} cm
 145                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 146                    JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id
 147                    JOIN {workshop} w ON cm.instance = w.id
 148               LEFT JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 149               LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND (
 150                      wa.reviewerid = :wareviewerid
 151                          OR
 152                      wa.gradinggradeoverby = :wagradinggradeoverby
 153                  )
 154               LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id AND wr.userid = :wruserid
 155                   WHERE ws.authorid = :wsauthorid
 156                      OR ws.gradeoverby = :wsgradeoverby
 157                      OR wa.id IS NOT NULL
 158                      OR wr.id IS NOT NULL";
 159  
 160          $params = [
 161              'module' => 'workshop',
 162              'contextlevel' => CONTEXT_MODULE,
 163              'wsauthorid' => $userid,
 164              'wsgradeoverby' => $userid,
 165              'wareviewerid' => $userid,
 166              'wagradinggradeoverby' => $userid,
 167              'wruserid' => $userid,
 168          ];
 169  
 170          $contextlist->add_from_sql($sql, $params);
 171  
 172          return $contextlist;
 173      }
 174  
 175      /**
 176       * Get the list of users within a specific context.
 177       *
 178       * @param userlist $userlist To be filled list of users who have data in this context/plugin combination.
 179       */
 180      public static function get_users_in_context(userlist $userlist) {
 181          global $DB;
 182  
 183          $context = $userlist->get_context();
 184  
 185          if (!$context instanceof \context_module) {
 186              return;
 187          }
 188  
 189          $params = [
 190              'instanceid' => $context->instanceid,
 191              'module' => 'workshop',
 192          ];
 193  
 194          // One query to fetch them all, one query to find them, one query to bring them all and into the userlist add them.
 195          $sql = "SELECT ws.authorid, ws.gradeoverby, wa.reviewerid, wa.gradinggradeoverby, wr.userid
 196                    FROM {course_modules} cm
 197                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 198                    JOIN {workshop} w ON cm.instance = w.id
 199                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 200               LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id
 201               LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id
 202                   WHERE cm.id = :instanceid";
 203  
 204          $userids = [];
 205          $rs = $DB->get_recordset_sql($sql, $params);
 206  
 207          foreach ($rs as $r) {
 208              if ($r->authorid) {
 209                  $userids[$r->authorid] = true;
 210              }
 211              if ($r->gradeoverby) {
 212                  $userids[$r->gradeoverby] = true;
 213              }
 214              if ($r->reviewerid) {
 215                  $userids[$r->reviewerid] = true;
 216              }
 217              if ($r->gradinggradeoverby) {
 218                  $userids[$r->gradinggradeoverby] = true;
 219              }
 220              if ($r->userid) {
 221                  $userids[$r->userid] = true;
 222              }
 223          }
 224  
 225          $rs->close();
 226  
 227          if ($userids) {
 228              $userlist->add_users(array_keys($userids));
 229          }
 230      }
 231  
 232      /**
 233       * Export personal data stored in the given contexts.
 234       *
 235       * @param approved_contextlist $contextlist List of contexts approved for export.
 236       */
 237      public static function export_user_data(approved_contextlist $contextlist) {
 238          global $DB;
 239  
 240          if (!count($contextlist)) {
 241              return;
 242          }
 243  
 244          $user = $contextlist->get_user();
 245  
 246          // Export general information about all workshops.
 247          foreach ($contextlist->get_contexts() as $context) {
 248              if ($context->contextlevel != CONTEXT_MODULE) {
 249                  continue;
 250              }
 251              $data = helper::get_context_data($context, $user);
 252              static::append_extra_workshop_data($context, $user, $data, []);
 253              writer::with_context($context)->export_data([], $data);
 254              helper::export_context_files($context, $user);
 255          }
 256  
 257          // Export the user's own submission and all example submissions he/she created.
 258          static::export_submissions($contextlist);
 259  
 260          // Export all given assessments.
 261          static::export_assessments($contextlist);
 262      }
 263  
 264      /**
 265       * Export user preferences controlled by this plugin.
 266       *
 267       * @param int $userid ID of the user we are exporting data for
 268       */
 269      public static function export_user_preferences(int $userid) {
 270          $userprefs = self::get_user_prefs();
 271          $expandstr = get_string('expand');
 272          $collapsestr = get_string('collapse');
 273          foreach ($userprefs as $userpref) {
 274              $userprefval = get_user_preferences($userpref, null, $userid);
 275              if ($userprefval !== null) {
 276                  $langid = str_replace('workshop-', '', $userpref);
 277                  $description = get_string("privacy:metadata:preference:$langid", 'mod_workshop');
 278                  if ($userpref === 'workshop_perpage') {
 279                      writer::export_user_preference('mod_workshop', $userpref, $userprefval,
 280                              get_string('privacy:metadata:preference:perpage', 'mod_workshop'));
 281                  } else {
 282                      writer::export_user_preference('mod_workshop', $userpref,
 283                          $userprefval == 1 ? $collapsestr : $expandstr, $description);
 284                  }
 285              }
 286          }
 287      }
 288  
 289      /**
 290       * Append additional relevant data into the base data about the workshop instance.
 291       *
 292       * Relevant are data that are important for interpreting or evaluating the performance of the user expressed in
 293       * his/her exported personal data. For example, we need to know what were the instructions for submissions or what
 294       * was the phase of the workshop when it was exported.
 295       *
 296       * @param \context $context Workshop module content.
 297       * @param stdClass $user User for which we are exporting data.
 298       * @param stdClass $data Base data about the workshop instance to append to.
 299       * @param array $subcontext Subcontext path items to eventually write files into.
 300       */
 301      protected static function append_extra_workshop_data(\context $context, \stdClass $user, \stdClass $data, array $subcontext) {
 302          global $DB;
 303  
 304          if ($context->contextlevel != CONTEXT_MODULE) {
 305              throw new \coding_exception('Unexpected context provided');
 306          }
 307  
 308          $sql = "SELECT w.instructauthors, w.instructauthorsformat, w.instructreviewers, w.instructreviewersformat, w.phase,
 309                         w.strategy, w.evaluation, w.latesubmissions, w.submissionstart, w.submissionend, w.assessmentstart,
 310                         w.assessmentend, w.conclusion, w.conclusionformat
 311                    FROM {course_modules} cm
 312                    JOIN {workshop} w ON cm.instance = w.id
 313                   WHERE cm.id = :cmid";
 314  
 315          $params = [
 316              'cmid' => $context->instanceid,
 317          ];
 318  
 319          $record = $DB->get_record_sql($sql, $params, MUST_EXIST);
 320          $writer = writer::with_context($context);
 321  
 322          if ($record->phase >= \workshop::PHASE_SUBMISSION) {
 323              $data->instructauthors = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructauthors', 0,
 324                  $record->instructauthors);
 325              $data->instructauthorsformat = $record->instructauthorsformat;
 326          }
 327  
 328          if ($record->phase >= \workshop::PHASE_ASSESSMENT) {
 329              $data->instructreviewers = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructreviewers', 0,
 330                  $record->instructreviewers);
 331              $data->instructreviewersformat = $record->instructreviewersformat;
 332          }
 333  
 334          if ($record->phase >= \workshop::PHASE_CLOSED) {
 335              $data->conclusion = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'conclusion', 0, $record->conclusion);
 336              $data->conclusionformat = $record->conclusionformat;
 337          }
 338  
 339          $data->strategy = \workshop::available_strategies_list()[$record->strategy];
 340          $data->evaluation = \workshop::available_evaluators_list()[$record->evaluation];
 341          $data->latesubmissions = transform::yesno($record->latesubmissions);
 342          $data->submissionstart = $record->submissionstart ? transform::datetime($record->submissionstart) : null;
 343          $data->submissionend = $record->submissionend ? transform::datetime($record->submissionend) : null;
 344          $data->assessmentstart = $record->assessmentstart ? transform::datetime($record->assessmentstart) : null;
 345          $data->assessmentend = $record->assessmentend ? transform::datetime($record->assessmentend) : null;
 346  
 347          switch ($record->phase) {
 348              case \workshop::PHASE_SETUP:
 349                  $data->phase = get_string('phasesetup', 'mod_workshop');
 350                  break;
 351              case \workshop::PHASE_SUBMISSION:
 352                  $data->phase = get_string('phasesubmission', 'mod_workshop');
 353                  break;
 354              case \workshop::PHASE_ASSESSMENT:
 355                  $data->phase = get_string('phaseassessment', 'mod_workshop');
 356                  break;
 357              case \workshop::PHASE_EVALUATION:
 358                  $data->phase = get_string('phaseevaluation', 'mod_workshop');
 359                  break;
 360              case \workshop::PHASE_CLOSED:
 361                  $data->phase = get_string('phaseclosed', 'mod_workshop');
 362                  break;
 363          }
 364  
 365          $writer->export_area_files($subcontext, 'mod_workshop', 'instructauthors', 0);
 366          $writer->export_area_files($subcontext, 'mod_workshop', 'instructreviewers', 0);
 367          $writer->export_area_files($subcontext, 'mod_workshop', 'conclusion', 0);
 368      }
 369  
 370      /**
 371       * Export all user's submissions and example submissions he/she created in the given contexts.
 372       *
 373       * @param approved_contextlist $contextlist List of contexts approved for export.
 374       */
 375      protected static function export_submissions(approved_contextlist $contextlist) {
 376          global $DB;
 377  
 378          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 379          $user = $contextlist->get_user();
 380  
 381          $sql = "SELECT ws.id, ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title,
 382                         ws.content, ws.contentformat, ws.grade, ws.gradeover, ws.feedbackauthor, ws.feedbackauthorformat,
 383                         ws.published, ws.late,
 384                         w.phase, w.course, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')."
 385                    FROM {course_modules} cm
 386                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 387                    JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id
 388                    JOIN {workshop} w ON cm.instance = w.id
 389                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid
 390                   WHERE ctx.id {$contextsql}";
 391  
 392          $params = $contextparams + [
 393              'module' => 'workshop',
 394              'contextlevel' => CONTEXT_MODULE,
 395              'authorid' => $user->id,
 396          ];
 397  
 398          $rs = $DB->get_recordset_sql($sql, $params);
 399  
 400          foreach ($rs as $record) {
 401              \context_helper::preload_from_record($record);
 402              $context = \context_module::instance($record->cmid);
 403              $writer = \core_privacy\local\request\writer::with_context($context);
 404  
 405              if ($record->example) {
 406                  $subcontext = [get_string('examplesubmissions', 'mod_workshop'), $record->id];
 407                  $mysubmission = null;
 408              } else {
 409                  $subcontext = [get_string('mysubmission', 'mod_workshop')];
 410                  $mysubmission = $record;
 411              }
 412  
 413              $phase = $record->phase;
 414              $courseid = $record->course;
 415  
 416              $data = (object) [
 417                  'example' => transform::yesno($record->example),
 418                  'timecreated' => transform::datetime($record->timecreated),
 419                  'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null,
 420                  'title' => $record->title,
 421                  'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop',
 422                      'submission_content', $record->id, $record->content),
 423                  'contentformat' => $record->contentformat,
 424                  'grade' => $record->grade,
 425                  'gradeover' => $record->gradeover,
 426                  'feedbackauthor' => $record->feedbackauthor,
 427                  'feedbackauthorformat' => $record->feedbackauthorformat,
 428                  'published' => transform::yesno($record->published),
 429                  'late' => transform::yesno($record->late),
 430              ];
 431  
 432              $writer->export_data($subcontext, $data);
 433              $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->id);
 434              $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->id);
 435  
 436              // Export peer-assessments of my submission if the workshop was closed. We do not export received
 437              // assessments from peers before they were actually effective. Before the workshop is closed, grades are not
 438              // pushed into the gradebook. So peer assessments did not affect evaluation of the user's performance and
 439              // they should not be considered as their personal data. This is different from assessments given by the
 440              // user that are always exported.
 441              if ($mysubmission && $phase == \workshop::PHASE_CLOSED) {
 442                  $assessments = $DB->get_records('workshop_assessments', ['submissionid' => $mysubmission->id], '',
 443                      'id, reviewerid, weight, timecreated, timemodified, grade, feedbackauthor, feedbackauthorformat');
 444  
 445                  foreach ($assessments as $assessment) {
 446                      $assid = $assessment->id;
 447                      $assessment->selfassessment = transform::yesno($assessment->reviewerid == $user->id);
 448                      $assessment->timecreated = transform::datetime($assessment->timecreated);
 449                      $assessment->timemodified = $assessment->timemodified ? transform::datetime($assessment->timemodified) : null;
 450                      $assessment->feedbackauthor = $writer->rewrite_pluginfile_urls($subcontext,
 451                          'mod_workshop', 'overallfeedback_content', $assid, $assessment->feedbackauthor);
 452  
 453                      $assessmentsubcontext = array_merge($subcontext, [get_string('assessments', 'mod_workshop'), $assid]);
 454  
 455                      unset($assessment->id);
 456                      unset($assessment->reviewerid);
 457  
 458                      $writer->export_data($assessmentsubcontext, $assessment);
 459                      $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_content', $assid);
 460                      $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_attachment', $assid);
 461  
 462                      // Export details of how the assessment forms were filled.
 463                      static::export_assessment_forms($user, $context, $assessmentsubcontext, $assid);
 464                  }
 465              }
 466  
 467              // Export plagiarism data related to the submission content.
 468              // The last $linkarray argument consistent with how we call {@link plagiarism_get_links()} in the renderer.
 469              \core_plagiarism\privacy\provider::export_plagiarism_user_data($user->id, $context, $subcontext, [
 470                  'userid' => $user->id,
 471                  'content' => format_text($data->content, $data->contentformat, ['overflowdiv' => true]),
 472                  'cmid' => $context->instanceid,
 473                  'course' => $courseid,
 474              ]);
 475          }
 476  
 477          $rs->close();
 478      }
 479  
 480      /**
 481       * Export all assessments given by the user.
 482       *
 483       * @param approved_contextlist $contextlist List of contexts approved for export.
 484       */
 485      protected static function export_assessments(approved_contextlist $contextlist) {
 486          global $DB;
 487  
 488          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 489          $user = $contextlist->get_user();
 490  
 491          $sql = "SELECT ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title, ws.content, ws.contentformat,
 492                         wa.id, wa.submissionid, wa.reviewerid, wa.weight, wa.timecreated, wa.timemodified, wa.grade,
 493                         wa.gradinggrade, wa.gradinggradeover, wa.feedbackauthor, wa.feedbackauthorformat, wa.feedbackreviewer,
 494                         wa.feedbackreviewerformat, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')."
 495                    FROM {course_modules} cm
 496                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 497                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 498                    JOIN {workshop} w ON cm.instance = w.id
 499                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 500                    JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND wa.reviewerid = :reviewerid
 501                   WHERE ctx.id {$contextsql}";
 502  
 503          $params = $contextparams + [
 504              'module' => 'workshop',
 505              'contextlevel' => CONTEXT_MODULE,
 506              'reviewerid' => $user->id,
 507          ];
 508  
 509          $rs = $DB->get_recordset_sql($sql, $params);
 510  
 511          foreach ($rs as $record) {
 512              \context_helper::preload_from_record($record);
 513              $context = \context_module::instance($record->cmid);
 514              $writer = \core_privacy\local\request\writer::with_context($context);
 515              $subcontext = [get_string('myassessments', 'mod_workshop'), $record->id];
 516  
 517              $data = (object) [
 518                  'weight' => $record->weight,
 519                  'timecreated' => transform::datetime($record->timecreated),
 520                  'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null,
 521                  'grade' => $record->grade,
 522                  'gradinggrade' => $record->gradinggrade,
 523                  'gradinggradeover' => $record->gradinggradeover,
 524                  'feedbackauthor' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop',
 525                      'overallfeedback_content', $record->id, $record->feedbackauthor),
 526                  'feedbackauthorformat' => $record->feedbackauthorformat,
 527                  'feedbackreviewer' => $record->feedbackreviewer,
 528                  'feedbackreviewerformat' => $record->feedbackreviewerformat,
 529              ];
 530  
 531              $submission = (object) [
 532                  'myownsubmission' => transform::yesno($record->authorid == $user->id),
 533                  'example' => transform::yesno($record->example),
 534                  'timecreated' => transform::datetime($record->timecreated),
 535                  'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null,
 536                  'title' => $record->title,
 537                  'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop',
 538                      'submission_content', $record->submissionid, $record->content),
 539                  'contentformat' => $record->contentformat,
 540              ];
 541  
 542              $writer->export_data($subcontext, $data);
 543              $writer->export_related_data($subcontext, 'submission', $submission);
 544              $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_content', $record->id);
 545              $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_attachment', $record->id);
 546              $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->submissionid);
 547              $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->submissionid);
 548  
 549              // Export details of how the assessment forms were filled.
 550              static::export_assessment_forms($user, $context, $subcontext, $record->id);
 551          }
 552  
 553          $rs->close();
 554      }
 555  
 556      /**
 557       * Export the grading strategy data related to the particular assessment.
 558       *
 559       * @param stdClass $user User we are exporting for
 560       * @param context $context Workshop activity content
 561       * @param array $subcontext Subcontext path of the assessment
 562       * @param int $assessmentid ID of the exported assessment
 563       */
 564      protected static function export_assessment_forms(\stdClass $user, \context $context, array $subcontext, int $assessmentid) {
 565  
 566          foreach (\workshop::available_strategies_list() as $strategy => $title) {
 567              $providername = '\workshopform_'.$strategy.'\privacy\provider';
 568  
 569              if (is_subclass_of($providername, '\mod_workshop\privacy\workshopform_provider')) {
 570                  component_class_callback($providername, 'export_assessment_form',
 571                      [
 572                          $user,
 573                          $context,
 574                          array_merge($subcontext, [get_string('assessmentform', 'mod_workshop'), $title]),
 575                          $assessmentid,
 576                      ]
 577                  );
 578  
 579              } else {
 580                  debugging('Missing class '.$providername.' implementing workshopform_provider interface', DEBUG_DEVELOPER);
 581              }
 582          }
 583      }
 584  
 585      /**
 586       * Delete personal data for all users in the context.
 587       *
 588       * @param context $context Context to delete personal data from.
 589       */
 590      public static function delete_data_for_all_users_in_context(\context $context) {
 591          global $CFG, $DB;
 592          require_once($CFG->libdir.'/gradelib.php');
 593  
 594          if ($context->contextlevel != CONTEXT_MODULE) {
 595              return;
 596          }
 597  
 598          $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING);
 599  
 600          if (!$cm) {
 601              // Probably some kind of expired context.
 602              return;
 603          }
 604  
 605          $workshop = $DB->get_record('workshop', ['id' => $cm->instance], 'id, course', MUST_EXIST);
 606  
 607          $submissions = $DB->get_records('workshop_submissions', ['workshopid' => $workshop->id], '', 'id');
 608          $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($submissions), '', 'id');
 609  
 610          $DB->delete_records('workshop_aggregations', ['workshopid' => $workshop->id]);
 611          $DB->delete_records_list('workshop_grades', 'assessmentid', array_keys($assessments));
 612          $DB->delete_records_list('workshop_assessments', 'id', array_keys($assessments));
 613          $DB->delete_records_list('workshop_submissions', 'id', array_keys($submissions));
 614  
 615          $fs = get_file_storage();
 616          $fs->delete_area_files($context->id, 'mod_workshop', 'submission_content');
 617          $fs->delete_area_files($context->id, 'mod_workshop', 'submission_attachment');
 618          $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_content');
 619          $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_attachment');
 620  
 621          grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 0, null, ['reset' => true]);
 622          grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 1, null, ['reset' => true]);
 623  
 624          \core_plagiarism\privacy\provider::delete_plagiarism_for_context($context);
 625      }
 626  
 627      /**
 628       * Delete personal data for the user in a list of contexts.
 629       *
 630       * Removing assessments of submissions from the Workshop is not trivial. Removing one user's data can easily affect
 631       * other users' grades and completion criteria. So we replace the non-essential contents with a "deleted" message,
 632       * but keep the actual info in place. The argument is that one's right for privacy should not overweight others'
 633       * right for accessing their own personal data and be evaluated on their basis.
 634       *
 635       * @param approved_contextlist $contextlist List of contexts to delete data from.
 636       */
 637      public static function delete_data_for_user(approved_contextlist $contextlist) {
 638          global $DB;
 639  
 640          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 641          $user = $contextlist->get_user();
 642          $fs = get_file_storage();
 643  
 644          // Replace sensitive data in all submissions by the user in the given contexts.
 645  
 646          $sql = "SELECT ws.id AS submissionid
 647                    FROM {course_modules} cm
 648                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 649                    JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id
 650                    JOIN {workshop} w ON cm.instance = w.id
 651                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid
 652                   WHERE ctx.id {$contextsql}";
 653  
 654          $params = $contextparams + [
 655              'module' => 'workshop',
 656              'contextlevel' => CONTEXT_MODULE,
 657              'authorid' => $user->id,
 658          ];
 659  
 660          $submissionids = $DB->get_fieldset_sql($sql, $params);
 661  
 662          if ($submissionids) {
 663              list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED);
 664  
 665              $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title',
 666                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 667              $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content',
 668                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 669              $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content',
 670                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 671  
 672              foreach ($contextlist->get_contextids() as $contextid) {
 673                  $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_content',
 674                      $submissionidsql, $submissionidparams);
 675                  $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_attachment',
 676                      $submissionidsql, $submissionidparams);
 677              }
 678          }
 679  
 680          // Replace personal data in received assessments - feedback is seen as belonging to the recipient.
 681  
 682          $sql = "SELECT wa.id AS assessmentid
 683                    FROM {course_modules} cm
 684                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 685                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 686                    JOIN {workshop} w ON cm.instance = w.id
 687                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid
 688                    JOIN {workshop_assessments} wa ON wa.submissionid = ws.id
 689                   WHERE ctx.id {$contextsql}";
 690  
 691          $params = $contextparams + [
 692              'module' => 'workshop',
 693              'contextlevel' => CONTEXT_MODULE,
 694              'authorid' => $user->id,
 695          ];
 696  
 697          $assessmentids = $DB->get_fieldset_sql($sql, $params);
 698  
 699          if ($assessmentids) {
 700              list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED);
 701  
 702              $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content',
 703                  'mod_workshop'), "id $assessmentidsql", $assessmentidparams);
 704  
 705              foreach ($contextlist->get_contextids() as $contextid) {
 706                  $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_content',
 707                      $assessmentidsql, $assessmentidparams);
 708                  $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_attachment',
 709                      $assessmentidsql, $assessmentidparams);
 710              }
 711          }
 712  
 713          // Replace sensitive data in provided assessments records.
 714  
 715          $sql = "SELECT wa.id AS assessmentid
 716                    FROM {course_modules} cm
 717                    JOIN {modules} m ON cm.module = m.id AND m.name = :module
 718                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 719                    JOIN {workshop} w ON cm.instance = w.id
 720                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 721                    JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND wa.reviewerid = :reviewerid
 722                   WHERE ctx.id {$contextsql}";
 723  
 724          $params = $contextparams + [
 725              'module' => 'workshop',
 726              'contextlevel' => CONTEXT_MODULE,
 727              'reviewerid' => $user->id,
 728          ];
 729  
 730          $assessmentids = $DB->get_fieldset_sql($sql, $params);
 731  
 732          if ($assessmentids) {
 733              list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED);
 734  
 735              $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content',
 736                  'mod_workshop'), "id $assessmentidsql", $assessmentidparams);
 737          }
 738  
 739          foreach ($contextlist as $context) {
 740              \core_plagiarism\privacy\provider::delete_plagiarism_for_user($user->id, $context);
 741          }
 742      }
 743  
 744      /**
 745       * Delete personal data for multiple users within a single workshop context.
 746       *
 747       * See documentation for {@link self::delete_data_for_user()} for more details on what we do and don't actually
 748       * delete and why.
 749       *
 750       * @param approved_userlist $userlist The approved context and user information to delete information for.
 751       */
 752      public static function delete_data_for_users(approved_userlist $userlist) {
 753          global $DB;
 754  
 755          $context = $userlist->get_context();
 756          $fs = get_file_storage();
 757  
 758          if ($context->contextlevel != CONTEXT_MODULE) {
 759              // This should not happen but let's be double sure when it comes to deleting data.
 760              return;
 761          }
 762  
 763          $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING);
 764  
 765          if (!$cm) {
 766              // Probably some kind of expired context.
 767              return;
 768          }
 769  
 770          $userids = $userlist->get_userids();
 771  
 772          if (!$userids) {
 773              return;
 774          }
 775  
 776          list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 777  
 778          // Erase sensitive data in all submissions by all the users in the given context.
 779  
 780          $sql = "SELECT ws.id AS submissionid
 781                    FROM {workshop} w
 782                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 783                   WHERE w.id = :workshopid AND ws.authorid $usersql";
 784  
 785          $params = $userparams + [
 786              'workshopid' => $cm->instance,
 787          ];
 788  
 789          $submissionids = $DB->get_fieldset_sql($sql, $params);
 790  
 791          if ($submissionids) {
 792              list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED);
 793  
 794              $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title',
 795                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 796              $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content',
 797                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 798              $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content',
 799                  'mod_workshop'), "id $submissionidsql", $submissionidparams);
 800  
 801              $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_content',
 802                  $submissionidsql, $submissionidparams);
 803              $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_attachment',
 804                  $submissionidsql, $submissionidparams);
 805          }
 806  
 807          // Erase personal data in received assessments - feedback is seen as belonging to the recipient.
 808  
 809          $sql = "SELECT wa.id AS assessmentid
 810                    FROM {workshop} w
 811                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 812                    JOIN {workshop_assessments} wa ON wa.submissionid = ws.id
 813                   WHERE w.id = :workshopid AND ws.authorid $usersql";
 814  
 815          $params = $userparams + [
 816              'workshopid' => $cm->instance,
 817          ];
 818  
 819          $assessmentids = $DB->get_fieldset_sql($sql, $params);
 820  
 821          if ($assessmentids) {
 822              list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED);
 823  
 824              $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content',
 825                  'mod_workshop'), "id $assessmentidsql", $assessmentidparams);
 826  
 827              $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_content',
 828                  $assessmentidsql, $assessmentidparams);
 829              $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_attachment',
 830                  $assessmentidsql, $assessmentidparams);
 831          }
 832  
 833          // Erase sensitive data in provided assessments records.
 834  
 835          $sql = "SELECT wa.id AS assessmentid
 836                    FROM {workshop} w
 837                    JOIN {workshop_submissions} ws ON ws.workshopid = w.id
 838                    JOIN {workshop_assessments} wa ON wa.submissionid = ws.id
 839                   WHERE w.id = :workshopid AND wa.reviewerid $usersql";
 840  
 841          $params = $userparams + [
 842              'workshopid' => $cm->instance,
 843          ];
 844  
 845          $assessmentids = $DB->get_fieldset_sql($sql, $params);
 846  
 847          if ($assessmentids) {
 848              list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED);
 849  
 850              $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content',
 851                  'mod_workshop'), "id $assessmentidsql", $assessmentidparams);
 852          }
 853  
 854          foreach ($userids as $userid) {
 855              \core_plagiarism\privacy\provider::delete_plagiarism_for_user($userid, $context);
 856          }
 857      }
 858  
 859      /**
 860       * Get the user preferences.
 861       *
 862       * @return array List of user preferences
 863       */
 864      protected static function get_user_prefs(): array {
 865          return [
 866              'workshop_perpage',
 867              'workshop-viewlet-allexamples-collapsed',
 868              'workshop-viewlet-allsubmissions-collapsed',
 869              'workshop-viewlet-assessmentform-collapsed',
 870              'workshop-viewlet-assignedassessments-collapsed',
 871              'workshop-viewlet-cleargrades-collapsed',
 872              'workshop-viewlet-conclusion-collapsed',
 873              'workshop-viewlet-examples-collapsed',
 874              'workshop-viewlet-examplesfail-collapsed',
 875              'workshop-viewlet-gradereport-collapsed',
 876              'workshop-viewlet-instructauthors-collapsed',
 877              'workshop-viewlet-instructreviewers-collapsed',
 878              'workshop-viewlet-intro-collapsed',
 879              'workshop-viewlet-overallfeedback-collapsed',
 880              'workshop-viewlet-ownsubmission-collapsed',
 881              'workshop-viewlet-publicsubmissions-collapsed',
 882              'workshop-viewlet-yourgrades-collapsed'
 883          ];
 884      }
 885  }