Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Data provider.
19
 *
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
24
 */
25
 
26
namespace core_grades\privacy;
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use context;
30
use context_course;
31
use context_system;
32
use grade_item;
33
use grade_grade;
34
use grade_scale;
35
use stdClass;
36
use core_grades\privacy\grade_grade_with_history;
37
use core_privacy\local\metadata\collection;
38
use core_privacy\local\request\approved_contextlist;
39
use core_privacy\local\request\transform;
40
use core_privacy\local\request\writer;
41
 
42
require_once($CFG->libdir . '/gradelib.php');
43
 
44
/**
45
 * Data provider class.
46
 *
47
 * @package    core_grades
48
 * @copyright  2018 Frédéric Massart
49
 * @author     Frédéric Massart <fred@branchup.tech>
50
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
51
 */
52
class provider implements
53
    \core_privacy\local\metadata\provider,
54
    \core_privacy\local\request\subsystem\provider,
55
    \core_privacy\local\request\core_userlist_provider {
56
 
57
    /**
58
     * Returns metadata.
59
     *
60
     * @param collection $collection The initialised collection to add items to.
61
     * @return collection A listing of user data stored through this system.
62
     */
63
    public static function get_metadata(collection $collection): collection {
64
 
65
        // Tables without 'real' user information.
66
        $collection->add_database_table('grade_outcomes', [
67
            'timemodified' => 'privacy:metadata:outcomes:timemodified',
68
            'usermodified' => 'privacy:metadata:outcomes:usermodified',
69
        ], 'privacy:metadata:outcomes');
70
 
71
        $collection->add_database_table('grade_outcomes_history', [
72
            'timemodified' => 'privacy:metadata:history:timemodified',
73
            'loggeduser' => 'privacy:metadata:history:loggeduser',
74
        ], 'privacy:metadata:outcomeshistory');
75
 
76
        $collection->add_database_table('grade_categories_history', [
77
            'timemodified' => 'privacy:metadata:history:timemodified',
78
            'loggeduser' => 'privacy:metadata:history:loggeduser',
79
        ], 'privacy:metadata:categorieshistory');
80
 
81
        $collection->add_database_table('grade_items_history', [
82
            'timemodified' => 'privacy:metadata:history:timemodified',
83
            'loggeduser' => 'privacy:metadata:history:loggeduser',
84
        ], 'privacy:metadata:itemshistory');
85
 
86
        $collection->add_database_table('scale', [
87
            'userid' => 'privacy:metadata:scale:userid',
88
            'timemodified' => 'privacy:metadata:scale:timemodified',
89
        ], 'privacy:metadata:scale');
90
 
91
        $collection->add_database_table('scale_history', [
92
            'userid' => 'privacy:metadata:scale:userid',
93
            'timemodified' => 'privacy:metadata:history:timemodified',
94
            'loggeduser' => 'privacy:metadata:history:loggeduser',
95
        ], 'privacy:metadata:scalehistory');
96
 
97
        // Table with user information.
98
        $gradescommonfields = [
99
            'userid' => 'privacy:metadata:grades:userid',
100
            'usermodified' => 'privacy:metadata:grades:usermodified',
101
            'finalgrade' => 'privacy:metadata:grades:finalgrade',
102
            'feedback' => 'privacy:metadata:grades:feedback',
103
            'information' => 'privacy:metadata:grades:information',
104
        ];
105
 
106
        $collection->add_database_table('grade_grades', array_merge($gradescommonfields, [
107
            'timemodified' => 'privacy:metadata:grades:timemodified',
108
        ]), 'privacy:metadata:grades');
109
 
110
        $collection->add_database_table('grade_grades_history', array_merge($gradescommonfields, [
111
            'timemodified' => 'privacy:metadata:history:timemodified',
112
            'loggeduser' => 'privacy:metadata:history:loggeduser',
113
        ]), 'privacy:metadata:gradeshistory');
114
 
115
        // The following tables are reported but not exported/deleted because their data is temporary and only
116
        // used during an import. It's content is deleted after a successful, or failed, import.
117
 
118
        $collection->add_database_table('grade_import_newitem', [
119
            'itemname' => 'privacy:metadata:grade_import_newitem:itemname',
120
            'importcode' => 'privacy:metadata:grade_import_newitem:importcode',
121
            'importer' => 'privacy:metadata:grade_import_newitem:importer'
122
        ], 'privacy:metadata:grade_import_newitem');
123
 
124
        $collection->add_database_table('grade_import_values', [
125
            'userid' => 'privacy:metadata:grade_import_values:userid',
126
            'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade',
127
            'feedback' => 'privacy:metadata:grade_import_values:feedback',
128
            'importcode' => 'privacy:metadata:grade_import_values:importcode',
129
            'importer' => 'privacy:metadata:grade_import_values:importer',
130
            'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback'
131
        ], 'privacy:metadata:grade_import_values');
132
 
133
        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
134
 
135
        return $collection;
136
    }
137
 
138
    /**
139
     * Get the list of contexts that contain user information for the specified user.
140
     *
141
     * @param int $userid The user to search.
142
     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
143
     */
144
    public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
145
        $contextlist = new \core_privacy\local\request\contextlist();
146
 
147
        // Add where we modified outcomes.
148
        $sql = "
149
            SELECT DISTINCT ctx.id
150
              FROM {grade_outcomes} go
151
              JOIN {context} ctx
152
                ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
153
                OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid)
154
             WHERE go.usermodified = :userid";
155
        $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
156
        $contextlist->add_from_sql($sql, $params);
157
 
158
        // Add where we modified scales.
159
        $sql = "
160
            SELECT DISTINCT ctx.id
161
              FROM {scale} s
162
              JOIN {context} ctx
163
                ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel)
164
                OR (s.courseid = 0 AND ctx.id = :syscontextid)
165
             WHERE s.userid = :userid";
166
        $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
167
        $contextlist->add_from_sql($sql, $params);
168
 
169
        // Add where appear in the history of outcomes, categories, scales or items.
170
        $sql = "
171
            SELECT DISTINCT ctx.id
172
              FROM {context} ctx
173
              JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND goh.courseid > 0
174
               AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1";
175
        $params = [
176
            'courselevel1' => CONTEXT_COURSE,
177
            'userid1' => $userid,
178
        ];
179
        $contextlist->add_from_sql($sql, $params);
180
        $sql = "
181
            SELECT DISTINCT ctx.id
