2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
20 * @package core_grades
21 * @copyright 2018 Frédéric Massart
22 * @author Frédéric Massart <fred@branchup.tech>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 namespace core_grades\privacy
;
27 defined('MOODLE_INTERNAL') ||
die();
36 use core_privacy\local\metadata\collection
;
37 use core_privacy\local\request\approved_contextlist
;
38 use core_privacy\local\request\transform
;
39 use core_privacy\local\request\writer
;
41 require_once($CFG->libdir
. '/gradelib.php');
44 * Data provider class.
46 * @package core_grades
47 * @copyright 2018 Frédéric Massart
48 * @author Frédéric Massart <fred@branchup.tech>
49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
51 class provider
implements
52 \core_privacy\local\metadata\provider
,
53 \core_privacy\local\request\subsystem\provider
{
58 * @param collection $collection The initialised collection to add items to.
59 * @return collection A listing of user data stored through this system.
61 public static function get_metadata(collection
$collection) : collection
{
63 // Tables without 'real' user information.
64 $collection->add_database_table('grade_outcomes', [
65 'timemodified' => 'privacy:metadata:outcomes:timemodified',
66 'usermodified' => 'privacy:metadata:outcomes:usermodified',
67 ], 'privacy:metadata:outcomes');
69 $collection->add_database_table('grade_outcomes_history', [
70 'timemodified' => 'privacy:metadata:history:timemodified',
71 'loggeduser' => 'privacy:metadata:history:loggeduser',
72 ], 'privacy:metadata:outcomeshistory');
74 $collection->add_database_table('grade_categories_history', [
75 'timemodified' => 'privacy:metadata:history:timemodified',
76 'loggeduser' => 'privacy:metadata:history:loggeduser',
77 ], 'privacy:metadata:categorieshistory');
79 $collection->add_database_table('grade_items_history', [
80 'timemodified' => 'privacy:metadata:history:timemodified',
81 'loggeduser' => 'privacy:metadata:history:loggeduser',
82 ], 'privacy:metadata:itemshistory');
84 $collection->add_database_table('scale', [
85 'userid' => 'privacy:metadata:scale:userid',
86 'timemodified' => 'privacy:metadata:scale:timemodified',
87 ], 'privacy:metadata:scale');
89 $collection->add_database_table('scale_history', [
90 'userid' => 'privacy:metadata:scale:userid',
91 'timemodified' => 'privacy:metadata:history:timemodified',
92 'loggeduser' => 'privacy:metadata:history:loggeduser',
93 ], 'privacy:metadata:scalehistory');
95 // Table with user information.
96 $gradescommonfields = [
97 'userid' => 'privacy:metadata:grades:userid',
98 'usermodified' => 'privacy:metadata:grades:usermodified',
99 'finalgrade' => 'privacy:metadata:grades:finalgrade',
100 'feedback' => 'privacy:metadata:grades:feedback',
101 'information' => 'privacy:metadata:grades:information',
104 $collection->add_database_table('grade_grades', array_merge($gradescommonfields, [
105 'timemodified' => 'privacy:metadata:grades:timemodified',
106 ]), 'privacy:metadata:grades');
108 $collection->add_database_table('grade_grades_history', array_merge($gradescommonfields, [
109 'timemodified' => 'privacy:metadata:history:timemodified',
110 'loggeduser' => 'privacy:metadata:history:loggeduser',
111 ]), 'privacy:metadata:gradeshistory');
113 // The following tables are reported but not exported/deleted because their data is temporary and only
114 // used during an import. It's content is deleted after a successful, or failed, import.
116 $collection->add_database_table('grade_import_newitem', [
117 'itemname' => 'privacy:metadata:grade_import_newitem:itemname',
118 'importcode' => 'privacy:metadata:grade_import_newitem:importcode',
119 'importer' => 'privacy:metadata:grade_import_newitem:importer'
120 ], 'privacy:metadata:grade_import_newitem');
122 $collection->add_database_table('grade_import_values', [
123 'userid' => 'privacy:metadata:grade_import_values:userid',
124 'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade',
125 'feedback' => 'privacy:metadata:grade_import_values:feedback',
126 'importcode' => 'privacy:metadata:grade_import_values:importcode',
127 'importer' => 'privacy:metadata:grade_import_values:importer',
128 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback'
129 ], 'privacy:metadata:grade_import_values');
135 * Get the list of contexts that contain user information for the specified user.
137 * @param int $userid The user to search.
138 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
140 public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist
{
141 $contextlist = new \core_privacy\local\request\
contextlist();
143 // Add where we modified outcomes.
145 SELECT DISTINCT ctx.id
146 FROM {grade_outcomes} go
148 ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
149 OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid)
150 WHERE go.usermodified = :userid";
151 $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE
, 'syscontextid' => SYSCONTEXTID
];
152 $contextlist->add_from_sql($sql, $params);
154 // Add where we modified scales.
156 SELECT DISTINCT ctx.id
159 ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel)
160 OR (s.courseid = 0 AND ctx.id = :syscontextid)
161 WHERE s.userid = :userid";
162 $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE
, 'syscontextid' => SYSCONTEXTID
];
163 $contextlist->add_from_sql($sql, $params);
165 // Add where appear in the history of outcomes, categories, scales or items.
167 SELECT DISTINCT ctx.id
169 LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND (
170 (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1)
171 OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1)
173 LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND (
174 gch.courseid = ctx.instanceid
175 AND ctx.contextlevel = :courselevel2
177 LEFT JOIN {grade_items_history} gih ON gih.loggeduser = :userid3 AND (
178 gih.courseid = ctx.instanceid
179 AND ctx.contextlevel = :courselevel3
181 LEFT JOIN {scale_history} sh
182 ON (sh.userid = :userid4 OR sh.loggeduser = :userid5)
184 (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4)
185 OR (sh.courseid = 0 AND ctx.id = :syscontextid2)
187 WHERE goh.id IS NOT NULL
188 OR gch.id IS NOT NULL
189 OR gih.id IS NOT NULL
190 OR sh.id IS NOT NULL";
192 'syscontextid1' => SYSCONTEXTID
,
193 'syscontextid2' => SYSCONTEXTID
,
194 'courselevel1' => CONTEXT_COURSE
,
195 'courselevel2' => CONTEXT_COURSE
,
196 'courselevel3' => CONTEXT_COURSE
,
197 'courselevel4' => CONTEXT_COURSE
,
198 'userid1' => $userid,
199 'userid2' => $userid,
200 'userid3' => $userid,
201 'userid4' => $userid,
202 'userid5' => $userid,
204 $contextlist->add_from_sql($sql, $params);
206 // Add where we were graded or modified grades, including in the history table.
208 SELECT DISTINCT ctx.id
209 FROM {grade_items} gi
211 ON ctx.instanceid = gi.courseid
212 AND ctx.contextlevel = :courselevel
213 LEFT JOIN {grade_grades} gg
215 AND (gg.userid = :userid1 OR gg.usermodified = :userid2)
216 LEFT JOIN {grade_grades_history} ggh
217 ON ggh.itemid = gi.id
219 ggh.userid = :userid3
220 OR ggh.loggeduser = :userid4
221 OR ggh.usermodified = :userid5
223 WHERE gg.id IS NOT NULL
224 OR ggh.id IS NOT NULL";
226 'courselevel' => CONTEXT_COURSE
,
227 'userid1' => $userid,
228 'userid2' => $userid,
229 'userid3' => $userid,
230 'userid4' => $userid,
231 'userid5' => $userid,
233 $contextlist->add_from_sql($sql, $params);
235 // Historical grades can be made orphans when the corresponding itemid is deleted. When that happens
236 // we cannot tie the historical grade to a course context, so we report the user context as a last resort.
238 SELECT DISTINCT ctx.id
240 JOIN {grade_grades_history} ggh
241 ON ctx.contextlevel = :userlevel
242 AND ggh.userid = ctx.instanceid
244 ggh.userid = :userid1
245 OR ggh.usermodified = :userid2
246 OR ggh.loggeduser = :userid3
248 LEFT JOIN {grade_items} gi
249 ON ggh.itemid = gi.id
250 WHERE gi.id IS NULL";
252 'userlevel' => CONTEXT_USER
,
253 'userid1' => $userid,
254 'userid2' => $userid,
257 $contextlist->add_from_sql($sql, $params);
263 * Export all user data for the specified user, in the specified contexts.
265 * @param approved_contextlist $contextlist The approved contexts to export information for.
267 public static function export_user_data(approved_contextlist
$contextlist) {
270 $user = $contextlist->get_user();
272 $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
273 if ($context->contextlevel
== CONTEXT_COURSE
) {
274 $carry[$context->contextlevel
][] = $context;
276 } else if ($context->contextlevel
== CONTEXT_USER
) {
277 $carry[$context->contextlevel
][] = $context;
287 $rootpath = [get_string('grades', 'core_grades')];
288 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
290 // Export the outcomes.
291 static::export_user_data_outcomes_in_contexts($contextlist);
293 // Export the scales.
294 static::export_user_data_scales_in_contexts($contextlist);
296 // Export the historical grades which have become orphans (their grade items were deleted).
297 // We place those in ther user context of the graded user.
298 $userids = array_values(array_map(function($context) {
299 return $context->instanceid
;
300 }, $contexts[CONTEXT_USER
]));
301 if (!empty($userids)) {
303 // Export own historical grades and related ones.
304 list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED
);
305 list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED
);
306 list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED
);
307 $usercontext = $contexts[CONTEXT_USER
];
308 $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
310 SELECT $gghfields, ctx.id as ctxid
311 FROM {grade_grades_history} ggh
313 ON ctx.instanceid = ggh.userid
314 AND ctx.contextlevel = :userlevel
315 LEFT JOIN {grade_items} gi
316 ON gi.id = ggh.itemid
318 AND (ggh.userid $inuseridsql
319 OR ggh.usermodified $inusermodifiedsql
320 OR ggh.loggeduser $inloggedusersql)
321 AND (ggh.userid = :userid1
322 OR ggh.usermodified = :userid2
323 OR ggh.loggeduser = :userid3)
324 ORDER BY ggh.userid, ggh.timemodified, ggh.id";
325 $params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams,
326 ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER
]);
328 $deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades');
329 $recordset = $DB->get_recordset_sql($sql, $params);
330 static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) {
331 $context = context
::instance_by_id($record->ctxid
);
332 $gghrecord = static::extract_record($record, 'ggh_');
334 // Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades.
336 'name' => $deletedstr,
337 'graded_user_was_you' => transform
::yesno($userid == $gghrecord->userid
),
338 'grade' => $gghrecord->finalgrade
,
339 'feedback' => format_text($gghrecord->feedback
, $gghrecord->feedbackformat
, ['context' => $context]),
340 'information' => format_text($gghrecord->information
, $gghrecord->informationformat
, ['context' => $context]),
341 'timemodified' => transform
::datetime($gghrecord->timemodified
),
342 'logged_in_user_was_you' => transform
::yesno($userid == $gghrecord->loggeduser
),
343 'author_of_change_was_you' => transform
::yesno($userid == $gghrecord->usermodified
),
344 'action' => static::transform_history_action($gghrecord->action
)
349 }, function($ctxid, $data) use ($rootpath) {
350 $context = context
::instance_by_id($ctxid);
351 writer
::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
355 // Find out the course IDs.
356 $courseids = array_values(array_map(function($context) {
357 return $context->instanceid
;
358 }, $contexts[CONTEXT_COURSE
]));
359 if (empty($courseids)) {
362 list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED
);
364 // Ensure that the grades are final and do not need regrading.
365 array_walk($courseids, function($courseid) {
366 grade_regrade_final_grades($courseid);
369 // Export own grades.
370 $ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
371 $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
372 $scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_');
374 SELECT $ggfields, $gifields, $scalefields
375 FROM {grade_grades} gg
376 JOIN {grade_items} gi
379 ON sc.id = gi.scaleid
380 WHERE gi.courseid $incoursesql
381 AND gg.userid = :userid
382 ORDER BY gi.courseid, gi.id, gg.id";
383 $params = array_merge($incourseparams, ['userid' => $userid]);
385 $recordset = $DB->get_recordset_sql($sql, $params);
386 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
387 $context = context_course
::instance($record->gi_courseid
);
388 $gg = static::extract_grade_grade_from_record($record);
389 $carry[] = static::transform_grade($gg, $context);
392 }, function($courseid, $data) use ($rootpath) {
393 $context = context_course
::instance($courseid);
394 writer
::with_context($context)->export_data($rootpath, (object) ['grades' => $data]);
397 // Export own historical grades in courses.
398 $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
400 SELECT $gghfields, $gifields, $scalefields
401 FROM {grade_grades_history} ggh
402 JOIN {grade_items} gi
403 ON gi.id = ggh.itemid
405 ON sc.id = gi.scaleid
406 WHERE gi.courseid $incoursesql
407 AND ggh.userid = :userid
408 ORDER BY gi.courseid, ggh.timemodified, ggh.id";
409 $params = array_merge($incourseparams, ['userid' => $userid]);
411 $recordset = $DB->get_recordset_sql($sql, $params);
412 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
413 $context = context_course
::instance($record->gi_courseid
);
414 $gg = static::extract_grade_grade_from_record($record, true);
415 $carry[] = array_merge(static::transform_grade($gg, $context), [
416 'action' => static::transform_history_action($record->ggh_action
)
420 }, function($courseid, $data) use ($rootpath) {
421 $context = context_course
::instance($courseid);
422 writer
::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
425 // Export edits of categories history.
427 SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action
428 FROM {grade_categories_history} gch
429 WHERE gch.courseid $incoursesql
430 AND gch.loggeduser = :userid
431 ORDER BY gch.courseid, gch.timemodified, gch.id";
432 $params = array_merge($incourseparams, ['userid' => $userid]);
433 $recordset = $DB->get_recordset_sql($sql, $params);
434 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
436 'name' => $record->fullname
,
437 'timemodified' => transform
::datetime($record->timemodified
),
438 'logged_in_user_was_you' => transform
::yesno(true),
439 'action' => static::transform_history_action($record->action
),
443 }, function($courseid, $data) use ($relatedtomepath) {
444 $context = context_course
::instance($courseid);
445 writer
::with_context($context)->export_related_data($relatedtomepath, 'categories_history',
446 (object) ['modified_records' => $data]);
449 // Export edits of items history.
451 SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action
452 FROM {grade_items_history} gih
453 WHERE gih.courseid $incoursesql
454 AND gih.loggeduser = :userid
455 ORDER BY gih.courseid, gih.timemodified, gih.id";
456 $params = array_merge($incourseparams, ['userid' => $userid]);
457 $recordset = $DB->get_recordset_sql($sql, $params);
458 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
460 'name' => $record->itemname
,
461 'module' => $record->itemmodule
,
462 'info' => $record->iteminfo
,
463 'timemodified' => transform
::datetime($record->timemodified
),
464 'logged_in_user_was_you' => transform
::yesno(true),
465 'action' => static::transform_history_action($record->action
),
469 }, function($courseid, $data) use ($relatedtomepath) {
470 $context = context_course
::instance($courseid);
471 writer
::with_context($context)->export_related_data($relatedtomepath, 'items_history',
472 (object) ['modified_records' => $data]);
475 // Export edits of grades in course.
477 SELECT $ggfields, $gifields, $scalefields
478 FROM {grade_grades} gg
479 JOIN {grade_items} gi
482 ON sc.id = gi.scaleid
483 WHERE gi.courseid $incoursesql
484 AND gg.userid <> :userid1 -- Our grades have already been exported.
485 AND gg.usermodified = :userid2
486 ORDER BY gi.courseid, gg.timemodified, gg.id";
487 $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]);
488 $recordset = $DB->get_recordset_sql($sql, $params);
489 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
490 $context = context_course
::instance($record->gi_courseid
);
491 $gg = static::extract_grade_grade_from_record($record);
492 $carry[] = array_merge(static::transform_grade($gg, $context), [
493 'userid' => transform
::user($gg->userid
),
494 'created_or_modified_by_you' => transform
::yesno(true),
498 }, function($courseid, $data) use ($relatedtomepath) {
499 $context = context_course
::instance($courseid);
500 writer
::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]);
503 // Export edits of grades history in course.
505 SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser
506 FROM {grade_grades_history} ggh
507 JOIN {grade_items} gi
508 ON ggh.itemid = gi.id
510 ON sc.id = gi.scaleid
511 WHERE gi.courseid $incoursesql
512 AND ggh.userid <> :userid1 -- We've already exported our history.
513 AND (ggh.loggeduser = :userid2
514 OR ggh.usermodified = :userid3)
515 ORDER BY gi.courseid, ggh.timemodified, ggh.id";
516 $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]);
517 $recordset = $DB->get_recordset_sql($sql, $params);
518 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) {
519 $context = context_course
::instance($record->gi_courseid
);
520 $gg = static::extract_grade_grade_from_record($record, true);
521 $carry[] = array_merge(static::transform_grade($gg, $context), [
522 'userid' => transform
::user($gg->userid
),
523 'logged_in_user_was_you' => transform
::yesno($userid == $record->loggeduser
),
524 'author_of_change_was_you' => transform
::yesno($userid == $gg->usermodified
),
525 'action' => static::transform_history_action($record->ggh_action
),
529 }, function($courseid, $data) use ($relatedtomepath) {
530 $context = context_course
::instance($courseid);
531 writer
::with_context($context)->export_related_data($relatedtomepath, 'grades_history',
532 (object) ['modified_records' => $data]);
537 * Delete all data for all users in the specified context.
539 * @param context $context The specific context to delete data for.
541 public static function delete_data_for_all_users_in_context(context
$context) {
544 switch ($context->contextlevel
) {
546 // The user context is only reported when there are orphan historical grades, so we only delete those.
547 static::delete_orphan_historical_grades($context->instanceid
);
551 // We must not change the structure of the course, so we only delete user content.
552 $itemids = static::get_item_ids_from_course_ids([$context->instanceid
]);
553 if (empty($itemids)) {
556 list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED
);
557 $DB->delete_records_select('grade_grades', "itemid $insql", $inparams);
558 $DB->delete_records_select('grade_grades_history', "itemid $insql", $inparams);
565 * Delete all user data for the specified user, in the specified contexts.
567 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
569 public static function delete_data_for_user(approved_contextlist
$contextlist) {
571 $userid = $contextlist->get_user()->id
;
574 foreach ($contextlist->get_contexts() as $context) {
575 if ($context->contextlevel
== CONTEXT_USER
&& $userid == $context->instanceid
) {
576 // User attempts to delete data in their own context.
577 static::delete_orphan_historical_grades($userid);
579 } else if ($context->contextlevel
== CONTEXT_COURSE
) {
580 // Log the list of course IDs.
581 $courseids[] = $context->instanceid
;
585 $itemids = static::get_item_ids_from_course_ids($courseids);
586 if (empty($itemids)) {
587 // Our job here is done!
591 // Delete all the grades.
592 list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED
);
593 $params = array_merge($inparams, ['userid' => $userid]);
594 $DB->delete_records_select('grade_grades', "itemid $insql AND userid = :userid", $params);
595 $DB->delete_records_select('grade_grades_history', "itemid $insql AND userid = :userid", $params);
599 * Delete orphan historical grades.
601 * @param int $userid The user ID.
604 protected static function delete_orphan_historical_grades($userid) {
608 FROM {grade_grades_history} ggh
609 LEFT JOIN {grade_items} gi
610 ON ggh.itemid = gi.id
612 AND ggh.userid = :userid";
613 $ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
617 list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED
);
618 $DB->delete_records_select('grade_grades_history', "id $insql", $inparams);
622 * Export the user data related to outcomes.
624 * @param approved_contextlist $contextlist The approved contexts to export information for.
627 protected static function export_user_data_outcomes_in_contexts(approved_contextlist
$contextlist) {
630 $rootpath = [get_string('grades', 'core_grades')];
631 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
632 $userid = $contextlist->get_user()->id
;
634 // Reorganise the contexts.
635 $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
636 if ($context->contextlevel
== CONTEXT_SYSTEM
) {
637 $carry['in_system'] = true;
638 } else if ($context->contextlevel
== CONTEXT_COURSE
) {
639 $carry['courseids'][] = $context->instanceid
;
643 'in_system' => false,
648 $sqltemplateparts = [];
649 $templateparams = [];
650 if ($reduced['in_system']) {
651 $sqltemplateparts[] = '{prefix}.courseid IS NULL';
653 if (!empty($reduced['courseids'])) {
654 list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED
);
655 $sqltemplateparts[] = "{prefix}.courseid $insql";
656 $templateparams = array_merge($templateparams, $inparams);
658 if (empty($sqltemplateparts)) {
661 $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
663 // Export edited outcomes.
664 $sqlwhere = str_replace('{prefix}', 'go', $sqltemplate);
666 SELECT go.id, COALESCE(go.courseid, 0) AS courseid, go.shortname, go.fullname, go.timemodified
667 FROM {grade_outcomes} go
669 AND go.usermodified = :userid
670 ORDER BY go.courseid, go.timemodified, go.id";
671 $params = array_merge($templateparams, ['userid' => $userid]);
672 $recordset = $DB->get_recordset_sql($sql, $params);
673 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
675 'shortname' => $record->shortname
,
676 'fullname' => $record->fullname
,
677 'timemodified' => transform
::datetime($record->timemodified
),
678 'created_or_modified_by_you' => transform
::yesno(true)
682 }, function($courseid, $data) use ($relatedtomepath) {
683 $context = $courseid ? context_course
::instance($courseid) : context_system
::instance();
684 writer
::with_context($context)->export_related_data($relatedtomepath, 'outcomes',
685 (object) ['outcomes' => $data]);
688 // Export edits of outcomes history.
689 $sqlwhere = str_replace('{prefix}', 'goh', $sqltemplate);
691 SELECT goh.id, COALESCE(goh.courseid, 0) AS courseid, goh.shortname, goh.fullname, goh.timemodified, goh.action
692 FROM {grade_outcomes_history} goh
694 AND goh.loggeduser = :userid
695 ORDER BY goh.courseid, goh.timemodified, goh.id";
696 $params = array_merge($templateparams, ['userid' => $userid]);
697 $recordset = $DB->get_recordset_sql($sql, $params);
698 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
700 'shortname' => $record->shortname
,
701 'fullname' => $record->fullname
,
702 'timemodified' => transform
::datetime($record->timemodified
),
703 'logged_in_user_was_you' => transform
::yesno(true),
704 'action' => static::transform_history_action($record->action
)
708 }, function($courseid, $data) use ($relatedtomepath) {
709 $context = $courseid ? context_course
::instance($courseid) : context_system
::instance();
710 writer
::with_context($context)->export_related_data($relatedtomepath, 'outcomes_history',
711 (object) ['modified_records' => $data]);
716 * Export the user data related to scales.
718 * @param approved_contextlist $contextlist The approved contexts to export information for.
721 protected static function export_user_data_scales_in_contexts(approved_contextlist
$contextlist) {
724 $rootpath = [get_string('grades', 'core_grades')];
725 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
726 $userid = $contextlist->get_user()->id
;
728 // Reorganise the contexts.
729 $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
730 if ($context->contextlevel
== CONTEXT_SYSTEM
) {
731 $carry['in_system'] = true;
732 } else if ($context->contextlevel
== CONTEXT_COURSE
) {
733 $carry['courseids'][] = $context->instanceid
;
737 'in_system' => false,
742 $sqltemplateparts = [];
743 $templateparams = [];
744 if ($reduced['in_system']) {
745 $sqltemplateparts[] = '{prefix}.courseid = 0';
747 if (!empty($reduced['courseids'])) {
748 list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED
);
749 $sqltemplateparts[] = "{prefix}.courseid $insql";
750 $templateparams = array_merge($templateparams, $inparams);
752 if (empty($sqltemplateparts)) {
755 $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
757 // Export edited scales.
758 $sqlwhere = str_replace('{prefix}', 's', $sqltemplate);
760 SELECT s.id, s.courseid, s.name, s.timemodified
763 AND s.userid = :userid
764 ORDER BY s.courseid, s.timemodified, s.id";
765 $params = array_merge($templateparams, ['userid' => $userid]);
766 $recordset = $DB->get_recordset_sql($sql, $params);
767 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
769 'name' => $record->name
,
770 'timemodified' => transform
::datetime($record->timemodified
),
771 'created_or_modified_by_you' => transform
::yesno(true)
775 }, function($courseid, $data) use ($relatedtomepath) {
776 $context = $courseid ? context_course
::instance($courseid) : context_system
::instance();
777 writer
::with_context($context)->export_related_data($relatedtomepath, 'scales',
778 (object) ['scales' => $data]);
781 // Export edits of scales history.
782 $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate);
784 SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser
785 FROM {scale_history} sh
787 AND sh.loggeduser = :userid1
788 OR sh.userid = :userid2
789 ORDER BY sh.courseid, sh.timemodified, sh.id";
790 $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]);
791 $recordset = $DB->get_recordset_sql($sql, $params);
792 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) {
794 'name' => $record->name
,
795 'timemodified' => transform
::datetime($record->timemodified
),
796 'author_of_change_was_you' => transform
::yesno($record->userid
== $userid),
797 'author_of_action_was_you' => transform
::yesno($record->loggeduser
== $userid),
798 'action' => static::transform_history_action($record->action
)
802 }, function($courseid, $data) use ($relatedtomepath) {
803 $context = $courseid ? context_course
::instance($courseid) : context_system
::instance();
804 writer
::with_context($context)->export_related_data($relatedtomepath, 'scales_history',
805 (object) ['modified_records' => $data]);
810 * Extract grade_grade from a record.
812 * @param stdClass $record The record.
813 * @param bool $ishistory Whether we're extracting a historical grade.
814 * @return grade_grade
816 protected static function extract_grade_grade_from_record(stdClass
$record, $ishistory = false) {
817 $prefix = $ishistory ?
'ggh_' : 'gg_';
818 $ggrecord = static::extract_record($record, $prefix);
820 // The grade history is not a real grade_grade so we remove the ID.
821 unset($ggrecord->id
);
823 $gg = new grade_grade($ggrecord, false);
825 // There is a grade item in the record.
826 if (!empty($record->gi_id
)) {
827 $gi = new grade_item(static::extract_record($record, 'gi_'), false);
828 $gg->grade_item
= $gi; // This is a common hack throughout the grades API.
831 // Load the scale, when it still exists.
832 if (!empty($gi->scaleid
) && !empty($record->sc_id
)) {
833 $scalerec = static::extract_record($record, 'sc_');
834 $gi->scale
= new grade_scale($scalerec, false);
835 $gi->scale
->load_items();
842 * Extract a record from another one.
844 * @param object $record The record to extract from.
845 * @param string $prefix The prefix used.
848 protected static function extract_record($record, $prefix) {
850 $prefixlength = strlen($prefix);
851 foreach ($record as $key => $value) {
852 if (strpos($key, $prefix) === 0) {
853 $result[substr($key, $prefixlength)] = $value;
856 return (object) $result;
860 * Get fields SQL for a grade related object.
862 * @param string $target The related object.
863 * @param string $alias The table alias.
864 * @param string $prefix A prefix.
867 protected static function get_fields_sql($target, $alias, $prefix) {
869 case 'grade_category':
872 case 'grade_outcome':
874 $obj = new $target([], false);
875 $fields = array_merge(array_keys($obj->optional_fields
), $obj->required_fields
);
878 case 'grade_grades_history':
879 $fields = ['id', 'action', 'oldid', 'source', 'timemodified', 'loggeduser', 'itemid', 'userid', 'rawgrade',
880 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime',
881 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat'];
885 throw new \
coding_exception('Unrecognised target: ' . $target);
889 return implode(', ', array_map(function($field) use ($alias, $prefix) {
890 return "{$alias}.{$field} AS {$prefix}{$field}";
895 * Get all the items IDs from course IDs.
897 * @param array $courseids The course IDs.
900 protected static function get_item_ids_from_course_ids($courseids) {
902 if (empty($courseids)) {
905 list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED
);
906 return $DB->get_fieldset_select('grade_items', 'id', "courseid $insql", $inparams);
910 * Loop and export from a recordset.
912 * @param moodle_recordset $recordset The recordset.
913 * @param string $splitkey The record key to determine when to export.
914 * @param mixed $initial The initial data to reduce from.
915 * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
916 * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
919 protected static function recordset_loop_and_export(\moodle_recordset
$recordset, $splitkey, $initial,
920 callable
$reducer, callable
$export) {
925 foreach ($recordset as $record) {
926 if ($lastid !== null && $record->{$splitkey} != $lastid) {
927 $export($lastid, $data);
930 $data = $reducer($data, $record);
931 $lastid = $record->{$splitkey};
935 if ($lastid !== null) {
936 $export($lastid, $data);
941 * Transform an history action.
943 * @param int $action The action.
946 protected static function transform_history_action($action) {
948 case GRADE_HISTORY_INSERT
:
949 return get_string('privacy:request:historyactioninsert', 'core_grades');
951 case GRADE_HISTORY_UPDATE
:
952 return get_string('privacy:request:historyactionupdate', 'core_grades');
954 case GRADE_HISTORY_DELETE
:
955 return get_string('privacy:request:historyactiondelete', 'core_grades');
965 * @param grade_grade $gg The grade object.
966 * @param context $context The context.
969 protected static function transform_grade(grade_grade
$gg, context
$context) {
970 $gi = $gg->load_grade_item();
971 $timemodified = $gg->timemodified ? transform
::datetime($gg->timemodified
) : null;
972 $timecreated = $gg->timecreated ? transform
::datetime($gg->timecreated
) : $timemodified; // When null we use timemodified.
974 'item' => $gi->get_name(),
975 'grade' => $gg->finalgrade
,
976 'grade_formatted' => grade_format_gradevalue($gg->finalgrade
, $gi),
977 'feedback' => format_text($gg->feedback
, $gg->feedbackformat
, ['context' => $context]),
978 'information' => format_text($gg->information
, $gg->informationformat
, ['context' => $context]),
979 'timecreated' => $timecreated,
980 'timemodified' => $timemodified,