MDL-62384 mod_assign: Update to sql query for performance.
[moodle.git] / mod / assign / classes / privacy / provider.php
CommitLineData
5c41cd77
AG
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
25namespace mod_assign\privacy;
26
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->dirroot . '/mod/assign/locallib.php');
30
31use \core_privacy\local\metadata\collection;
32use \core_privacy\local\metadata\provider as metadataprovider;
33use \core_privacy\local\request\contextlist;
34use \core_privacy\local\request\plugin\provider as pluginprovider;
35use \core_privacy\local\request\user_preference_provider as preference_provider;
36use \core_privacy\local\request\writer;
37use \core_privacy\local\request\approved_contextlist;
38use \core_privacy\local\request\transform;
39use \core_privacy\local\request\helper;
40use \core_privacy\manager;
41
42/**
43 * Privacy class for requesting user data.
44 *
45 * @package mod_assign
46 * @copyright 2018 Adrian Greeve <adrian@moodle.com>
47 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 */
49class provider implements metadataprovider, pluginprovider, preference_provider {
50
51 /** Interface for all assign submission sub-plugins. */
52 const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider';
53
54 /** Interface for all assign feedback sub-plugins. */
55 const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider';
56
57 /**
58 * Provides meta data that is stored about a user with mod_assign
59 *
60 * @param collection $collection A collection of meta data items to be added to.
61 * @return collection Returns the collection of metadata.
62 */
63 public static function get_metadata(collection $collection) : collection {
64 $assigngrades = [
65 'userid' => 'privacy:metadata:userid',
66 'timecreated' => 'privacy:metadata:timecreated',
67 'timemodified' => 'timemodified',
68 'grader' => 'privacy:metadata:grader',
69 'grade' => 'privacy:metadata:grade',
70 'attemptnumber' => 'attemptnumber'
71 ];
72 $assignoverrides = [
73 'groupid' => 'privacy:metadata:groupid',
74 'userid' => 'privacy:metadata:userid',
75 'allowsubmissionsfromdate' => 'allowsubmissionsfromdate',
76 'duedate' => 'duedate',
77 'cutoffdate' => 'cutoffdate'
78 ];
79 $assignsubmission = [
80 'userid' => 'privacy:metadata:userid',
81 'timecreated' => 'privacy:metadata:timecreated',
82 'timemodified' => 'timemodified',
83 'status' => 'gradingstatus',
84 'groupid' => 'privacy:metadata:groupid',
85 'attemptnumber' => 'attemptnumber',
86 'latest' => 'privacy:metadata:latest'
87 ];
88 $assignuserflags = [
89 'userid' => 'privacy:metadata:userid',
90 'assignment' => 'privacy:metadata:assignmentid',
91 'locked' => 'locksubmissions',
92 'mailed' => 'privacy:metadata:mailed',
93 'extensionduedate' => 'extensionduedate',
94 'workflowstate' => 'markingworkflowstate',
95 'allocatedmarker' => 'allocatedmarker'
96 ];
97 $assignusermapping = [
98 'assignment' => 'privacy:metadata:assignmentid',
99 'userid' => 'privacy:metadata:userid'
100 ];
101 $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades');
102 $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides');
103 $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail');
104 $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags');
105 $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping');
106 $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage');
107 $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter');
108 $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter');
109 $collection->add_user_preference('assign_workflowfilter', 'privacy:metadata:assignworkflowfilter');
110 $collection->add_user_preference('assign_quickgrading', 'privacy:metadata:assignquickgrading');
111 $collection->add_user_preference('assign_downloadasfolders', 'privacy:metadata:assigndownloadasfolders');
112
113 // Link to subplugins.
114 $collection->add_plugintype_link('assignsubmission', [],'privacy:metadata:assignsubmissionpluginsummary');
115 $collection->add_plugintype_link('assignfeedback', [], 'privacy:metadata:assignfeedbackpluginsummary');
116 $collection->add_subsystem_link('core_message', [], 'privacy:metadata:assignmessageexplanation');
117
118 return $collection;
119 }
120
121 /**
122 * Returns all of the contexts that has information relating to the userid.
123 *
124 * @param int $userid The user ID.
125 * @return contextlist an object with the contexts related to a userid.
126 */
127 public static function get_contexts_for_userid(int $userid) : contextlist {
128 $params = ['modulename' => 'assign',
129 'contextlevel' => CONTEXT_MODULE,
130 'userid' => $userid,
131 'graderid' => $userid,
132 'aouserid' => $userid,
133 'asnuserid' => $userid,
134 'aufuserid' => $userid,
135 'aumuserid' => $userid];
136
137 $sql = "SELECT ctx.id
138 FROM {course_modules} cm
139 JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
140 JOIN {assign} a ON cm.instance = a.id
141 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
8a5b84cf
AG
142 JOIN {assign_grades} ag ON a.id = ag.assignment AND (ag.userid = :userid OR ag.grader = :graderid)";
143
144 global $DB;
145
5c41cd77
AG
146 $contextlist = new contextlist();
147 $contextlist->add_from_sql($sql, $params);
8a5b84cf
AG
148
149 $sql = "SELECT ctx.id
150 FROM {course_modules} cm
151 JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
152 JOIN {assign} a ON cm.instance = a.id
153 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
154 JOIN {assign_overrides} ao ON a.id = ao.assignid
155 WHERE ao.userid = :aouserid";
156
157 $contextlist->add_from_sql($sql, $params);
158
159 $sql = "SELECT ctx.id
160 FROM {course_modules} cm
161 JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
162 JOIN {assign} a ON cm.instance = a.id
163 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
164 JOIN {assign_submission} asn ON a.id = asn.assignment
165 WHERE asn.userid = :asnuserid";
166
167 $contextlist->add_from_sql($sql, $params);
168
169 $sql = "SELECT ctx.id
170 FROM {course_modules} cm
171 JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
172 JOIN {assign} a ON cm.instance = a.id
173 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
174 JOIN {assign_user_flags} auf ON a.id = auf.assignment
175 WHERE auf.userid = :aufuserid";
176
177 $contextlist->add_from_sql($sql, $params);
178
179 $sql = "SELECT ctx.id
180 FROM {course_modules} cm
181 JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
182 JOIN {assign} a ON cm.instance = a.id
183 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
184 JOIN {assign_user_mapping} aum ON a.id = aum.assignment
185 WHERE aum.userid = :aumuserid";
186
187 $contextlist->add_from_sql($sql, $params);
188
5c41cd77
AG
189 manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
190 'get_context_for_userid_within_feedback', [$userid, $contextlist]);
191 manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
192 'get_context_for_userid_within_submission', [$userid, $contextlist]);
193
194 return $contextlist;
195 }
196
197 /**
198 * Write out the user data filtered by contexts.
199 *
200 * @param approved_contextlist $contextlist contexts that we are writing data out from.
201 */
202 public static function export_user_data(approved_contextlist $contextlist) {
203 foreach ($contextlist->get_contexts() as $context) {
204 // Check that the context is a module context.
205 if ($context->contextlevel != CONTEXT_MODULE) {
206 continue;
207 }
208 $user = $contextlist->get_user();
209 $assigndata = helper::get_context_data($context, $user);
210 helper::export_context_files($context, $user);
211
212 writer::with_context($context)->export_data([], $assigndata);
213 $assign = new \assign($context, null, null);
214
215 // I need to find out if I'm a student or a teacher.
216 if ($userids = self::get_graded_users($user->id, $assign)) {
217 // Return teacher info.
218 $currentpath = [get_string('privacy:studentpath', 'mod_assign')];
219 foreach ($userids as $studentuserid) {
220 $studentpath = array_merge($currentpath, [$studentuserid->id]);
221 static::export_submission($assign, $studentuserid, $context, $studentpath, true);
222 }
223 }
224
225 static::export_overrides($context, $assign, $user);
226 static::export_submission($assign, $user, $context, []);
227 // Meta data.
228 self::store_assign_user_flags($context, $assign, $user->id);
229 if ($assign->is_blind_marking()) {
230 $uniqueid = $assign->get_uniqueid_for_user_static($assign->get_instance()->id, $contextlist->get_user()->id);
231 if ($uniqueid) {
232 writer::with_context($context)
233 ->export_metadata([get_string('blindmarking', 'mod_assign')], 'blindmarkingid', $uniqueid,
234 get_string('privacy:blindmarkingidentifier', 'mod_assign'));
235 }
236 }
237 }
238 }
239
240 /**
241 * Delete all use data which matches the specified context.
242 *
243 * @param context $context The module context.
244 */
245 public static function delete_data_for_all_users_in_context(\context $context) {
246 global $DB;
247
248 if ($context->contextlevel == CONTEXT_MODULE) {
249 // Apparently we can't trust anything that comes via the context.
250 // Go go mega query to find out it we have an assign context that matches an existing assignment.
251 $sql = "SELECT a.id
252 FROM {assign} a
253 JOIN {course_modules} cm ON a.id = cm.instance
254 JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
255 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule
256 WHERE ctx.id = :contextid";
257 $params = ['modulename' => 'assign', 'contextmodule' => CONTEXT_MODULE, 'contextid' => $context->id];
258 $count = $DB->get_field_sql($sql, $params);
259 // If we have a count over zero then we can proceed.
260 if ($count > 0) {
261 // Get the assignment related to this context.
262 $assign = new \assign($context, null, null);
263 // What to do first... Get sub plugins to delete their stuff.
264 $requestdata = new assign_plugin_request_data($context, $assign);
265 manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
266 'delete_submission_for_context', [$requestdata]);
267 $requestdata = new assign_plugin_request_data($context, $assign);
268 manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
269 'delete_feedback_for_context', [$requestdata]);
270 $DB->delete_records('assign_grades', ['assignment' => $assign->get_instance()->id]);
271
272 // Time to roll my own method for deleting overrides.
273 static::delete_user_overrides($assign);
274 $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]);
275 $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]);
276 $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]);
277 }
278 }
279 }
280
281 /**
282 * Delete all user data for the specified user, in the specified contexts.
283 *
284 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
285 */
286 public static function delete_data_for_user(approved_contextlist $contextlist) {
287 global $DB;
288
289 $user = $contextlist->get_user();
290
291 foreach ($contextlist as $context) {
292 if ($context->contextlevel != CONTEXT_MODULE) {
3d110a5f 293 continue;
5c41cd77
AG
294 }
295 // Get the assign object.
296 $assign = new \assign($context, null, null);
297 $assignid = $assign->get_instance()->id;
298
299 $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
300 foreach ($submissions as $submission) {
301 $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user);
302 manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
303 'delete_submission_for_userid', [$requestdata]);
304 }
305
306 $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
307 foreach ($grades as $grade) {
308 $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user);
309 manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
310 'delete_feedback_for_grade', [$requestdata]);
311 }
312
313 static::delete_user_overrides($assign, $user);
314 $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
315 $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
316 $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
317 $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
318 }
319 }
320
321 /**
322 * Deletes assignment overrides.
323 *
324 * @param \assign $assign The assignment object
325 * @param \stdClass $user The user object if we are deleting only the overrides for one user.
326 */
327 protected static function delete_user_overrides(\assign $assign, \stdClass $user = null) {
328 global $DB;
329
330 $assignid = $assign->get_instance()->id;
331 $params = (isset($user)) ? ['assignid' => $assignid, 'userid' => $user->id] : ['assignid' => $assignid];
332
333 $overrides = $DB->get_records('assign_overrides', $params);
334 if (!empty($overrides)) {
335 foreach ($overrides as $override) {
336
337 // First delete calendar events associated with this override.
338 $conditions = ['modulename' => 'assign', 'instance' => $assignid];
339 if (isset($user)) {
340 $conditions['userid'] = $user->id;
341 }
342 $DB->delete_records('event', $conditions);
343
344 // Next delete the overrides.
345 $DB->delete_records('assign_overrides', ['id' => $override->id]);
346 }
347 }
348 }
349
350 /**
351 * Find out if this user has graded any users.
352 *
353 * @param int $userid The user ID (potential teacher).
354 * @param assign $assign The assignment object.
355 * @return array If successful an array of objects with userids that this user graded, otherwise false.
356 */
357 protected static function get_graded_users(int $userid, \assign $assign) {
358 $params = ['grader' => $userid, 'assignid' => $assign->get_instance()->id];
359
360 $sql = "SELECT DISTINCT userid AS id
361 FROM {assign_grades}
362 WHERE grader = :grader AND assignment = :assignid";
363
364 $useridlist = new useridlist($userid, $assign->get_instance()->id);
365 $useridlist->add_from_sql($sql, $params);
366
367 // Call sub-plugins to see if they have information not already collected.
368 manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_student_user_ids',
369 [$useridlist]);
370 manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_student_user_ids', [$useridlist]);
371
372 $userids = $useridlist->get_userids();
373 return ($userids) ? $userids : false;
374 }
375
376 /**
377 * Writes out various user meta data about the assignment.
378 *
379 * @param \context $context The context of this assignment.
380 * @param \assign $assign The assignment object.
381 * @param int $userid The user ID
382 */
383 protected static function store_assign_user_flags(\context $context, \assign $assign, int $userid) {
384 $datatypes = ['locked' => get_string('locksubmissions', 'mod_assign'),
385 'mailed' => get_string('privacy:metadata:mailed', 'mod_assign'),
386 'extensionduedate' => get_string('extensionduedate', 'mod_assign'),
387 'workflowstate' => get_string('markingworkflowstate', 'mod_assign'),
388 'allocatedmarker' => get_string('allocatedmarker_help', 'mod_assign')];
389 $userflags = (array)$assign->get_user_flags($userid, false);
390
391 foreach ($datatypes as $key => $description) {
392 if (isset($userflags[$key]) && !empty($userflags[$key])) {
393 $value = $userflags[$key];
394 if ($key == 'locked' || $key == 'mailed') {
395 $value = transform::yesno($value);
396 } else if ($key == 'extensionduedate') {
397 $value = transform::datetime($value);
398 }
399 writer::with_context($context)->export_metadata([], $key, $value, $description);
400 }
401 }
402 }
403
404 /**
405 * Formats and then exports the user's grade data.
406 *
407 * @param \stdClass $grade The assign grade object
408 * @param \context $context The context object
409 * @param array $currentpath Current directory path that we are exporting to.
410 */
411 protected static function export_grade_data(\stdClass $grade, \context $context, array $currentpath) {
412 $gradedata = (object)[
413 'timecreated' => transform::datetime($grade->timecreated),
414 'timemodified' => transform::datetime($grade->timemodified),
415 'grader' => transform::user($grade->grader),
416 'grade' => $grade->grade,
417 'attemptnumber' => $grade->attemptnumber
418 ];
419 writer::with_context($context)
420 ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata);
421 }
422
423 /**
424 * Formats and then exports the user's submission data.
425 *
426 * @param \stdClass $submission The assign submission object
427 * @param \context $context The context object
428 * @param array $currentpath Current directory path that we are exporting to.
429 */
430 protected static function export_submission_data(\stdClass $submission, \context $context, array $currentpath) {
431 $submissiondata = (object)[
432 'timecreated' => transform::datetime($submission->timecreated),
433 'timemodified' => transform::datetime($submission->timemodified),
434 'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'),
435 'groupid' => $submission->groupid,
436 'attemptnumber' => $submission->attemptnumber,
437 'latest' => transform::yesno($submission->latest)
438 ];
439 writer::with_context($context)
440 ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_assign')]), $submissiondata);
441 }
442
443 /**
444 * Stores the user preferences related to mod_assign.
445 *
446 * @param int $userid The user ID that we want the preferences for.
447 */
448 public static function export_user_preferences(int $userid) {
449 $context = \context_system::instance();
450 $assignpreferences = [
451 'assign_perpage' => ['string' => get_string('privacy:metadata:assignperpage', 'mod_assign'), 'bool' => false],
452 'assign_filter' => ['string' => get_string('privacy:metadata:assignfilter', 'mod_assign'), 'bool' => false],
453 'assign_markerfilter' => ['string' => get_string('privacy:metadata:assignmarkerfilter', 'mod_assign'), 'bool' => true],
454 'assign_workflowfilter' => ['string' => get_string('privacy:metadata:assignworkflowfilter', 'mod_assign'),
455 'bool' => true],
456 'assign_quickgrading' => ['string' => get_string('privacy:metadata:assignquickgrading', 'mod_assign'), 'bool' => true],
457 'assign_downloadasfolders' => ['string' => get_string('privacy:metadata:assigndownloadasfolders', 'mod_assign'),
458 'bool' => true]
459 ];
460 foreach ($assignpreferences as $key => $preference) {
461 $value = get_user_preferences($key, null, $userid);
462 if ($preference['bool']) {
463 $value = transform::yesno($value);
464 }
465 if (isset($value)) {
466 writer::with_context($context)->export_user_preference('mod_assign', $key, $value, $preference['string']);
467 }
468 }
469 }
470
471 /**
472 * Export overrides for this assignment.
473 *
474 * @param \context $context Context
475 * @param \assign $assign The assign object.
476 * @param \stdClass $user The user object.
477 */
478 public static function export_overrides(\context $context, \assign $assign, \stdClass $user) {
479
480 $overrides = $assign->override_exists($user->id);
481 // Overrides returns an array with data in it, but an override with actual data will have the assign ID set.
482 if (isset($overrides->assignid)) {
483 $data = new \stdClass();
484 if (!empty($overrides->duedate)) {
485 $data->duedate = transform::datetime($overrides->duedate);
486 }
487 if (!empty($overrides->cutoffdate)) {
488 $overrides->cutoffdate = transform::datetime($overrides->cutoffdate);
489 }
490 if (!empty($overrides->allowsubmissionsfromdate)) {
491 $overrides->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate);
492 }
493 if (!empty($data)) {
494 writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data);
495 }
496 }
497 }
498
499 /**
500 * Exports assignment submission data for a user.
501 *
502 * @param \assign $assign The assignment object
503 * @param \stdClass $user The user object
504 * @param \context_module $context The context
505 * @param array $path The path for exporting data
506 * @param bool|boolean $exportforteacher A flag for if this is exporting data as a teacher.
507 */
508 protected static function export_submission(\assign $assign, \stdClass $user, \context_module $context, array $path,
509 bool $exportforteacher = false) {
510 $submissions = $assign->get_all_submissions($user->id);
511 $teacher = ($exportforteacher) ? $user : null;
512 foreach ($submissions as $submission) {
513 // Attempt numbers start at zero, which is fine for programming, but doesn't make as much sense
514 // for users.
515 $submissionpath = array_merge($path,
516 [get_string('privacy:attemptpath', 'mod_assign', ($submission->attemptnumber + 1))]);
517
518 $params = new assign_plugin_request_data($context, $assign, $submission, $submissionpath ,$teacher);
519 manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
520 'export_submission_user_data', [$params]);
521 if (!isset($teacher)) {
522 self::export_submission_data($submission, $context, $submissionpath);
523 }
524 $grade = $assign->get_user_grade($user->id, false, $submission->attemptnumber);
525 if ($grade) {
526 $params = new assign_plugin_request_data($context, $assign, $grade, $submissionpath, $teacher);
527 manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'export_feedback_user_data',
528 [$params]);
529
530 self::export_grade_data($grade, $context, $submissionpath);
531 }
532 }
533 }
534}