182
              FROM {context} ctx
183
              JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1
184
               AND (goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1";
185
        $params = [
186
            'syscontextid1' => SYSCONTEXTID,
187
            'courselevel1' => CONTEXT_COURSE,
188
            'userid1' => $userid,
189
        ];
190
        $contextlist->add_from_sql($sql, $params);
191
 
192
        $sql = "
193
            SELECT DISTINCT ctx.id
194
              FROM {context} ctx
195
              JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2
196
               AND gch.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel2";
197
        $params = [
198
            'courselevel2' => CONTEXT_COURSE,
199
            'userid2' => $userid,
200
        ];
201
        $contextlist->add_from_sql($sql, $params);
202
        $sql = "
203
            SELECT DISTINCT ctx.id
204
              FROM {context} ctx
205
              JOIN {grade_items_history} gih ON gih.loggeduser = :userid3
206
               AND gih.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel3";
207
        $params = [
208
            'courselevel3' => CONTEXT_COURSE,
209
            'userid3' => $userid,
210
        ];
211
        $contextlist->add_from_sql($sql, $params);
212
        $sql = "
213
            SELECT DISTINCT ctx.id
214
              FROM {context} ctx
215
              JOIN {scale_history} sh ON sh.userid = :userid4
216
               AND sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4";
217
        $params = [
218
            'courselevel4' => CONTEXT_COURSE,
219
            'userid4' => $userid,
220
        ];
221
        $contextlist->add_from_sql($sql, $params);
222
        $sql = "
223
            SELECT DISTINCT ctx.id
224
              FROM {context} ctx
225
              JOIN {scale_history} sh ON sh.loggeduser = :userid5
226
               AND sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4";
227
        $params = [
228
            'courselevel4' => CONTEXT_COURSE,
229
            'userid5' => $userid,
230
        ];
231
        $contextlist->add_from_sql($sql, $params);
232
        $sql = "
233
            SELECT DISTINCT ctx.id
234
              FROM {context} ctx
235
              JOIN {scale_history} sh ON sh.userid = :userid4 AND sh.courseid = 0 AND ctx.id = :syscontextid2";
236
        $params = [
237
            'syscontextid2' => SYSCONTEXTID,
238
            'userid4' => $userid,
239
        ];
240
        $contextlist->add_from_sql($sql, $params);
241
        $sql = "
242
            SELECT DISTINCT ctx.id
243
              FROM {context} ctx
244
              JOIN {scale_history} sh ON sh.loggeduser = :userid5 AND sh.courseid = 0 AND ctx.id = :syscontextid2";
245
        $params = [
246
            'syscontextid2' => SYSCONTEXTID,
247
            'userid5' => $userid,
248
        ];
249
        $contextlist->add_from_sql($sql, $params);
250
 
251
        // Add where we were graded or modified grades, including in the history table.
252
        $sql = "
253
            SELECT DISTINCT ctx.id
254
              FROM {grade_items} gi
255
              JOIN {context} ctx
256
                ON ctx.instanceid = gi.courseid
257
               AND ctx.contextlevel = :courselevel
258
              JOIN {grade_grades} gg
259
                ON gg.itemid = gi.id
260
             WHERE gg.userid = :userid1 OR gg.usermodified = :userid2";
261
        $params = [
262
            'courselevel' => CONTEXT_COURSE,
263
            'userid1' => $userid,
264
            'userid2' => $userid
265
        ];
266
        $contextlist->add_from_sql($sql, $params);
267
 
268
        $sql = "
269
            SELECT DISTINCT ctx.id
270
              FROM {grade_items} gi
271
              JOIN {context} ctx
272
                ON ctx.instanceid = gi.courseid
273
               AND ctx.contextlevel = :courselevel
274
              JOIN {grade_grades_history} ggh
275
                ON ggh.itemid = gi.id
276
             WHERE ggh.userid = :userid1
277
                OR ggh.loggeduser = :userid2
278
                OR ggh.usermodified = :userid3";
279
        $params = [
280
            'courselevel' => CONTEXT_COURSE,
281
            'userid1' => $userid,
282
            'userid2' => $userid,
283
            'userid3' => $userid
284
        ];
285
        $contextlist->add_from_sql($sql, $params);
286
 
287
        // Historical grades can be made orphans when the corresponding itemid is deleted. When that happens
288
        // we cannot tie the historical grade to a course context, so we report the user context as a last resort.
289
        $sql = "
290
           SELECT DISTINCT ctx.id
291
             FROM {context} ctx
292
             JOIN {grade_grades_history} ggh
293
               ON ctx.contextlevel = :userlevel
294
              AND ggh.userid = ctx.instanceid
295
              AND (
296
                  ggh.userid = :userid1
297
               OR ggh.usermodified = :userid2
298
               OR ggh.loggeduser = :userid3
299
              )
300
        LEFT JOIN {grade_items} gi
301
               ON ggh.itemid = gi.id
302
            WHERE gi.id IS NULL";
303
        $params = [
304
            'userlevel' => CONTEXT_USER,
305
            'userid1' => $userid,
306
            'userid2' => $userid,
307
            'userid3' => $userid
308
        ];
309
        $contextlist->add_from_sql($sql, $params);
310
 
311
        return $contextlist;
312
    }
313
 
314
    /**
315
     * Get the list of contexts that contain user information for the specified user.
316
     *
317
     * @param   \core_privacy\local\request\userlist    $userlist   The userlist containing the list of users who have data
318
     * in this context/plugin combination.
319
     */
320
    public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
321
        $context = $userlist->get_context();
322
 
