Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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   * Privacy class for requesting user data.
  19   *
  20   * @package    mod_assign
  21   * @copyright  2018 Adrian Greeve <adrian@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_assign\privacy;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/mod/assign/locallib.php');
  30  
  31  use \core_privacy\local\metadata\collection;
  32  use \core_privacy\local\request\contextlist;
  33  use \core_privacy\local\request\writer;
  34  use \core_privacy\local\request\approved_contextlist;
  35  use \core_privacy\local\request\transform;
  36  use \core_privacy\local\request\helper;
  37  use \core_privacy\local\request\userlist;
  38  use \core_privacy\local\request\approved_userlist;
  39  use \core_privacy\manager;
  40  
  41  /**
  42   * Privacy class for requesting user data.
  43   *
  44   * @package    mod_assign
  45   * @copyright  2018 Adrian Greeve <adrian@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\plugin\provider,
  51          \core_privacy\local\request\user_preference_provider,
  52          \core_privacy\local\request\core_userlist_provider {
  53  
  54      /** Interface for all assign submission sub-plugins. */
  55      const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider';
  56  
  57      /** Interface for all assign submission sub-plugins. This allows for deletion of users with a context. */
  58      const ASSIGNSUBMISSION_USER_INTERFACE = 'mod_assign\privacy\assignsubmission_user_provider';
  59  
  60      /** Interface for all assign feedback sub-plugins. This allows for deletion of users with a context. */
  61      const ASSIGNFEEDBACK_USER_INTERFACE = 'mod_assign\privacy\assignfeedback_user_provider';
  62  
  63      /** Interface for all assign feedback sub-plugins. */
  64      const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider';
  65  
  66      /**
  67       * Provides meta data that is stored about a user with mod_assign
  68       *
  69       * @param  collection $collection A collection of meta data items to be added to.
  70       * @return  collection Returns the collection of metadata.
  71       */
  72      public static function get_metadata(collection $collection) : collection {
  73          $assigngrades = [
  74                  'userid' => 'privacy:metadata:userid',
  75                  'timecreated' => 'privacy:metadata:timecreated',
  76                  'timemodified' => 'timemodified',
  77                  'grader' => 'privacy:metadata:grader',
  78                  'grade' => 'privacy:metadata:grade',
  79                  'attemptnumber' => 'attemptnumber'
  80          ];
  81          $assignoverrides = [
  82                  'groupid' => 'privacy:metadata:groupid',
  83                  'userid' => 'privacy:metadata:userid',
  84                  'allowsubmissionsfromdate' => 'allowsubmissionsfromdate',
  85                  'duedate' => 'duedate',
  86                  'cutoffdate' => 'cutoffdate'
  87          ];
  88          $assignsubmission = [
  89                  'userid' => 'privacy:metadata:userid',
  90                  'timecreated' => 'privacy:metadata:timecreated',
  91                  'timemodified' => 'timemodified',
  92                  'timestarted' => 'privacy:metadata:timestarted',
  93                  'status' => 'gradingstatus',
  94                  'groupid' => 'privacy:metadata:groupid',
  95                  'attemptnumber' => 'attemptnumber',
  96                  'latest' => 'privacy:metadata:latest'
  97          ];
  98          $assignuserflags = [
  99                  'userid' => 'privacy:metadata:userid',
 100                  'assignment' => 'privacy:metadata:assignmentid',
 101                  'locked' => 'locksubmissions',
 102                  'mailed' => 'privacy:metadata:mailed',
 103                  'extensionduedate' => 'extensionduedate',
 104                  'workflowstate' => 'markingworkflowstate',
 105                  'allocatedmarker' => 'allocatedmarker'
 106          ];
 107          $assignusermapping = [
 108                  'assignment' => 'privacy:metadata:assignmentid',
 109                  'userid' => 'privacy:metadata:userid'
 110          ];
 111          $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades');
 112          $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides');
 113          $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail');
 114          $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags');
 115          $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping');
 116          $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage');
 117          $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter');
 118          $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter');
 119          $collection->add_user_preference('assign_workflowfilter', 'privacy:metadata:assignworkflowfilter');
 120          $collection->add_user_preference('assign_quickgrading', 'privacy:metadata:assignquickgrading');
 121          $collection->add_user_preference('assign_downloadasfolders', 'privacy:metadata:assigndownloadasfolders');
 122  
 123          // Link to subplugins.
 124          $collection->add_plugintype_link('assignsubmission', [],'privacy:metadata:assignsubmissionpluginsummary');
 125          $collection->add_plugintype_link('assignfeedback', [], 'privacy:metadata:assignfeedbackpluginsummary');
 126          $collection->add_subsystem_link('core_message', [], 'privacy:metadata:assignmessageexplanation');
 127  
 128          return $collection;
 129      }
 130  
 131      /**
 132       * Returns all of the contexts that has information relating to the userid.
 133       *
 134       * @param  int $userid The user ID.
 135       * @return contextlist an object with the contexts related to a userid.
 136       */
 137      public static function get_contexts_for_userid(int $userid) : contextlist {
 138          $params = ['modulename' => 'assign',
 139                     'contextlevel' => CONTEXT_MODULE,
 140                     'userid' => $userid,
 141                     'graderid' => $userid,
 142                     'aouserid' => $userid,
 143                     'asnuserid' => $userid,
 144                     'aufuserid' => $userid,
 145                     'aumuserid' => $userid];
 146  
 147          $sql = "SELECT ctx.id
 148                    FROM {course_modules} cm
 149                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 150                    JOIN {assign} a ON cm.instance = a.id
 151                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 152                    JOIN {assign_grades} ag ON a.id = ag.assignment AND (ag.userid = :userid OR ag.grader = :graderid)";
 153  
 154          $contextlist = new contextlist();
 155          $contextlist->add_from_sql($sql, $params);
 156  
 157          $sql = "SELECT ctx.id
 158                    FROM {course_modules} cm
 159                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 160                    JOIN {assign} a ON cm.instance = a.id
 161                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 162                    JOIN {assign_overrides} ao ON a.id = ao.assignid
 163                   WHERE ao.userid = :aouserid";
 164  
 165          $contextlist->add_from_sql($sql, $params);
 166  
 167          $sql = "SELECT ctx.id
 168                    FROM {course_modules} cm
 169                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 170                    JOIN {assign} a ON cm.instance = a.id
 171                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 172                    JOIN {assign_submission} asn ON a.id = asn.assignment
 173                   WHERE asn.userid = :asnuserid";
 174  
 175          $contextlist->add_from_sql($sql, $params);
 176  
 177          $sql = "SELECT ctx.id
 178                    FROM {course_modules} cm
 179                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 180                    JOIN {assign} a ON cm.instance = a.id
 181                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 182                    JOIN {assign_user_flags} auf ON a.id = auf.assignment
 183                   WHERE auf.userid = :aufuserid";
 184  
 185          $contextlist->add_from_sql($sql, $params);
 186  
 187          $sql = "SELECT ctx.id
 188                    FROM {course_modules} cm
 189                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 190                    JOIN {assign} a ON cm.instance = a.id
 191                    JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
 192                    JOIN {assign_user_mapping} aum ON a.id = aum.assignment
 193                   WHERE aum.userid = :aumuserid";
 194  
 195          $contextlist->add_from_sql($sql, $params);
 196  
 197          manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
 198                  'get_context_for_userid_within_feedback', [$userid, $contextlist]);
 199          manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
 200                  'get_context_for_userid_within_submission', [$userid, $contextlist]);
 201  
 202          return $contextlist;
 203      }
 204  
 205      /**
 206       * Get the list of users who have data within a context.
 207       *
 208       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 209       */
 210      public static function get_users_in_context(userlist $userlist) {
 211  
 212          $context = $userlist->get_context();
 213          if ($context->contextlevel != CONTEXT_MODULE) {
 214              return;
 215          }
 216  
 217          $params = [
 218              'modulename' => 'assign',
 219              'contextid' => $context->id,
 220              'contextlevel' => CONTEXT_MODULE
 221          ];
 222  
 223          $sql = "SELECT g.userid, g.grader
 224                    FROM {context} ctx
 225                    JOIN {course_modules} cm ON cm.id = ctx.instanceid
 226                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 227                    JOIN {assign} a ON a.id = cm.instance
 228                    JOIN {assign_grades} g ON a.id = g.assignment
 229                   WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
 230          $userlist->add_from_sql('userid', $sql, $params);
 231          $userlist->add_from_sql('grader', $sql, $params);
 232  
 233          $sql = "SELECT o.userid
 234                    FROM {context} ctx
 235                    JOIN {course_modules} cm ON cm.id = ctx.instanceid
 236                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 237                    JOIN {assign} a ON a.id = cm.instance
 238                    JOIN {assign_overrides} o ON a.id = o.assignid
 239                   WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
 240          $userlist->add_from_sql('userid', $sql, $params);
 241  
 242          $sql = "SELECT s.userid
 243                    FROM {context} ctx
 244                    JOIN {course_modules} cm ON cm.id = ctx.instanceid
 245                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 246                    JOIN {assign} a ON a.id = cm.instance
 247                    JOIN {assign_submission} s ON a.id = s.assignment
 248                   WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
 249          $userlist->add_from_sql('userid', $sql, $params);
 250  
 251          $sql = "SELECT uf.userid
 252                    FROM {context} ctx
 253                    JOIN {course_modules} cm ON cm.id = ctx.instanceid
 254                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 255                    JOIN {assign} a ON a.id = cm.instance
 256                    JOIN {assign_user_flags} uf ON a.id = uf.assignment
 257                   WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
 258          $userlist->add_from_sql('userid', $sql, $params);
 259  
 260          $sql = "SELECT um.userid
 261                    FROM {context} ctx
 262                    JOIN {course_modules} cm ON cm.id = ctx.instanceid
 263                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 264                    JOIN {assign} a ON a.id = cm.instance
 265                    JOIN {assign_user_mapping} um ON a.id = um.assignment
 266                   WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
 267          $userlist->add_from_sql('userid', $sql, $params);
 268  
 269          manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE,
 270                  'get_userids_from_context', [$userlist]);
 271          manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE,
 272                  'get_userids_from_context', [$userlist]);
 273      }
 274  
 275      /**
 276       * Write out the user data filtered by contexts.
 277       *
 278       * @param approved_contextlist $contextlist contexts that we are writing data out from.
 279       */
 280      public static function export_user_data(approved_contextlist $contextlist) {
 281          foreach ($contextlist->get_contexts() as $context) {
 282              // Check that the context is a module context.
 283              if ($context->contextlevel != CONTEXT_MODULE) {
 284                  continue;
 285              }
 286              $user = $contextlist->get_user();
 287              $assigndata = helper::get_context_data($context, $user);
 288              helper::export_context_files($context, $user);
 289  
 290              writer::with_context($context)->export_data([], $assigndata);
 291              $assign = new \assign($context, null, null);
 292  
 293              // I need to find out if I'm a student or a teacher.
 294              if ($userids = self::get_graded_users($user->id, $assign)) {
 295                  // Return teacher info.
 296                  $currentpath = [get_string('privacy:studentpath', 'mod_assign')];
 297                  foreach ($userids as $studentuserid) {
 298                      $studentpath = array_merge($currentpath, [$studentuserid->id]);
 299                      static::export_submission($assign, $studentuserid, $context, $studentpath, true);
 300                  }
 301              }
 302  
 303              static::export_overrides($context, $assign, $user);
 304              static::export_submission($assign, $user, $context, []);
 305              // Meta data.
 306              self::store_assign_user_flags($context, $assign, $user->id);
 307              if ($assign->is_blind_marking()) {
 308                  $uniqueid = $assign->get_uniqueid_for_user_static($assign->get_instance()->id, $contextlist->get_user()->id);
 309                  if ($uniqueid) {
 310                      writer::with_context($context)
 311                              ->export_metadata([get_string('blindmarking', 'mod_assign')], 'blindmarkingid', $uniqueid,
 312                                      get_string('privacy:blindmarkingidentifier', 'mod_assign'));
 313                  }
 314              }
 315          }
 316      }
 317  
 318      /**
 319       * Delete all use data which matches the specified context.
 320       *
 321       * @param \context $context The module context.
 322       */
 323      public static function delete_data_for_all_users_in_context(\context $context) {
 324          global $DB;
 325  
 326          if ($context->contextlevel == CONTEXT_MODULE) {
 327              $cm = get_coursemodule_from_id('assign', $context->instanceid);
 328              if ($cm) {
 329                  // Get the assignment related to this context.
 330                  $assign = new \assign($context, null, null);
 331                  // What to do first... Get sub plugins to delete their stuff.
 332                  $requestdata = new assign_plugin_request_data($context, $assign);
 333                  manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
 334                      'delete_submission_for_context', [$requestdata]);
 335                  $requestdata = new assign_plugin_request_data($context, $assign);
 336                  manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
 337                      'delete_feedback_for_context', [$requestdata]);
 338                  $DB->delete_records('assign_grades', ['assignment' => $assign->get_instance()->id]);
 339  
 340                  // Delete advanced grading information.
 341                  $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
 342                  $controller = $gradingmanager->get_active_controller();
 343                  if (isset($controller)) {
 344                      \core_grading\privacy\provider::delete_instance_data($context);
 345                  }
 346  
 347                  // Time to roll my own method for deleting overrides.
 348                  static::delete_overrides_for_users($assign);
 349                  $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]);
 350                  $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]);
 351                  $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]);
 352              }
 353          }
 354      }
 355  
 356      /**
 357       * Delete all user data for the specified user, in the specified contexts.
 358       *
 359       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 360       */
 361      public static function delete_data_for_user(approved_contextlist $contextlist) {
 362          global $DB;
 363  
 364          $user = $contextlist->get_user();
 365  
 366          foreach ($contextlist as $context) {
 367              if ($context->contextlevel != CONTEXT_MODULE) {
 368                  continue;
 369              }
 370              // Get the assign object.
 371              $assign = new \assign($context, null, null);
 372              $assignid = $assign->get_instance()->id;
 373  
 374              $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
 375              foreach ($submissions as $submission) {
 376                  $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user);
 377                  manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
 378                          'delete_submission_for_userid', [$requestdata]);
 379              }
 380  
 381              $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
 382              $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
 383              $controller = $gradingmanager->get_active_controller();
 384              foreach ($grades as $grade) {
 385                  $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user);
 386                  manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
 387                          'delete_feedback_for_grade', [$requestdata]);
 388                  // Delete advanced grading information.
 389                  if (isset($controller)) {
 390                      \core_grading\privacy\provider::delete_instance_data($context, $grade->id);
 391                  }
 392              }
 393  
 394              static::delete_overrides_for_users($assign, [$user->id]);
 395              $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
 396              $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
 397              $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
 398              $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
 399          }
 400      }
 401  
 402      /**
 403       * Delete multiple users within a single context.
 404       *
 405       * @param  approved_userlist $userlist The approved context and user information to delete information for.
 406       */
 407      public static function delete_data_for_users(approved_userlist $userlist) {
 408          global $DB;
 409  
 410          $context = $userlist->get_context();
 411          if ($context->contextlevel != CONTEXT_MODULE) {
 412              return;
 413          }
 414  
 415          $userids = $userlist->get_userids();
 416  
 417          $assign = new \assign($context, null, null);
 418          $assignid = $assign->get_instance()->id;
 419          $requestdata = new assign_plugin_request_data($context, $assign);
 420          $requestdata->set_userids($userids);
 421          $requestdata->populate_submissions_and_grades();
 422          manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE, 'delete_submissions',
 423                  [$requestdata]);
 424          manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE, 'delete_feedback_for_grades',
 425                  [$requestdata]);
 426  
 427          // Update this function to delete advanced grading information.
 428          $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
 429          $controller = $gradingmanager->get_active_controller();
 430          if (isset($controller)) {
 431              $gradeids = $requestdata->get_gradeids();
 432              // Careful here, if no gradeids are provided then all data is deleted for the context.
 433              if (!empty($gradeids)) {
 434                  \core_grading\privacy\provider::delete_data_for_instances($context, $gradeids);
 435              }
 436          }
 437  
 438          static::delete_overrides_for_users($assign, $userids);
 439          list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 440          $params['assignment'] = $assignid;
 441          $DB->delete_records_select('assign_user_flags', "assignment = :assignment AND userid $sql", $params);
 442          $DB->delete_records_select('assign_user_mapping', "assignment = :assignment AND userid $sql", $params);
 443          $DB->delete_records_select('assign_grades', "assignment = :assignment AND userid $sql", $params);
 444          $DB->delete_records_select('assign_submission', "assignment = :assignment AND userid $sql", $params);
 445      }
 446  
 447      /**
 448       * Deletes assignment overrides in bulk
 449       *
 450       * @param  \assign $assign  The assignment object
 451       * @param  array   $userids An array of user IDs
 452       */
 453      protected static function delete_overrides_for_users(\assign $assign, array $userids = []) {
 454          global $DB;
 455          $assignid = $assign->get_instance()->id;
 456  
 457          $usersql = '';
 458          $params = ['assignid' => $assignid];
 459          if (!empty($userids)) {
 460              list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 461              $params = array_merge($params, $userparams);
 462              $overrides = $DB->get_records_select('assign_overrides', "assignid = :assignid AND userid $usersql", $params);
 463          } else {
 464              $overrides = $DB->get_records('assign_overrides', $params);
 465          }
 466          if (!empty($overrides)) {
 467              $params = ['modulename' => 'assign', 'instance' => $assignid];
 468              if (!empty($userids)) {
 469                  $params = array_merge($params, $userparams);
 470                  $DB->delete_records_select('event', "modulename = :modulename AND instance = :instance AND userid $usersql",
 471                          $params);
 472                  // Setting up for the next query.
 473                  $params = $userparams;
 474                  $usersql = "AND userid $usersql";
 475              } else {
 476                  $DB->delete_records('event', $params);
 477                  // Setting up for the next query.
 478                  $params = [];
 479              }
 480              list($overridesql, $overrideparams) = $DB->get_in_or_equal(array_keys($overrides), SQL_PARAMS_NAMED);
 481              $params = array_merge($params, $overrideparams);
 482              $DB->delete_records_select('assign_overrides', "id $overridesql $usersql", $params);
 483          }
 484      }
 485  
 486      /**
 487       * Find out if this user has graded any users.
 488       *
 489       * @param  int $userid The user ID (potential teacher).
 490       * @param  assign $assign The assignment object.
 491       * @return array If successful an array of objects with userids that this user graded, otherwise false.
 492       */
 493      protected static function get_graded_users(int $userid, \assign $assign) {
 494          $params = ['grader' => $userid, 'assignid' => $assign->get_instance()->id];
 495  
 496          $sql = "SELECT DISTINCT userid AS id
 497                    FROM {assign_grades}
 498                   WHERE grader = :grader AND assignment = :assignid";
 499  
 500          $useridlist = new useridlist($userid, $assign->get_instance()->id);
 501          $useridlist->add_from_sql($sql, $params);
 502  
 503          // Call sub-plugins to see if they have information not already collected.
 504          manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_student_user_ids',
 505                  [$useridlist]);
 506          manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_student_user_ids', [$useridlist]);
 507  
 508          $userids = $useridlist->get_userids();
 509          return ($userids) ? $userids : false;
 510      }
 511  
 512      /**
 513       * Writes out various user meta data about the assignment.
 514       *
 515       * @param  \context $context The context of this assignment.
 516       * @param  \assign $assign The assignment object.
 517       * @param  int $userid The user ID
 518       */
 519      protected static function store_assign_user_flags(\context $context, \assign $assign, int $userid) {
 520          $datatypes = ['locked' => get_string('locksubmissions', 'mod_assign'),
 521                        'mailed' => get_string('privacy:metadata:mailed', 'mod_assign'),
 522                        'extensionduedate' => get_string('extensionduedate', 'mod_assign'),
 523                        'workflowstate' => get_string('markingworkflowstate', 'mod_assign'),
 524                        'allocatedmarker' => get_string('allocatedmarker_help', 'mod_assign')];
 525          $userflags = (array)$assign->get_user_flags($userid, false);
 526  
 527          foreach ($datatypes as $key => $description) {
 528              if (isset($userflags[$key]) && !empty($userflags[$key])) {
 529                  $value = $userflags[$key];
 530                  if ($key == 'locked' || $key == 'mailed') {
 531                      $value = transform::yesno($value);
 532                  } else if ($key == 'extensionduedate') {
 533                      $value = transform::datetime($value);
 534                  }
 535                  writer::with_context($context)->export_metadata([], $key, $value, $description);
 536              }
 537          }
 538      }
 539  
 540      /**
 541       * Formats and then exports the user's grade data.
 542       *
 543       * @param  \stdClass $grade The assign grade object
 544       * @param  \context $context The context object
 545       * @param  array $currentpath Current directory path that we are exporting to.
 546       */
 547      protected static function export_grade_data(\stdClass $grade, \context $context, array $currentpath) {
 548          $gradedata = (object)[
 549              'timecreated' => transform::datetime($grade->timecreated),
 550              'timemodified' => transform::datetime($grade->timemodified),
 551              'grader' => transform::user($grade->grader),
 552              'grade' => $grade->grade,
 553              'attemptnumber' => ($grade->attemptnumber + 1)
 554          ];
 555          writer::with_context($context)
 556                  ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata);
 557      }
 558  
 559      /**
 560       * Formats and then exports the user's submission data.
 561       *
 562       * @param  \stdClass $submission The assign submission object
 563       * @param  \context $context The context object
 564       * @param  array $currentpath Current directory path that we are exporting to.
 565       */
 566      protected static function export_submission_data(\stdClass $submission, \context $context, array $currentpath) {
 567          $submissiondata = (object)[
 568              'timecreated' => transform::datetime($submission->timecreated),
 569              'timemodified' => transform::datetime($submission->timemodified),
 570              'timestarted' => transform::datetime($submission->timestarted),
 571              'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'),
 572              'groupid' => $submission->groupid,
 573              'attemptnumber' => ($submission->attemptnumber + 1),
 574              'latest' => transform::yesno($submission->latest)
 575          ];
 576          writer::with_context($context)
 577                  ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_assign')]), $submissiondata);
 578      }
 579  
 580      /**
 581       * Stores the user preferences related to mod_assign.
 582       *
 583       * @param  int $userid The user ID that we want the preferences for.
 584       */
 585      public static function export_user_preferences(int $userid) {
 586          $context = \context_system::instance();
 587          $assignpreferences = [
 588              'assign_perpage' => ['string' => get_string('privacy:metadata:assignperpage', 'mod_assign'), 'bool' => false],
 589              'assign_filter' => ['string' => get_string('privacy:metadata:assignfilter', 'mod_assign'), 'bool' => false],
 590              'assign_markerfilter' => ['string' => get_string('privacy:metadata:assignmarkerfilter', 'mod_assign'), 'bool' => true],
 591              'assign_workflowfilter' => ['string' => get_string('privacy:metadata:assignworkflowfilter', 'mod_assign'),
 592                      'bool' => true],
 593              'assign_quickgrading' => ['string' => get_string('privacy:metadata:assignquickgrading', 'mod_assign'), 'bool' => true],
 594              'assign_downloadasfolders' => ['string' => get_string('privacy:metadata:assigndownloadasfolders', 'mod_assign'),
 595                      'bool' => true]
 596          ];
 597          foreach ($assignpreferences as $key => $preference) {
 598              $value = get_user_preferences($key, null, $userid);
 599              if ($preference['bool']) {
 600                  $value = transform::yesno($value);
 601              }
 602              if (isset($value)) {
 603                  writer::with_context($context)->export_user_preference('mod_assign', $key, $value, $preference['string']);
 604              }
 605          }
 606      }
 607  
 608      /**
 609       * Export overrides for this assignment.
 610       *
 611       * @param  \context $context Context
 612       * @param  \assign $assign The assign object.
 613       * @param  \stdClass $user The user object.
 614       */
 615      public static function export_overrides(\context $context, \assign $assign, \stdClass $user) {
 616  
 617          $overrides = $assign->override_exists($user->id);
 618          // Overrides returns an array with data in it, but an override with actual data will have the assign ID set.
 619          if (isset($overrides->assignid)) {
 620              $data = new \stdClass();
 621              if (!empty($overrides->duedate)) {
 622                  $data->duedate = transform::datetime($overrides->duedate);
 623              }
 624              if (!empty($overrides->cutoffdate)) {
 625                  $data->cutoffdate = transform::datetime($overrides->cutoffdate);
 626              }
 627              if (!empty($overrides->allowsubmissionsfromdate)) {
 628                  $data->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate);
 629              }
 630              if (!empty($data)) {
 631                  writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data);
 632              }
 633          }
 634      }
 635  
 636      /**
 637       * Exports assignment submission data for a user.
 638       *
 639       * @param  \assign         $assign           The assignment object
 640       * @param  \stdClass        $user             The user object
 641       * @param  \context_module $context          The context
 642       * @param  array           $path             The path for exporting data
 643       * @param  bool|boolean    $exportforteacher A flag for if this is exporting data as a teacher.
 644       */
 645      protected static function export_submission(\assign $assign, \stdClass $user, \context_module $context, array $path,
 646              bool $exportforteacher = false) {
 647          $submissions = $assign->get_all_submissions($user->id);
 648          $teacher = ($exportforteacher) ? $user : null;
 649          $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
 650          $controller = $gradingmanager->get_active_controller();
 651          foreach ($submissions as $submission) {
 652              // Attempt numbers start at zero, which is fine for programming, but doesn't make as much sense
 653              // for users.
 654              $submissionpath = array_merge($path,
 655                      [get_string('privacy:attemptpath', 'mod_assign', ($submission->attemptnumber + 1))]);
 656  
 657              $params = new assign_plugin_request_data($context, $assign, $submission, $submissionpath ,$teacher);
 658              manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
 659                      'export_submission_user_data', [$params]);
 660              if (!isset($teacher)) {
 661                  self::export_submission_data($submission, $context, $submissionpath);
 662              }
 663              $grade = $assign->get_user_grade($user->id, false, $submission->attemptnumber);
 664              if ($grade) {
 665                  $params = new assign_plugin_request_data($context, $assign, $grade, $submissionpath, $teacher);
 666                  manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'export_feedback_user_data',
 667                          [$params]);
 668  
 669                  self::export_grade_data($grade, $context, $submissionpath);
 670                  // Check for advanced grading and retrieve that information.
 671                  if (isset($controller)) {
 672                      \core_grading\privacy\provider::export_item_data($context, $grade->id, $submissionpath);
 673                  }
 674              }
 675          }
 676      }
 677  }