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
 * Privacy Subsystem implementation for mod_quiz.
19
 *
20
 * @package    mod_quiz
21
 * @category   privacy
22
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace mod_quiz\privacy;
27
 
28
use core_privacy\local\request\approved_contextlist;
29
use core_privacy\local\request\approved_userlist;
30
use core_privacy\local\request\contextlist;
31
use core_privacy\local\request\deletion_criteria;
32
use core_privacy\local\request\transform;
33
use core_privacy\local\metadata\collection;
34
use core_privacy\local\request\userlist;
35
use core_privacy\local\request\writer;
36
use core_privacy\manager;
37
use mod_quiz\quiz_attempt;
38
 
39
defined('MOODLE_INTERNAL') || die();
40
 
41
require_once($CFG->dirroot . '/mod/quiz/lib.php');
42
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
43
 
44
/**
45
 * Privacy Subsystem implementation for mod_quiz.
46
 *
47
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
48
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49
 */
50
class provider implements
51
    // This plugin has data.
52
    \core_privacy\local\metadata\provider,
53
 
54
    // This plugin currently implements the original plugin_provider interface.
55
    \core_privacy\local\request\plugin\provider,
56
 
57
    // This plugin is capable of determining which users have data within it.
58
    \core_privacy\local\request\core_userlist_provider {
59
 
60
    /**
61
     * Get the list of contexts that contain user information for the specified user.
62
     *
63
     * @param   collection  $items  The collection to add metadata to.
64
     * @return  collection  The array of metadata
65
     */
66
    public static function get_metadata(collection $items): collection {
67
        // The table 'quiz' stores a record for each quiz.
68
        // It does not contain user personal data, but data is returned from it for contextual requirements.
69
 
70
        // The table 'quiz_attempts' stores a record of each quiz attempt.
71
        // It contains a userid which links to the user making the attempt and contains information about that attempt.
72
        $items->add_database_table('quiz_attempts', [
73
                'attempt'                    => 'privacy:metadata:quiz_attempts:attempt',
74
                'currentpage'                => 'privacy:metadata:quiz_attempts:currentpage',
75
                'preview'                    => 'privacy:metadata:quiz_attempts:preview',
76
                'state'                      => 'privacy:metadata:quiz_attempts:state',
77
                'timestart'                  => 'privacy:metadata:quiz_attempts:timestart',
78
                'timefinish'                 => 'privacy:metadata:quiz_attempts:timefinish',
79
                'timemodified'               => 'privacy:metadata:quiz_attempts:timemodified',
80
                'timemodifiedoffline'        => 'privacy:metadata:quiz_attempts:timemodifiedoffline',
81
                'timecheckstate'             => 'privacy:metadata:quiz_attempts:timecheckstate',
82
                'sumgrades'                  => 'privacy:metadata:quiz_attempts:sumgrades',
83
                'gradednotificationsenttime' => 'privacy:metadata:quiz_attempts:gradednotificationsenttime',
84
            ], 'privacy:metadata:quiz_attempts');
85
 
86
        // The table 'quiz_feedback' contains the feedback responses which will be shown to users depending upon the
87
        // grade they achieve in the quiz.
88
        // It does not identify the user who wrote the feedback item so cannot be returned directly and is not
89
        // described, but relevant feedback items will be included with the quiz export for a user who has a grade.
90
 
91
        // The table 'quiz_grades' contains the current grade for each quiz/user combination.
92
        $items->add_database_table('quiz_grades', [
93
                'quiz'                  => 'privacy:metadata:quiz_grades:quiz',
94
                'userid'                => 'privacy:metadata:quiz_grades:userid',
95
                'grade'                 => 'privacy:metadata:quiz_grades:grade',
96
                'timemodified'          => 'privacy:metadata:quiz_grades:timemodified',
97
            ], 'privacy:metadata:quiz_grades');
98
 
99
        // The table 'quiz_overrides' contains any user or group overrides for users.
100
        // It should be included where data exists for a user.
101
        $items->add_database_table('quiz_overrides', [
102
                'quiz'                  => 'privacy:metadata:quiz_overrides:quiz',
103
                'userid'                => 'privacy:metadata:quiz_overrides:userid',
104
                'timeopen'              => 'privacy:metadata:quiz_overrides:timeopen',
105
                'timeclose'             => 'privacy:metadata:quiz_overrides:timeclose',
106
                'timelimit'             => 'privacy:metadata:quiz_overrides:timelimit',
107
            ], 'privacy:metadata:quiz_overrides');
108
 
109
        // These define the structure of the quiz.
110
 
111
        // The table 'quiz_sections' contains data about the structure of a quiz.
112
        // It does not contain any user identifying data and does not need a mapping.
113
 
114
        // The table 'quiz_slots' contains data about the structure of a quiz.
115
        // It does not contain any user identifying data and does not need a mapping.
116
 
117
        // The table 'quiz_reports' does not contain any user identifying data and does not need a mapping.
118
 
119
        // The table 'quiz_statistics' contains abstract statistics about question usage and cannot be mapped to any
120
        // specific user.
121
        // It does not contain any user identifying data and does not need a mapping.
122
 
123
        // The quiz links to the 'core_question' subsystem for all question functionality.
124
        $items->add_subsystem_link('core_question', [], 'privacy:metadata:core_question');
125
 
126
        // The quiz has two subplugins..
127
        $items->add_plugintype_link('quiz', [], 'privacy:metadata:quiz');
128
        $items->add_plugintype_link('quizaccess', [], 'privacy:metadata:quizaccess');
129
 
130
        // Although the quiz supports the core_completion API and defines custom completion items, these will be
131
        // noted by the manager as all activity modules are capable of supporting this functionality.
132
 
133
        return $items;
134
    }
135
 
136
    /**
137
     * Get the list of contexts where the specified user has attempted a quiz, or been involved with manual marking
138
     * and/or grading of a quiz.
139
     *
140
     * @param   int             $userid The user to search.
141
     * @return  contextlist     $contextlist The contextlist containing the list of contexts used in this plugin.
142
     */
143
    public static function get_contexts_for_userid(int $userid): contextlist {
144
        $resultset = new contextlist();
145
 
146
        // Users who attempted the quiz.
147
        $sql = "SELECT c.id
148
                  FROM {context} c
149
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
150
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
151
                  JOIN {quiz} q ON q.id = cm.instance
152
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
153
                 WHERE qa.userid = :userid AND qa.preview = 0";
154
        $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz', 'userid' => $userid];
155
        $resultset->add_from_sql($sql, $params);
156
 
157
        // Users with quiz overrides.
158
        $sql = "SELECT c.id
159
                  FROM {context} c
160
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
161
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
162
                  JOIN {quiz} q ON q.id = cm.instance
163
                  JOIN {quiz_overrides} qo ON qo.quiz = q.id
164
                 WHERE qo.userid = :userid";
165
        $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz', 'userid' => $userid];
166
        $resultset->add_from_sql($sql, $params);
167
 
168
        // Get the SQL used to link indirect question usages for the user.
169
        // This includes where a user is the manual marker on a question attempt.
170
        $qubaid = \core_question\privacy\provider::get_related_question_usages_for_user('rel', 'mod_quiz', 'qa.uniqueid', $userid);
171
 
172
        // Select the context of any quiz attempt where a user has an attempt, plus the related usages.
173
        $sql = "SELECT c.id
174
                  FROM {context} c
175
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
176
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
177
                  JOIN {quiz} q ON q.id = cm.instance
178
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
179
            " . $qubaid->from . "
180
            WHERE " . $qubaid->where() . " AND qa.preview = 0";
181
        $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz'] + $qubaid->from_where_params();
182
        $resultset->add_from_sql($sql, $params);
183
 
184
        return $resultset;
185
    }
186
 
187
    /**
188
     * Get the list of users who have data within a context.
189
     *
190
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
191
     */
192
    public static function get_users_in_context(userlist $userlist) {
193
        $context = $userlist->get_context();
194
 
195
        if (!$context instanceof \context_module) {
196
            return;
197
        }
198
 
199
        $params = [
200
            'cmid'    => $context->instanceid,
201
            'modname' => 'quiz',
202
        ];
203
 
204
        // Users who attempted the quiz.
205
        $sql = "SELECT qa.userid
206
                  FROM {course_modules} cm
207
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
208
                  JOIN {quiz} q ON q.id = cm.instance
209
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
210
                 WHERE cm.id = :cmid AND qa.preview = 0";
211
        $userlist->add_from_sql('userid', $sql, $params);
212
 
213
        // Users with quiz overrides.
214
        $sql = "SELECT qo.userid
215
                  FROM {course_modules} cm
216
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
217
                  JOIN {quiz} q ON q.id = cm.instance
218
                  JOIN {quiz_overrides} qo ON qo.quiz = q.id
219
                 WHERE cm.id = :cmid";
220
        $userlist->add_from_sql('userid', $sql, $params);
221
 
222
        // Question usages in context.
223
        // This includes where a user is the manual marker on a question attempt.
224
        $sql = "SELECT qa.uniqueid
225
                  FROM {course_modules} cm
226
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
227
                  JOIN {quiz} q ON q.id = cm.instance
228
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
229
                 WHERE cm.id = :cmid AND qa.preview = 0";
230
        \core_question\privacy\provider::get_users_in_context_from_sql($userlist, 'qn', $sql, $params);
231
    }
232
 
233
    /**
234
     * Export all user data for the specified user, in the specified contexts.
235
     *
236
     * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
237
     */
238
    public static function export_user_data(approved_contextlist $contextlist) {
239
        global $DB;
240
 
241
        if (!count($contextlist)) {
242
            return;
243
        }
244
 
245
        $user = $contextlist->get_user();
246
        $userid = $user->id;
247
        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
248
 
249
        $sql = "SELECT
250
                    q.*,
251
                    qg.id AS hasgrade,
252
                    qg.grade AS bestgrade,
253
                    qg.timemodified AS grademodified,
254
                    qo.id AS hasoverride,
255
                    qo.timeopen AS override_timeopen,
256
                    qo.timeclose AS override_timeclose,
257
                    qo.timelimit AS override_timelimit,
258
                    c.id AS contextid,
259
                    cm.id AS cmid
260
                  FROM {context} c
261
            INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
262
            INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
263
            INNER JOIN {quiz} q ON q.id = cm.instance
264
             LEFT JOIN {quiz_overrides} qo ON qo.quiz = q.id AND qo.userid = :qouserid
265
             LEFT JOIN {quiz_grades} qg ON qg.quiz = q.id AND qg.userid = :qguserid
266
                 WHERE c.id {$contextsql}";
267
 
268
        $params = [
269
            'contextlevel'      => CONTEXT_MODULE,
270
            'modname'           => 'quiz',
271
            'qguserid'          => $userid,
272
            'qouserid'          => $userid,
273
        ];
274
        $params += $contextparams;
275
 
276
        // Fetch the individual quizzes.
277
        $quizzes = $DB->get_recordset_sql($sql, $params);
278
        foreach ($quizzes as $quiz) {
279
            list($course, $cm) = get_course_and_cm_from_cmid($quiz->cmid, 'quiz');
280
            $quizobj = new \mod_quiz\quiz_settings($quiz, $cm, $course);
281
            $context = $quizobj->get_context();
282
 
283
            $quizdata = \core_privacy\local\request\helper::get_context_data($context, $contextlist->get_user());
284
            \core_privacy\local\request\helper::export_context_files($context, $contextlist->get_user());
285
 
286
            if (!empty($quizdata->timeopen)) {
287
                $quizdata->timeopen = transform::datetime($quiz->timeopen);
288
            }
289
            if (!empty($quizdata->timeclose)) {
290
                $quizdata->timeclose = transform::datetime($quiz->timeclose);
291
            }
292
            if (!empty($quizdata->timelimit)) {
293
                $quizdata->timelimit = $quiz->timelimit;
294
            }
295
 
296
            if (!empty($quiz->hasoverride)) {
297
                $quizdata->override = (object) [];
298
 
299
                if (!empty($quizdata->override_override_timeopen)) {
300
                    $quizdata->override->timeopen = transform::datetime($quiz->override_timeopen);
301
                }
302
                if (!empty($quizdata->override_timeclose)) {
303
                    $quizdata->override->timeclose = transform::datetime($quiz->override_timeclose);
304
                }
305
                if (!empty($quizdata->override_timelimit)) {
306
                    $quizdata->override->timelimit = $quiz->override_timelimit;
307
                }
308
            }
309
 
310
            $quizdata->accessdata = (object) [];
311
 
312
            $components = \core_component::get_plugin_list('quizaccess');
313
            $exportparams = [
314
                    $quizobj,
315
                    $user,
316
                ];
317
            foreach (array_keys($components) as $component) {
318
                $classname = manager::get_provider_classname_for_component("quizaccess_$component");
319
                if (class_exists($classname) && is_subclass_of($classname, quizaccess_provider::class)) {
320
                    $result = component_class_callback($classname, 'export_quizaccess_user_data', $exportparams);
321
                    if (count((array) $result)) {
322
                        $quizdata->accessdata->$component = $result;
323
                    }
324
                }
325
            }
326
 
327
            if (empty((array) $quizdata->accessdata)) {
328
                unset($quizdata->accessdata);
329
            }
330
 
331
            writer::with_context($context)
332
                ->export_data([], $quizdata);
333
        }
334
        $quizzes->close();
335
 
336
        // Store all quiz attempt data.
337
        static::export_quiz_attempts($contextlist);
338
    }
339
 
340
    /**
341
     * Delete all data for all users in the specified context.
342
     *
343
     * @param   context                 $context   The specific context to delete data for.
344
     */
345
    public static function delete_data_for_all_users_in_context(\context $context) {
346
        if ($context->contextlevel != CONTEXT_MODULE) {
347
            // Only quiz module will be handled.
348
            return;
349
        }
350
 
351
        $cm = get_coursemodule_from_id('quiz', $context->instanceid);
352
        if (!$cm) {
353
            // Only quiz module will be handled.
354
            return;
355
        }
356
 
357
        $quizobj = \mod_quiz\quiz_settings::create($cm->instance);
358
        $quiz = $quizobj->get_quiz();
359
 
360
        // Handle the 'quizaccess' subplugin.
361
        manager::plugintype_class_callback(
362
                'quizaccess',
363
                quizaccess_provider::class,
364
                'delete_subplugin_data_for_all_users_in_context',
365
                [$quizobj]
366
            );
367
 
368
        // Delete all overrides.
369
        $quizobj->get_override_manager()->delete_all_overrides(shouldlog: false);
370
 
371
        // This will delete all question attempts, quiz attempts, and quiz grades for this quiz.
372
        quiz_delete_all_attempts($quiz);
373
    }
374
 
375
    /**
376
     * Delete all user data for the specified user, in the specified contexts.
377
     *
378
     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
379
     */
380
    public static function delete_data_for_user(approved_contextlist $contextlist) {
381
        global $DB;
382
 
383
        foreach ($contextlist as $context) {
384
            if ($context->contextlevel != CONTEXT_MODULE) {
385
            // Only quiz module will be handled.
386
                continue;
387
            }
388
 
389
            $cm = get_coursemodule_from_id('quiz', $context->instanceid);
390
            if (!$cm) {
391
                // Only quiz module will be handled.
392
                continue;
393
            }
394
 
395
            // Fetch the details of the data to be removed.
396
            $quizobj = \mod_quiz\quiz_settings::create($cm->instance);
397
            $quiz = $quizobj->get_quiz();
398
            $user = $contextlist->get_user();
399
 
400
            // Handle the 'quizaccess' quizaccess.
401
            manager::plugintype_class_callback(
402
                    'quizaccess',
403
                    quizaccess_provider::class,
404
                    'delete_quizaccess_data_for_user',
405
                    [$quizobj, $user]
406
                );
407
 
408
            // Remove overrides for this user.
409
            $overrides = $DB->get_records('quiz_overrides' , [
410
                'quiz' => $quizobj->get_quizid(),
411
                'userid' => $user->id,
412
            ]);
413
 
414
            $manager = $quizobj->get_override_manager();
415
            $manager->delete_overrides(
416
                overrides: $overrides,
417
                shouldlog: false,
418
            );
419
 
420
            // This will delete all question attempts, quiz attempts, and quiz grades for this quiz.
421
            quiz_delete_user_attempts($quizobj, $user);
422
        }
423
    }
424
 
425
    /**
426
     * Delete multiple users within a single context.
427
     *
428
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
429
     */
430
    public static function delete_data_for_users(approved_userlist $userlist) {
431
        global $DB;
432
 
433
        $context = $userlist->get_context();
434
 
435
        if ($context->contextlevel != CONTEXT_MODULE) {
436
            // Only quiz module will be handled.
437
            return;
438
        }
439
 
440
        $cm = get_coursemodule_from_id('quiz', $context->instanceid);
441
        if (!$cm) {
442
            // Only quiz module will be handled.
443
            return;
444
        }
445
 
446
        $quizobj = \mod_quiz\quiz_settings::create($cm->instance);
447
        $quiz = $quizobj->get_quiz();
448
 
449
        $userids = $userlist->get_userids();
450
 
451
        // Handle the 'quizaccess' quizaccess.
452
        manager::plugintype_class_callback(
453
                'quizaccess',
454
                quizaccess_user_provider::class,
455
                'delete_quizaccess_data_for_users',
456
                [$userlist]
457
        );
458
 
459
        foreach ($userids as $userid) {
460
            // Remove overrides for this user.
461
            $overrides = $DB->get_records('quiz_overrides' , [
462
                'quiz' => $quizobj->get_quizid(),
463
                'userid' => $userid,
464
            ]);
465
 
466
            $manager = $quizobj->get_override_manager();
467
            $manager->delete_overrides(
468
                overrides: $overrides,
469
                shouldlog: false,
470
            );
471
 
472
            // This will delete all question attempts, quiz attempts, and quiz grades for this user in the given quiz.
473
            quiz_delete_user_attempts($quizobj, (object)['id' => $userid]);
474
        }
475
    }
476
 
477
    /**
478
     * Store all quiz attempts for the contextlist.
479
     *
480
     * @param   approved_contextlist    $contextlist
481
     */
482
    protected static function export_quiz_attempts(approved_contextlist $contextlist) {
483
        global $DB;
484
 
485
        $userid = $contextlist->get_user()->id;
486
        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
487
        $qubaid1 = \core_question\privacy\provider::get_related_question_usages_for_user(
488
            'rel1',
489
            'mod_quiz',
490
            'qa.uniqueid',
491
            $userid
492
        );
493
        $qubaid2 = \core_question\privacy\provider::get_related_question_usages_for_user(
494
            'rel2',
495
            'mod_quiz',
496
            'qa.uniqueid',
497
            $userid
498
        );
499
 
500
        // The layout column causes the union in the following query to fail on Oracle, it also appears to not be used.
501
        // So we can filter the return values to be only those used to generate the data, this will have the benefit
502
        // improving performance on all databases as we will no longer be returning a text field for each row.
503
        $attemptfields = 'qa.id, qa.quiz, qa.userid, qa.attempt, qa.uniqueid, qa.preview, qa.state, qa.timestart, ' .
504
            'qa.timefinish, qa.timemodified, qa.timemodifiedoffline, qa.timecheckstate, qa.sumgrades, ' .
505
            'qa.gradednotificationsenttime';
506
 
507
        $sql = "SELECT
508
                    c.id AS contextid,
509
                    cm.id AS cmid,
510
                    $attemptfields
511
                  FROM {context} c
512
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel1
513
                  JOIN {modules} m ON m.id = cm.module AND m.name = 'quiz'
514
                  JOIN {quiz} q ON q.id = cm.instance
515
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
516
            " . $qubaid1->from. "
517
                 WHERE qa.userid = :qauserid AND qa.preview = 0
518
                 UNION
519
                SELECT
520
                    c.id AS contextid,
521
                    cm.id AS cmid,
522
                    $attemptfields
523
                  FROM {context} c
524
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel2
525
                  JOIN {modules} m ON m.id = cm.module AND m.name = 'quiz'
526
                  JOIN {quiz} q ON q.id = cm.instance
527
                  JOIN {quiz_attempts} qa ON qa.quiz = q.id
528
            " . $qubaid2->from. "
529
                 WHERE " . $qubaid2->where() . " AND qa.preview = 0
530
        ";
531
 
532
        $params = array_merge(
533
            [
534
                'contextlevel1'      => CONTEXT_MODULE,
535
                'contextlevel2'      => CONTEXT_MODULE,
536
                'qauserid'          => $userid,
537
            ],
538
            $qubaid1->from_where_params(),
539
            $qubaid2->from_where_params(),
540
        );
541
 
542
        $attempts = $DB->get_recordset_sql($sql, $params);
543
        foreach ($attempts as $attempt) {
544
            $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz]);
545
            $context = \context_module::instance($attempt->cmid);
546
            $attemptsubcontext = helper::get_quiz_attempt_subcontext($attempt, $contextlist->get_user());
547
            $options = quiz_get_review_options($quiz, $attempt, $context);
548
 
549
            if ($attempt->userid == $userid) {
550
                // This attempt was made by the user.
551
                // They 'own' all data on it.
552
                // Store the question usage data.
553
                \core_question\privacy\provider::export_question_usage($userid,
554
                        $context,
555
                        $attemptsubcontext,
556
                        $attempt->uniqueid,
557
                        $options,
558
                        true
559
                    );
560
 
561
                // Store the quiz attempt data.
562
                $data = (object) [
563
                    'state' => quiz_attempt::state_name($attempt->state),
564
                ];
565
 
566
                if (!empty($attempt->timestart)) {
567
                    $data->timestart = transform::datetime($attempt->timestart);
568
                }
569
                if (!empty($attempt->timefinish)) {
570
                    $data->timefinish = transform::datetime($attempt->timefinish);
571
                }
572
                if (!empty($attempt->timemodified)) {
573
                    $data->timemodified = transform::datetime($attempt->timemodified);
574
                }
575
                if (!empty($attempt->timemodifiedoffline)) {
576
                    $data->timemodifiedoffline = transform::datetime($attempt->timemodifiedoffline);
577
                }
578
                if (!empty($attempt->timecheckstate)) {
579
                    $data->timecheckstate = transform::datetime($attempt->timecheckstate);
580
                }
581
                if (!empty($attempt->gradednotificationsenttime)) {
582
                    $data->gradednotificationsenttime = transform::datetime($attempt->gradednotificationsenttime);
583
                }
584
 
585
                if ($options->marks == \question_display_options::MARK_AND_MAX) {
586
                    $grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
587
                    $data->grade = (object) [
588
                            'grade' => quiz_format_grade($quiz, $grade),
589
                            'feedback' => quiz_feedback_for_grade($grade, $quiz, $context),
590
                        ];
591
                }
592
 
593
                writer::with_context($context)
594
                    ->export_data($attemptsubcontext, $data);
595
            } else {
596
                // This attempt was made by another user.
597
                // The current user may have marked part of the quiz attempt.
598
                \core_question\privacy\provider::export_question_usage(
599
                        $userid,
600
                        $context,
601
                        $attemptsubcontext,
602
                        $attempt->uniqueid,
603
                        $options,
604
                        false
605
                    );
606
            }
607
        }
608
        $attempts->close();
609
    }
610
}