323
        if ($context->contextlevel == CONTEXT_COURSE) {
324
            $params = ['contextinstanceid' => $context->instanceid];
325
 
326
            $sql = "SELECT usermodified
327
                      FROM {grade_outcomes}
328
                     WHERE courseid = :contextinstanceid";
329
            $userlist->add_from_sql('usermodified', $sql, $params);
330
 
331
            $sql = "SELECT loggeduser
332
                      FROM {grade_outcomes_history}
333
                     WHERE courseid = :contextinstanceid";
334
            $userlist->add_from_sql('loggeduser', $sql, $params);
335
 
336
            $sql = "SELECT userid
337
                      FROM {scale}
338
                     WHERE courseid = :contextinstanceid";
339
            $userlist->add_from_sql('userid', $sql, $params);
340
 
341
            $sql = "SELECT loggeduser, userid
342
                      FROM {scale_history}
343
                     WHERE courseid = :contextinstanceid";
344
            $userlist->add_from_sql('loggeduser', $sql, $params);
345
            $userlist->add_from_sql('userid', $sql, $params);
346
 
347
            $sql = "SELECT loggeduser
348
                      FROM {grade_items_history}
349
                     WHERE courseid = :contextinstanceid";
350
            $userlist->add_from_sql('loggeduser', $sql, $params);
351
 
352
            $sql = "SELECT ggh.userid
353
                      FROM {grade_grades_history} ggh
354
                      JOIN {grade_items} gi ON ggh.itemid = gi.id
355
                     WHERE gi.courseid = :contextinstanceid";
356
            $userlist->add_from_sql('userid', $sql, $params);
357
 
358
            $sql = "SELECT gg.userid, gg.usermodified
359
                      FROM {grade_grades} gg
360
                      JOIN {grade_items} gi ON gg.itemid = gi.id
361
                     WHERE gi.courseid = :contextinstanceid";
362
            $userlist->add_from_sql('userid', $sql, $params);
363
            $userlist->add_from_sql('usermodified', $sql, $params);
364
 
365
            $sql = "SELECT loggeduser
366
                      FROM {grade_categories_history}
367
                     WHERE courseid = :contextinstanceid";
368
            $userlist->add_from_sql('loggeduser', $sql, $params);
369
        }
370
 
371
        // None of these are currently used (user deletion).
372
        if ($context->contextlevel == CONTEXT_SYSTEM) {
373
            $params = ['contextinstanceid' => 0];
374
 
375
            $sql = "SELECT usermodified
376
                      FROM {grade_outcomes}
377
                     WHERE (courseid IS NULL OR courseid < 1)";
378
            $userlist->add_from_sql('usermodified', $sql, []);
379
 
380
            $sql = "SELECT loggeduser
381
                      FROM {grade_outcomes_history}
382
                     WHERE (courseid IS NULL OR courseid < 1)";
383
            $userlist->add_from_sql('loggeduser', $sql, []);
384
 
385
            $sql = "SELECT userid
386
                      FROM {scale}
387
                     WHERE courseid = :contextinstanceid";
388
            $userlist->add_from_sql('userid', $sql, $params);
389
 
390
            $sql = "SELECT loggeduser, userid
391
                      FROM {scale_history}
392
                     WHERE courseid = :contextinstanceid";
393
            $userlist->add_from_sql('loggeduser', $sql, $params);
394
            $userlist->add_from_sql('userid', $sql, $params);
395
        }
396
 
397
        if ($context->contextlevel == CONTEXT_USER) {
398
            // If the grade item has been removed and we have an orphan entry then we link to the
399
            // user context.
400
            $sql = "SELECT ggh.userid
401
                      FROM {grade_grades_history} ggh
402
                 LEFT JOIN {grade_items} gi ON ggh.itemid = gi.id
403
                     WHERE gi.id IS NULL
404
                       AND ggh.userid = :contextinstanceid";
405
            $userlist->add_from_sql('userid', $sql, ['contextinstanceid' => $context->instanceid]);
406
        }
407
    }
408
 
409
    /**
410
     * Export all user data for the specified user, in the specified contexts.
411
     *
412
     * @param approved_contextlist $contextlist The approved contexts to export information for.
413
     */
414
    public static function export_user_data(approved_contextlist $contextlist) {
415
        global $DB;
416
 
417
        $user = $contextlist->get_user();
418
        $userid = $user->id;
419
        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
420
            if ($context->contextlevel == CONTEXT_COURSE) {
421
                $carry[$context->contextlevel][] = $context;
422
 
423
            } else if ($context->contextlevel == CONTEXT_USER) {
424
                $carry[$context->contextlevel][] = $context;
425
 
426
            }
427
 
428
            return $carry;
429
        }, [
430
            CONTEXT_USER => [],
431
            CONTEXT_COURSE => []
432
        ]);
433
 
434
        $rootpath = [get_string('grades', 'core_grades')];
435
        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
436
 
437
        // Export the outcomes.
438
        static::export_user_data_outcomes_in_contexts($contextlist);
439
 
440
        // Export the scales.
441
        static::export_user_data_scales_in_contexts($contextlist);
442
 
443
        // Export the historical grades which have become orphans (their grade items were deleted).
444
        // We place those in ther user context of the graded user.
445
        $userids = array_values(array_map(function($context) {
446
            return $context->instanceid;
447
        }, $contexts[CONTEXT_USER]));
448
        if (!empty($userids)) {
449
 
450
            // Export own historical grades and related ones.
451
            list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
452
            list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
453
            list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
454
            $usercontext = $contexts[CONTEXT_USER];
455
            $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
456
            $sql = "
457
                SELECT $gghfields, ctx.id as ctxid
458
                  FROM {grade_grades_history} ggh
459
                  JOIN {context} ctx
460
                    ON ctx.instanceid = ggh.userid
461
                   AND ctx.contextlevel = :userlevel
462
             LEFT JOIN {grade_items} gi
463
                    ON gi.id = ggh.itemid
464
                 WHERE gi.id IS NULL
465
                   AND (ggh.userid $inuseridsql
466
                    OR ggh.usermodified $inusermodifiedsql
467
                    OR ggh.loggeduser $inloggedusersql)
468
                   AND (ggh.userid = :userid1
469
                    OR ggh.usermodified = :userid2
470
                    OR ggh.loggeduser = :userid3)
471
              ORDER BY ggh.userid, ggh.timemodified, ggh.id";
472
            $params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams,
473
                ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER]);
474
 
475
            $deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades');
476
            $recordset = $DB->get_recordset_sql($sql, $params);
477
            static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) {
478
                $context = context::instance_by_id($record->ctxid);
479
                $gghrecord = static::extract_record($record, 'ggh_');
480
 
481
                // Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades.
482
                $carry[] = [
483
                    'name' => $deletedstr,
484
                    'graded_user_was_you' => transform::yesno($userid == $gghrecord->userid),
485
                    'grade' => $gghrecord->finalgrade,
486
                    'feedback' => format_text($gghrecord->feedback, $gghrecord->feedbackformat, ['context' => $context]),
487
                    'information' => format_text($gghrecord->information, $gghrecord->informationformat, ['context' => $context]),
488
                    'timemodified' => transform::datetime($gghrecord->timemodified),
489
                    'logged_in_user_was_you' => transform::yesno($userid == $gghrecord->loggeduser),
490
                    'author_of_change_was_you' => transform::yesno($userid == $gghrecord->usermodified),
491
                    'action' => static::transform_history_action($gghrecord->action)
492
                ];
493
 
494
                return $carry;
495
 
496
            }, function($ctxid, $data) use ($rootpath) {
497
                $context = context::instance_by_id($ctxid);
498
                writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
499
            });
500
        }
501
 
502
        // Find out the course IDs.
503
        $courseids = array_values(array_map(function($context) {
504
            return $context->instanceid;
505
        }, $contexts[CONTEXT_COURSE]));
506
        if (empty($courseids)) {
507
            return;
508
        }
509
        list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
510
 
511
        // Ensure that the grades are final and do not need regrading.
512
        array_walk($courseids, function($courseid) {
513
            grade_regrade_final_grades($courseid);
514
        });
515
 
516
        // Export own grades.
517
        $ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
518
        $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
519
        $scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_');
520
        $sql = "
521
            SELECT $ggfields, $gifields, $scalefields
522
              FROM {grade_grades} gg
523
              JOIN {grade_items} gi
524
                ON gi.id = gg.itemid
525
         LEFT JOIN {scale} sc
526
                ON sc.id = gi.scaleid
527
             WHERE gi.courseid $incoursesql
528
               AND gg.userid = :userid
529
          ORDER BY gi.courseid, gi.id, gg.id";
530
        $params = array_merge($incourseparams, ['userid' => $userid]);
531
 
532
        $recordset = $DB->get_recordset_sql($sql, $params);
533
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
534
            $context = context_course::instance($record->gi_courseid);
535
            $gg = static::extract_grade_grade_from_record($record);
536
            $carry[] = static::transform_grade($gg, $context, false);
537
 
538
            return $carry;
539
 
540
        }, function($courseid, $data) use ($rootpath) {
541
            $context = context_course::instance($courseid);
542
 
543
            $pathtofiles = [
544
                get_string('grades', 'core_grades'),
545
                get_string('feedbackfiles', 'core_grades')
546
            ];
547
            foreach ($data as $key => $grades) {
548
                $gg = $grades['gradeobject'];
549
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
550
                    GRADE_FEEDBACK_FILEAREA, $gg->id);
551
                unset($data[$key]['gradeobject']); // Do not want to export this later.
552
            }
553
 
554
            writer::with_context($context)->export_data($rootpath, (object) ['grades' => $data]);
555
        });
556
 
557
        // Export own historical grades in courses.
558
        $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
559
        $sql = "
560
            SELECT $gghfields, $gifields, $scalefields
561
              FROM {grade_grades_history} ggh
562
              JOIN {grade_items} gi
563
                ON gi.id = ggh.itemid
564
         LEFT JOIN {scale} sc
565
                ON sc.id = gi.scaleid
566
             WHERE gi.courseid $incoursesql
567
               AND ggh.userid = :userid
568
          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
569
        $params = array_merge($incourseparams, ['userid' => $userid]);
570
 
571
        $recordset = $DB->get_recordset_sql($sql, $params);
572
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
573
            $context = context_course::instance($record->gi_courseid);
574
            $gg = static::extract_grade_grade_from_record($record, true);
575
            $carry[] = array_merge(static::transform_grade($gg, $context, true), [
576
                'action' => static::transform_history_action($record->ggh_action)
577
            ]);
578
            return $carry;
579
 
580
        }, function($courseid, $data) use ($rootpath) {
581
            $context = context_course::instance($courseid);
582
 
583
            $pathtofiles = [
584
                get_string('grades', 'core_grades'),
585
                get_string('feedbackhistoryfiles', 'core_grades')
586
            ];
587
            foreach ($data as $key => $grades) {
588
                /** @var grade_grade_with_history */
589
                $gg = $grades['gradeobject'];
590
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
591
                    GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
592
                unset($data[$key]['gradeobject']); // Do not want to export this later.
593
            }
594
 
595
            writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
596
        });
597
 
598
        // Export edits of categories history.
599
        $sql = "
600
            SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action
601
              FROM {grade_categories_history} gch
602
             WHERE gch.courseid $incoursesql
603
               AND gch.loggeduser = :userid
604
          ORDER BY gch.courseid, gch.timemodified, gch.id";
605
        $params = array_merge($incourseparams, ['userid' => $userid]);
606
        $recordset = $DB->get_recordset_sql($sql, $params);
607
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
608
            $carry[] = [
609
                'name' => $record->fullname,
610
                'timemodified' => transform::datetime($record->timemodified),
611
                'logged_in_user_was_you' => transform::yesno(true),
612
                'action' => static::transform_history_action($record->action),
613
            ];
614
            return $carry;
615
 
616
        }, function($courseid, $data) use ($relatedtomepath) {
617
            $context = context_course::instance($courseid);
618
            writer::with_context($context)->export_related_data($relatedtomepath, 'categories_history',
619
                (object) ['modified_records' => $data]);
620
        });
621
 
622
        // Export edits of items history.
623
        $sql = "
624
            SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action
625
              FROM {grade_items_history} gih
626
             WHERE gih.courseid $incoursesql
627
               AND gih.loggeduser = :userid
628
          ORDER BY gih.courseid, gih.timemodified, gih.id";
629
        $params = array_merge($incourseparams, ['userid' => $userid]);
630
        $recordset = $DB->get_recordset_sql($sql, $params);
631
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
632
            $carry[] = [
633
                'name' => $record->itemname,
634
                'module' => $record->itemmodule,
635
                'info' => $record->iteminfo,
636
                'timemodified' => transform::datetime($record->timemodified),
637
                'logged_in_user_was_you' => transform::yesno(true),
638
                'action' => static::transform_history_action($record->action),
639
            ];
640
            return $carry;
641
 
642
        }, function($courseid, $data) use ($relatedtomepath) {
643
            $context = context_course::instance($courseid);
644
            writer::with_context($context)->export_related_data($relatedtomepath, 'items_history',
645
                (object) ['modified_records' => $data]);
646
        });
647
 
648
        // Export edits of grades in course.
649
        $sql = "
650
            SELECT $ggfields, $gifields, $scalefields
651
              FROM {grade_grades} gg
652
              JOIN {grade_items} gi
653
                ON gg.itemid = gi.id
654
         LEFT JOIN {scale} sc
655
                ON sc.id = gi.scaleid
656
             WHERE gi.courseid $incoursesql
657
               AND gg.userid <> :userid1    -- Our grades have already been exported.
658
               AND gg.usermodified = :userid2
659
          ORDER BY gi.courseid, gg.timemodified, gg.id";
660
        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]);
661
        $recordset = $DB->get_recordset_sql($sql, $params);
662
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
663
            $context = context_course::instance($record->gi_courseid);
664
            $gg = static::extract_grade_grade_from_record($record);
665
            $carry[] = array_merge(static::transform_grade($gg, $context, false), [
666
                'userid' => transform::user($gg->userid),
667
                'created_or_modified_by_you' => transform::yesno(true),
668
            ]);
669
            return $carry;
670
 
671
        }, function($courseid, $data) use ($relatedtomepath) {
672
            $context = context_course::instance($courseid);
673
 
674
            $pathtofiles = [
675
                get_string('grades', 'core_grades'),
676
                get_string('feedbackfiles', 'core_grades')
677
            ];
678
            foreach ($data as $key => $grades) {
679
                $gg = $grades['gradeobject'];
680
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
681
                    GRADE_FEEDBACK_FILEAREA, $gg->id);
682
                unset($data[$key]['gradeobject']); // Do not want to export this later.
683
            }
684
 
685
            writer::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]);
686
        });
687
 
688
        // Export edits of grades history in course.
689
        $sql = "
690
            SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser
691
              FROM {grade_grades_history} ggh
692
              JOIN {grade_items} gi
693
                ON ggh.itemid = gi.id
694
         LEFT JOIN {scale} sc
695
                ON sc.id = gi.scaleid
696
             WHERE gi.courseid $incoursesql
697
               AND ggh.userid <> :userid1   -- We've already exported our history.
698
               AND (ggh.loggeduser = :userid2
699
                OR ggh.usermodified = :userid3)
700
          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
701
        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]);
702
        $recordset = $DB->get_recordset_sql($sql, $params);
703
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) {
704
            $context = context_course::instance($record->gi_courseid);
705
            $gg = static::extract_grade_grade_from_record($record, true);
706
            $carry[] = array_merge(static::transform_grade($gg, $context, true), [
707
                'userid' => transform::user($gg->userid),
708
                'logged_in_user_was_you' => transform::yesno($userid == $record->loggeduser),
709
                'author_of_change_was_you' => transform::yesno($userid == $gg->usermodified),
710
                'action' => static::transform_history_action($record->ggh_action),
711
            ]);
712
            return $carry;
713
 
714
        }, function($courseid, $data) use ($relatedtomepath) {
715
            $context = context_course::instance($courseid);
716
 
717
            $pathtofiles = [
718
                get_string('grades', 'core_grades'),
719
                get_string('feedbackhistoryfiles', 'core_grades')
720
            ];
721
            foreach ($data as $key => $grades) {
722
                /** @var grade_grade_with_history */
723
                $gg = $grades['gradeobject'];
724
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
725
                    GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
726
                unset($data[$key]['gradeobject']); // Do not want to export this later.
727
            }
728
 
729
            writer::with_context($context)->export_related_data($relatedtomepath, 'grades_history',
730
                (object) ['modified_records' => $data]);
731
        });
732
    }
733
 
734
    /**
735
     * Delete all data for all users in the specified context.
736
     *
737
     * @param context $context The specific context to delete data for.
738
     */
739
    public static function delete_data_for_all_users_in_context(context $context) {
740
        global $DB;
741
 
742
        switch ($context->contextlevel) {
743
            case CONTEXT_USER:
744
                // The user context is only reported when there are orphan historical grades, so we only delete those.
745
                static::delete_orphan_historical_grades($context->instanceid);
746
                break;
747
 
748
            case CONTEXT_COURSE:
749
                // We must not change the structure of the course, so we only delete user content.
750
                $itemids = static::get_item_ids_from_course_ids([$context->instanceid]);
751
                if (empty($itemids)) {
752
                    return;
753
                }
754
 
755
                self::delete_files($itemids, true);
756
                self::delete_files($itemids, false);
757
 
758
                list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
759
                $DB->delete_records_select('grade_grades', "itemid $insql", $inparams);
760
                $DB->delete_records_select('grade_grades_history', "itemid $insql", $inparams);
761
                break;
762
        }
763
 
764
    }
765
 
766
    /**
767
     * Delete all user data for the specified user, in the specified contexts.
768
     *
769
     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
770
     */
771
    public static function delete_data_for_user(approved_contextlist $contextlist) {
772
        global $DB;
773
        $userid = $contextlist->get_user()->id;
774
 
775
        $courseids = [];
776
        foreach ($contextlist->get_contexts() as $context) {
777
            if ($context->contextlevel == CONTEXT_USER && $userid == $context->instanceid) {
778
                // User attempts to delete data in their own context.
779
                static::delete_orphan_historical_grades($userid);
780
 
781
            } else if ($context->contextlevel == CONTEXT_COURSE) {
782
                // Log the list of course IDs.
783
                $courseids[] = $context->instanceid;
784
            }
785
        }
786
 
787
        $itemids = static::get_item_ids_from_course_ids($courseids);
788
        if (empty($itemids)) {
789
            // Our job here is done!
790
            return;
791
        }
792
 
793
        // Delete all the files.
794
        self::delete_files($itemids, true, [$userid]);
795
        self::delete_files($itemids, false, [$userid]);
796
 
797
        // Delete all the grades.
798
        list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
799
        $params = array_merge($inparams, ['userid' => $userid]);
800
 
801
        $DB->delete_records_select('grade_grades', "itemid $insql AND userid = :userid", $params);
802
        $DB->delete_records_select('grade_grades_history', "itemid $insql AND userid = :userid", $params);
803
    }
804
 
805
 
806
    /**
807
     * Delete multiple users within a single context.
808
     *
809
     * @param   \core_privacy\local\request\approved_userlist $userlist The approved context and user information to
810
     * delete information for.
811
     */
812
    public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
813
        global $DB;
814
 
815
        $context = $userlist->get_context();
816
        $userids = $userlist->get_userids();
817
        if ($context->contextlevel == CONTEXT_USER) {
818
            if (array_search($context->instanceid, $userids) !== false) {
819
                static::delete_orphan_historical_grades($context->instanceid);
820
            }
821
            return;
822
        }
823
 
824
        if ($context->contextlevel != CONTEXT_COURSE) {
825
            return;
826
        }
827
 
828
        $itemids = static::get_item_ids_from_course_ids([$context->instanceid]);
829
        if (empty($itemids)) {
830
            // Our job here is done!
831
            return;
832
        }
833
 
834
        // Delete all the files.
835
        self::delete_files($itemids, true, $userids);
836
        self::delete_files($itemids, false, $userids);
837
 
838
        // Delete all the grades.
839
        list($itemsql, $itemparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
840
        list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
841
        $params = array_merge($itemparams, $userparams);
842
 
843
        $DB->delete_records_select('grade_grades', "itemid $itemsql AND userid $usersql", $params);
844
        $DB->delete_records_select('grade_grades_history', "itemid $itemsql AND userid $usersql", $params);
845
    }
846
 
847
    /**
848
     * Delete orphan historical grades.
849
     *
850
     * @param int $userid The user ID.
851
     * @return void
852
     */
853
    protected static function delete_orphan_historical_grades($userid) {
854
        global $DB;
855
        $sql = "
856
            SELECT ggh.id
857
              FROM {grade_grades_history} ggh
858
         LEFT JOIN {grade_items} gi
859
                ON ggh.itemid = gi.id
860
             WHERE gi.id IS NULL
861
               AND ggh.userid = :userid";
862
        $ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
863
        if (empty($ids)) {
864
            return;
865
        }
866
        list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
867
 
868
        // First, let's delete their files.
869
        $sql = "
870
            SELECT gi.id
871
              FROM {grade_grades_history} ggh
872
              JOIN {grade_items} gi
873
                ON gi.id = ggh.itemid
874
             WHERE ggh.userid = :userid";
875
        $params = ['userid' => $userid];
876
        $gradeitems = $DB->get_records_sql($sql, $params);
877
        if ($gradeitems) {
878
            $itemids = array_keys($gradeitems);
879
            self::delete_files($itemids, true, [$userid]);
880
        }
881
 
882
        $DB->delete_records_select('grade_grades_history', "id $insql", $inparams);
883
    }
884
 
885
    /**
886
     * Export the user data related to outcomes.
887
     *
888
     * @param approved_contextlist $contextlist The approved contexts to export information for.
889
     * @return void
890
     */
891
    protected static function export_user_data_outcomes_in_contexts(approved_contextlist $contextlist) {
892
        global $DB;
893
 
894
        $rootpath = [get_string('grades', 'core_grades')];
895
        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
896
        $userid = $contextlist->get_user()->id;
897
 
898
        // Reorganise the contexts.
899
        $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
900
            if ($context->contextlevel == CONTEXT_SYSTEM) {
901
                $carry['in_system'] = true;
902
            } else if ($context->contextlevel == CONTEXT_COURSE) {
903
                $carry['courseids'][] = $context->instanceid;
904
            }
905
            return $carry;
906
        }, [
907
            'in_system' => false,
908
            'courseids' => []
909
        ]);
910
 
911
        // Construct SQL.
912
        $sqltemplateparts = [];
913
        $templateparams = [];
914
        if ($reduced['in_system']) {
915
            $sqltemplateparts[] = '{prefix}.courseid IS NULL';
916
        }
917
        if (!empty($reduced['courseids'])) {
918
            list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
919
            $sqltemplateparts[] = "{prefix}.courseid $insql";
920
            $templateparams = array_merge($templateparams, $inparams);
921
        }
922
        if (empty($sqltemplateparts)) {
923
            return;
924
        }
925
        $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
926
 
927
        // Export edited outcomes.
928
        $sqlwhere = str_replace('{prefix}', 'go', $sqltemplate);
929
        $sql = "
930
            SELECT go.id, COALESCE(go.courseid, 0) AS courseid, go.shortname, go.fullname, go.timemodified
931
              FROM {grade_outcomes} go
932
             WHERE $sqlwhere
933
               AND go.usermodified = :userid
934
          ORDER BY go.courseid, go.timemodified, go.id";
935
        $params = array_merge($templateparams, ['userid' => $userid]);
936
        $recordset = $DB->get_recordset_sql($sql, $params);
937
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
938
            $carry[] = [
939
                'shortname' => $record->shortname,
940
                'fullname' => $record->fullname,
941
                'timemodified' => transform::datetime($record->timemodified),
942
                'created_or_modified_by_you' => transform::yesno(true)
943
            ];
944
            return $carry;
945
 
946
        }, function($courseid, $data) use ($relatedtomepath) {
947
            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
948
            writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes',
949
                (object) ['outcomes' => $data]);
950
        });
951
 
952
        // Export edits of outcomes history.
953
        $sqlwhere = str_replace('{prefix}', 'goh', $sqltemplate);
954
        $sql = "
955
            SELECT goh.id, COALESCE(goh.courseid, 0) AS courseid, goh.shortname, goh.fullname, goh.timemodified, goh.action
956
              FROM {grade_outcomes_history} goh
957
             WHERE $sqlwhere
958
               AND goh.loggeduser = :userid
959
          ORDER BY goh.courseid, goh.timemodified, goh.id";
960
        $params = array_merge($templateparams, ['userid' => $userid]);
961
        $recordset = $DB->get_recordset_sql($sql, $params);
962
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
963
            $carry[] = [
964
                'shortname' => $record->shortname,
965
                'fullname' => $record->fullname,
966
                'timemodified' => transform::datetime($record->timemodified),
967
                'logged_in_user_was_you' => transform::yesno(true),
968
                'action' => static::transform_history_action($record->action)
969
            ];
970
            return $carry;
971
 
972
        }, function($courseid, $data) use ($relatedtomepath) {
973
            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
974
            writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes_history',
975
                (object) ['modified_records' => $data]);
976
        });
977
    }
978
 
979
    /**
980
     * Export the user data related to scales.
981
     *
982
     * @param approved_contextlist $contextlist The approved contexts to export information for.
983
     * @return void
984
     */
985
    protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) {
986
        global $DB;
987
 
988
        $rootpath = [get_string('grades', 'core_grades')];
989
        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
990
        $userid = $contextlist->get_user()->id;
991
 
992
        // Reorganise the contexts.
993
        $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
994
            if ($context->contextlevel == CONTEXT_SYSTEM) {
995
                $carry['in_system'] = true;
996
            } else if ($context->contextlevel == CONTEXT_COURSE) {
997
                $carry['courseids'][] = $context->instanceid;
998
            }
999
            return $carry;
1000
        }, [
1001
            'in_system' => false,
1002
            'courseids' => []
1003
        ]);
1004
 
1005
        // Construct SQL.
1006
        $sqltemplateparts = [];
1007
        $templateparams = [];
1008
        if ($reduced['in_system']) {
1009
            $sqltemplateparts[] = '{prefix}.courseid = 0';
1010
        }
1011
        if (!empty($reduced['courseids'])) {
1012
            list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
1013
            $sqltemplateparts[] = "{prefix}.courseid $insql";
1014
            $templateparams = array_merge($templateparams, $inparams);
1015
        }
1016
        if (empty($sqltemplateparts)) {
1017
            return;
1018
        }
1019
        $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
1020
 
1021
        // Export edited scales.
1022
        $sqlwhere = str_replace('{prefix}', 's', $sqltemplate);
1023
        $sql = "
1024
            SELECT s.id, s.courseid, s.name, s.timemodified
1025
              FROM {scale} s
1026
             WHERE $sqlwhere
1027
               AND s.userid = :userid
1028
          ORDER BY s.courseid, s.timemodified, s.id";
1029
        $params = array_merge($templateparams, ['userid' => $userid]);
1030
        $recordset = $DB->get_recordset_sql($sql, $params);
1031
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
1032
            $carry[] = [
1033
                'name' => $record->name,
1034
                'timemodified' => transform::datetime($record->timemodified),
1035
                'created_or_modified_by_you' => transform::yesno(true)
1036
            ];
1037
            return $carry;
1038
 
1039
        }, function($courseid, $data) use ($relatedtomepath) {
1040
            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
1041
            writer::with_context($context)->export_related_data($relatedtomepath, 'scales',
1042
                (object) ['scales' => $data]);
1043
        });
1044
 
1045
        // Export edits of scales history.
1046
        $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate);
1047
        $sql = "
1048
            SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser
1049
              FROM {scale_history} sh
1050
             WHERE $sqlwhere
1051
               AND sh.loggeduser = :userid1
1052
                OR sh.userid = :userid2
1053
          ORDER BY sh.courseid, sh.timemodified, sh.id";
1054
        $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]);
1055
        $recordset = $DB->get_recordset_sql($sql, $params);
1056
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) {
1057
            $carry[] = [
1058
                'name' => $record->name,
1059
                'timemodified' => transform::datetime($record->timemodified),
1060
                'author_of_change_was_you' => transform::yesno($record->userid == $userid),
1061
                'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid),
1062
                'action' => static::transform_history_action($record->action)
1063
            ];
1064
            return $carry;
1065
 
1066
        }, function($courseid, $data) use ($relatedtomepath) {
1067
            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
1068
            writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history',
1069
                (object) ['modified_records' => $data]);
1070
        });
1071
    }
1072
 
1073
    /**
1074
     * Extract grade_grade from a record.
1075
     *
1076
     * @param stdClass $record The record.
1077
     * @param bool $ishistory Whether we're extracting a historical grade.
1078
     * @return grade_grade
1079
     */
1080
    protected static function extract_grade_grade_from_record(stdClass $record, $ishistory = false) {
1081
        $prefix = $ishistory ? 'ggh_' : 'gg_';
1082
        $ggrecord = static::extract_record($record, $prefix);
1083
        if ($ishistory) {
1084
            $gg = new grade_grade_with_history($ggrecord, false);
1085
        } else {
1086
            $gg = new grade_grade($ggrecord, false);
1087
        }
1088
 
1089
        // There is a grade item in the record.
1090
        if (!empty($record->gi_id)) {
1091
            $gi = new grade_item(static::extract_record($record, 'gi_'), false);
1092
            $gg->grade_item = $gi;  // This is a common hack throughout the grades API.
1093
        }
1094
 
1095
        // Load the scale, when it still exists.
1096
        if (!empty($gi->scaleid) && !empty($record->sc_id)) {
1097
            $scalerec = static::extract_record($record, 'sc_');
1098
            $gi->scale = new grade_scale($scalerec, false);
1099
            $gi->scale->load_items();
1100
        }
1101
 
1102
        return $gg;
1103
    }
1104
 
1105
    /**
1106
     * Extract a record from another one.
1107
     *
1108
     * @param object $record The record to extract from.
1109
     * @param string $prefix The prefix used.
1110
     * @return object
1111
     */
1112
    protected static function extract_record($record, $prefix) {
1113
        $result = [];
1114
        $prefixlength = strlen($prefix);
1115
        foreach ($record as $key => $value) {
1116
            if (strpos($key, $prefix) === 0) {
1117
                $result[substr($key, $prefixlength)] = $value;
1118
            }
1119
        }
1120
        return (object) $result;
1121
    }
1122
 
1123
    /**
1124
     * Get fields SQL for a grade related object.
1125
     *
1126
     * @param string $target The related object.
1127
     * @param string $alias The table alias.
1128
     * @param string $prefix A prefix.
1129
     * @return string
1130
     */
1131
    protected static function get_fields_sql($target, $alias, $prefix) {
1132
        switch ($target) {
1133
            case 'grade_category':
1134
            case 'grade_grade':
1135
            case 'grade_item':
1136
            case 'grade_outcome':
1137
            case 'grade_scale':
1138
                $obj = new $target([], false);
1139
                $fields = array_merge(array_keys($obj->optional_fields), $obj->required_fields);
1140
                break;
1141
 
1142
            case 'grade_grades_history':
1143
                $fields = ['id', 'action', 'oldid', 'source', 'timemodified', 'loggeduser', 'itemid', 'userid', 'rawgrade',
1144
                    'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime',
1145
                    'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat'];
1146
                break;
1147
 
1148
            default:
1149
                throw new \coding_exception('Unrecognised target: ' . $target);
1150
                break;
1151
        }
1152
 
1153
        return implode(', ', array_map(function($field) use ($alias, $prefix) {
1154
            return "{$alias}.{$field} AS {$prefix}{$field}";
1155
        }, $fields));
1156
    }
1157
 
1158
    /**
1159
     * Get all the items IDs from course IDs.
1160
     *
1161
     * @param array $courseids The course IDs.
1162
     * @return array
1163
     */
1164
    protected static function get_item_ids_from_course_ids($courseids) {
1165
        global $DB;
1166
        if (empty($courseids)) {
1167
            return [];
1168
        }
1169
        list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
1170
        return $DB->get_fieldset_select('grade_items', 'id', "courseid $insql", $inparams);
1171
    }
1172
 
1173
    /**
1174
     * Loop and export from a recordset.
1175
     *
1176
     * @param moodle_recordset $recordset The recordset.
1177
     * @param string $splitkey The record key to determine when to export.
1178
     * @param mixed $initial The initial data to reduce from.
1179
     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
1180
     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
1181
     * @return void
1182
     */
1183
    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
1184
            callable $reducer, callable $export) {
1185
 
1186
        $data = $initial;
1187
        $lastid = null;
1188
 
1189
        foreach ($recordset as $record) {
1190
            if ($lastid !== null && $record->{$splitkey} != $lastid) {
1191
                $export($lastid, $data);
1192
                $data = $initial;
1193
            }
1194
            $data = $reducer($data, $record);
1195
            $lastid = $record->{$splitkey};
1196
        }
1197
        $recordset->close();
1198
 
1199
        if ($lastid !== null) {
1200
            $export($lastid, $data);
1201
        }
1202
    }
1203
 
1204
    /**
1205
     * Transform an history action.
1206
     *
1207
     * @param int $action The action.
1208
     * @return string
1209
     */
1210
    protected static function transform_history_action($action) {
1211
        switch ($action) {
1212
            case GRADE_HISTORY_INSERT:
1213
                return get_string('privacy:request:historyactioninsert', 'core_grades');
1214
                break;
1215
            case GRADE_HISTORY_UPDATE:
1216
                return get_string('privacy:request:historyactionupdate', 'core_grades');
1217
                break;
1218
            case GRADE_HISTORY_DELETE:
1219
                return get_string('privacy:request:historyactiondelete', 'core_grades');
1220
                break;
1221
        }
1222
 
1223
        return '?';
1224
    }
1225
 
1226
    /**
1227
     * Transform a grade.
1228
     *
1229
     * @param grade_grade $gg The grade object.
1230
     * @param context $context The context.
1231
     * @param bool $ishistory Whether we're extracting a historical grade.
1232
     * @return array
1233
     */
1234
    protected static function transform_grade(grade_grade $gg, context $context, bool $ishistory) {
1235
        $gi = $gg->load_grade_item();
1236
        $timemodified = $gg->timemodified ? transform::datetime($gg->timemodified) : null;
1237
        $timecreated = $gg->timecreated ? transform::datetime($gg->timecreated) : $timemodified; // When null we use timemodified.
1238
 
1239
        if ($gg instanceof grade_grade_with_history) {
1240
            $filearea = GRADE_HISTORY_FEEDBACK_FILEAREA;
1241
            $itemid = $gg->historyid;
1242
            $subpath = get_string('feedbackhistoryfiles', 'core_grades');
1243
        } else {
1244
            $filearea = GRADE_FEEDBACK_FILEAREA;
1245
            $itemid = $gg->id;
1246
            $subpath = get_string('feedbackfiles', 'core_grades');
1247
        }
1248
 
1249
        $pathtofiles = [
1250
            get_string('grades', 'core_grades'),
1251
            $subpath
1252
        ];
1253
        $gg->feedback = writer::with_context($gg->get_context())->rewrite_pluginfile_urls(
1254
            $pathtofiles,
1255
            GRADE_FILE_COMPONENT,
1256
            $filearea,
1257
            $itemid,
1258
            $gg->feedback
1259
        );
1260
 
1261
        return [
1262
            'gradeobject' => $gg,
1263
            'item' => $gi->get_name(),
1264
            'grade' => $gg->finalgrade,
1265
            'grade_formatted' => grade_format_gradevalue($gg->finalgrade, $gi),
1266
            'feedback' => format_text($gg->feedback, $gg->feedbackformat, ['context' => $context]),
1267
            'information' => format_text($gg->information, $gg->informationformat, ['context' => $context]),
1268
            'timecreated' => $timecreated,
1269
            'timemodified' => $timemodified,
1270
        ];
1271
    }
1272
 
1273
    /**
1274
     * Handles deleting files for a given list of grade items.
1275
     *
1276
     * If an array of userids if given then it handles deleting files for those users.
1277
     *
1278
     * @param array $itemids
1279
     * @param bool $ishistory
1280
     * @param array|null $userids
1281
     * @throws \coding_exception
1282
     * @throws \dml_exception
1283
     */
1284
    protected static function delete_files(array $itemids, bool $ishistory, array $userids = null) {
1285
        global $DB;
1286
 
1287
        list($iteminnsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
1288
        if (!is_null($userids)) {
1289
            list($userinnsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
1290
            $params = array_merge($params, $userparams);
1291
        }
1292
 
1293
        if ($ishistory) {
1294
            $gradefields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
1295
            $gradetable = 'grade_grades_history';
1296
            $tableprefix = 'ggh';
1297
            $filearea = GRADE_HISTORY_FEEDBACK_FILEAREA;
1298
        } else {
1299
            $gradefields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
1300
            $gradetable = 'grade_grades';
1301
            $tableprefix = 'gg';
1302
            $filearea = GRADE_FEEDBACK_FILEAREA;
1303
        }
1304
 
1305
        $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
1306
 
1307
        $fs = new \file_storage();
1308
        $sql = "SELECT $gradefields, $gifields
1309
                  FROM {{$gradetable}} $tableprefix
1310
                  JOIN {grade_items} gi
1311
                    ON gi.id = {$tableprefix}.itemid
1312
                 WHERE gi.id $iteminnsql ";
1313
        if (!is_null($userids)) {
1314
            $sql .= "AND {$tableprefix}.userid $userinnsql";
1315
        }
1316
 
1317
        $grades = $DB->get_recordset_sql($sql, $params);
1318
        foreach ($grades as $grade) {
1319
            $gg = static::extract_grade_grade_from_record($grade, $ishistory);
1320
            if ($gg instanceof grade_grade_with_history) {
1321
                $fileitemid = $gg->historyid;
1322
            } else {
1323
                $fileitemid = $gg->id;
1324
            }
1325
            $fs->delete_area_files($gg->get_context()->id, GRADE_FILE_COMPONENT, $filearea, $fileitemid);
1326
        }
1327
        $grades->close();
1328
    }
1329
}