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    mod_lesson
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 mod_lesson\privacy;
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use context;
30
use context_helper;
31
use context_module;
32
use stdClass;
33
use core_privacy\local\metadata\collection;
34
use core_privacy\local\request\approved_contextlist;
35
use core_privacy\local\request\approved_userlist;
36
use core_privacy\local\request\helper;
37
use core_privacy\local\request\transform;
38
use core_privacy\local\request\userlist;
39
use core_privacy\local\request\writer;
40
 
41
require_once($CFG->dirroot . '/mod/lesson/locallib.php');
42
require_once($CFG->dirroot . '/mod/lesson/pagetypes/essay.php');
43
require_once($CFG->dirroot . '/mod/lesson/pagetypes/matching.php');
44
require_once($CFG->dirroot . '/mod/lesson/pagetypes/multichoice.php');
45
 
46
/**
47
 * Data provider class.
48
 *
49
 * @package    mod_lesson
50
 * @copyright  2018 Frédéric Massart
51
 * @author     Frédéric Massart <fred@branchup.tech>
52
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53
 */
54
class provider implements
55
    \core_privacy\local\metadata\provider,
56
    \core_privacy\local\request\core_userlist_provider,
57
    \core_privacy\local\request\plugin\provider,
58
    \core_privacy\local\request\user_preference_provider {
59
 
60
    /**
61
     * Returns metadata.
62
     *
63
     * @param collection $collection The initialised collection to add items to.
64
     * @return collection A listing of user data stored through this system.
65
     */
66
    public static function get_metadata(collection $collection): collection {
67
        $collection->add_database_table('lesson_attempts', [
68
            'userid' => 'privacy:metadata:attempts:userid',
69
            'pageid' => 'privacy:metadata:attempts:pageid',
70
            'answerid' => 'privacy:metadata:attempts:answerid',
71
            'retry' => 'privacy:metadata:attempts:retry',
72
            'correct' => 'privacy:metadata:attempts:correct',
73
            'useranswer' => 'privacy:metadata:attempts:useranswer',
74
            'timeseen' => 'privacy:metadata:attempts:timeseen',
75
        ], 'privacy:metadata:attempts');
76
 
77
        $collection->add_database_table('lesson_grades', [
78
            'userid' => 'privacy:metadata:grades:userid',
79
            'grade' => 'privacy:metadata:grades:grade',
80
            'completed' => 'privacy:metadata:grades:completed',
81
            // The column late is not used.
82
        ], 'privacy:metadata:grades');
83
 
84
        $collection->add_database_table('lesson_timer', [
85
            'userid' => 'privacy:metadata:timer:userid',
86
            'starttime' => 'privacy:metadata:timer:starttime',
87
            'lessontime' => 'privacy:metadata:timer:lessontime',
88
            'completed' => 'privacy:metadata:timer:completed',
89
            'timemodifiedoffline' => 'privacy:metadata:timer:timemodifiedoffline',
90
        ], 'privacy:metadata:timer');
91
 
92
        $collection->add_database_table('lesson_branch', [
93
            'userid' => 'privacy:metadata:branch:userid',
94
            'pageid' => 'privacy:metadata:branch:pageid',
95
            'retry' => 'privacy:metadata:branch:retry',
96
            'flag' => 'privacy:metadata:branch:flag',
97
            'timeseen' => 'privacy:metadata:branch:timeseen',
98
            'nextpageid' => 'privacy:metadata:branch:nextpageid',
99
        ], 'privacy:metadata:branch');
100
 
101
        $collection->add_database_table('lesson_overrides', [
102
            'userid' => 'privacy:metadata:overrides:userid',
103
            'available' => 'privacy:metadata:overrides:available',
104
            'deadline' => 'privacy:metadata:overrides:deadline',
105
            'timelimit' => 'privacy:metadata:overrides:timelimit',
106
            'review' => 'privacy:metadata:overrides:review',
107
            'maxattempts' => 'privacy:metadata:overrides:maxattempts',
108
            'retake' => 'privacy:metadata:overrides:retake',
109
            'password' => 'privacy:metadata:overrides:password',
110
        ], 'privacy:metadata:overrides');
111
 
112
        $collection->add_user_preference('lesson_view', 'privacy:metadata:userpref:lessonview');
113
 
114
        return $collection;
115
    }
116
 
117
    /**
118
     * Get the list of contexts that contain user information for the specified user.
119
     *
120
     * @param int $userid The user to search.
121
     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
122
     */
123
    public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
124
        $contextlist = new \core_privacy\local\request\contextlist();
125
 
126
        $sql = "
127
            SELECT DISTINCT ctx.id
128
              FROM {lesson} l
129
              JOIN {modules} m
130
                ON m.name = :lesson
131
              JOIN {course_modules} cm
132
                ON cm.instance = l.id
133
               AND cm.module = m.id
134
              JOIN {context} ctx
135
                ON ctx.instanceid = cm.id
136
               AND ctx.contextlevel = :modulelevel
137
         LEFT JOIN {lesson_attempts} la
138
                ON la.lessonid = l.id
139
               AND la.userid = :userid1
140
         LEFT JOIN {lesson_branch} lb
141
                ON lb.lessonid = l.id
142
               AND lb.userid = :userid2
143
         LEFT JOIN {lesson_grades} lg
144
                ON lg.lessonid = l.id
145
               AND lg.userid = :userid3
146
         LEFT JOIN {lesson_overrides} lo
147
                ON lo.lessonid = l.id
148
               AND lo.userid = :userid4
149
         LEFT JOIN {lesson_timer} lt
150
                ON lt.lessonid = l.id
151
               AND lt.userid = :userid5
152
             WHERE la.id IS NOT NULL
153
                OR lb.id IS NOT NULL
154
                OR lg.id IS NOT NULL
155
                OR lo.id IS NOT NULL
156
                OR lt.id IS NOT NULL";
157
 
158
        $params = [
159
            'lesson' => 'lesson',
160
            'modulelevel' => CONTEXT_MODULE,
161
            'userid1' => $userid,
162
            'userid2' => $userid,
163
            'userid3' => $userid,
164
            'userid4' => $userid,
165
            'userid5' => $userid,
166
        ];
167
        $contextlist->add_from_sql($sql, $params);
168
 
169
        return $contextlist;
170
    }
171
 
172
    /**
173
     * Get the list of users who have data within a context.
174
     *
175
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
176
     *
177
     */
178
    public static function get_users_in_context(userlist $userlist) {
179
        $context = $userlist->get_context();
180
 
181
        if (!is_a($context, \context_module::class)) {
182
            return;
183
        }
184
 
185
        $params = [
186
            'lesson' => 'lesson',
187
            'modulelevel' => CONTEXT_MODULE,
188
            'contextid' => $context->id,
189
        ];
190
 
191
        // Mapping of lesson tables which may contain user data.
192
        $joins = [
193
            'lesson_attempts',
194
            'lesson_branch',
195
            'lesson_grades',
196
            'lesson_overrides',
197
            'lesson_timer',
198
        ];
199
 
200
        foreach ($joins as $join) {
201
            $sql = "
202
                SELECT lx.userid
203
                  FROM {lesson} l
204
                  JOIN {modules} m
205
                    ON m.name = :lesson
206
                  JOIN {course_modules} cm
207
                    ON cm.instance = l.id
208
                   AND cm.module = m.id
209
                  JOIN {context} ctx
210
                    ON ctx.instanceid = cm.id
211
                   AND ctx.contextlevel = :modulelevel
212
                  JOIN {{$join}} lx
213
                    ON lx.lessonid = l.id
214
                 WHERE ctx.id = :contextid";
215
 
216
            $userlist->add_from_sql('userid', $sql, $params);
217
        }
218
    }
219
 
220
    /**
221
     * Export all user data for the specified user, in the specified contexts.
222
     *
223
     * @param approved_contextlist $contextlist The approved contexts to export information for.
224
     */
225
    public static function export_user_data(approved_contextlist $contextlist) {
226
        global $DB;
227
 
228
        $user = $contextlist->get_user();
229
        $userid = $user->id;
230
        $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
231
            if ($context->contextlevel == CONTEXT_MODULE) {
232
                $carry[] = $context->instanceid;
233
            }
234
            return $carry;
235
        }, []);
236
        if (empty($cmids)) {
237
            return;
238
        }
239
 
240
        // If the context export was requested, then let's at least describe the lesson.
241
        foreach ($cmids as $cmid) {
242
            $context = context_module::instance($cmid);
243
            $contextdata = helper::get_context_data($context, $user);
244
            helper::export_context_files($context, $user);
245
            writer::with_context($context)->export_data([], $contextdata);
246
        }
247
 
248
        // Find the lesson IDs.
249
        $lessonidstocmids = static::get_lesson_ids_to_cmids_from_cmids($cmids);
250
 
251
        // Prepare the common SQL fragments.
252
        list($inlessonsql, $inlessonparams) = $DB->get_in_or_equal(array_keys($lessonidstocmids), SQL_PARAMS_NAMED);
253
        $sqluserlesson = "userid = :userid AND lessonid $inlessonsql";
254
        $paramsuserlesson = array_merge($inlessonparams, ['userid' => $userid]);
255
 
256
        // Export the overrides.
257
        $recordset = $DB->get_recordset_select('lesson_overrides', $sqluserlesson, $paramsuserlesson);
258
        static::recordset_loop_and_export($recordset, 'lessonid', null, function($carry, $record) {
259
            // We know that there is only one row per lesson, so no need to use $carry.
260
            return (object) [
261
                'available' => $record->available !== null ? transform::datetime($record->available) : null,
262
                'deadline' => $record->deadline !== null ? transform::datetime($record->deadline) : null,
263
                'timelimit' => $record->timelimit !== null ? format_time($record->timelimit) : null,
264
                'review' => $record->review !== null ? transform::yesno($record->review) : null,
265
                'maxattempts' => $record->maxattempts,
266
                'retake' => $record->retake !== null ? transform::yesno($record->retake) : null,
267
                'password' => $record->password,
268
            ];
269
        }, function($lessonid, $data) use ($lessonidstocmids) {
270
            $context = context_module::instance($lessonidstocmids[$lessonid]);
271
            writer::with_context($context)->export_related_data([], 'overrides', $data);
272
        });
273
 
274
        // Export the grades.
275
        $recordset = $DB->get_recordset_select('lesson_grades', $sqluserlesson, $paramsuserlesson, 'lessonid, completed');
276
        static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) {
277
            $carry[] = (object) [
278
                'grade' => $record->grade,
279
                'completed' => transform::datetime($record->completed),
280
            ];
281
            return $carry;
282
        }, function($lessonid, $data) use ($lessonidstocmids) {
283
            $context = context_module::instance($lessonidstocmids[$lessonid]);
284
            writer::with_context($context)->export_related_data([], 'grades', (object) ['grades' => $data]);
285
        });
286
 
287
        // Export the timers.
288
        $recordset = $DB->get_recordset_select('lesson_timer', $sqluserlesson, $paramsuserlesson, 'lessonid, starttime');
289
        static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) {
290
            $carry[] = (object) [
291
                'starttime' => transform::datetime($record->starttime),
292
                'lastactivity' => transform::datetime($record->lessontime),
293
                'completed' => transform::yesno($record->completed),
294
                'timemodifiedoffline' => $record->timemodifiedoffline ? transform::datetime($record->timemodifiedoffline) : null,
295
            ];
296
            return $carry;
297
        }, function($lessonid, $data) use ($lessonidstocmids) {
298
            $context = context_module::instance($lessonidstocmids[$lessonid]);
299
            writer::with_context($context)->export_related_data([], 'timers', (object) ['timers' => $data]);
300
        });
301
 
302
        // Export the attempts and branches.
303
        $sql = "
304
            SELECT " . $DB->sql_concat('lp.id', "':'", 'COALESCE(la.id, 0)', "':'", 'COALESCE(lb.id, 0)') . " AS uniqid,
305
                   lp.lessonid,
306
 
307
                   lp.id AS page_id,
308
                   lp.qtype AS page_qtype,
309
                   lp.qoption AS page_qoption,
310
                   lp.title AS page_title,
311
                   lp.contents AS page_contents,
312
                   lp.contentsformat AS page_contentsformat,
313
 
314
                   la.id AS attempt_id,
315
                   la.retry AS attempt_retry,
316
                   la.correct AS attempt_correct,
317
                   la.useranswer AS attempt_useranswer,
318
                   la.timeseen AS attempt_timeseen,
319
 
320
                   lb.id AS branch_id,
321
                   lb.retry AS branch_retry,
322
                   lb.timeseen AS branch_timeseen,
323
 
324
                   lpb.id AS nextpage_id,
325
                   lpb.title AS nextpage_title
326
 
327
              FROM {lesson_pages} lp
328
         LEFT JOIN {lesson_attempts} la
329
                ON la.pageid = lp.id
330
               AND la.userid = :userid1
331
         LEFT JOIN {lesson_branch} lb
332
                ON lb.pageid = lp.id
333
               AND lb.userid = :userid2
334
         LEFT JOIN {lesson_pages} lpb
335
                ON lpb.id = lb.nextpageid
336
             WHERE lp.lessonid $inlessonsql
337
               AND (la.id IS NOT NULL OR lb.id IS NOT NULL)
338
          ORDER BY lp.lessonid, lp.id, la.retry, lb.retry, la.id, lb.id";
339
        $params = array_merge($inlessonparams, ['userid1' => $userid, 'userid2' => $userid]);
340
 
341
        $recordset = $DB->get_recordset_sql($sql, $params);
342
        static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) use ($lessonidstocmids) {
343
            $context = context_module::instance($lessonidstocmids[$record->lessonid]);
344
            $options = ['context' => $context];
345
 
346
            $take = isset($record->attempt_retry) ? $record->attempt_retry : $record->branch_retry;
347
            if (!isset($carry[$take])) {
348
                $carry[$take] = (object) [
349
                    'number' => $take + 1,
350
                    'answers' => [],
351
                    'jumps' => []
352
                ];
353
            }
354
 
355
            $pagefilespath = [get_string('privacy:path:pages', 'mod_lesson'), $record->page_id];
356
            writer::with_context($context)->export_area_files($pagefilespath, 'mod_lesson', 'page_contents', $record->page_id);
357
            $pagecontents = format_text(
358
                writer::with_context($context)->rewrite_pluginfile_urls(
359
                    $pagefilespath,
360
                    'mod_lesson',
361
                    'page_contents',
362
                    $record->page_id,
363
                    $record->page_contents
364
                ),
365
                $record->page_contentsformat,
366
                $options
367
            );
368
 
369
            $pagebase = [
370
                'id' => $record->page_id,
371
                'page' => $record->page_title,
372
                'contents' => $pagecontents,
373
                'contents_files_folder' => implode('/', $pagefilespath)
374
            ];
375
 
376
            if (isset($record->attempt_id)) {
377
                $carry[$take]->answers[] = array_merge($pagebase, static::transform_attempt($record, $context));
378
 
379
            } else if (isset($record->branch_id)) {
380
                if (!empty($record->nextpage_id)) {
381
                    $wentto = $record->nextpage_title . " (id: {$record->nextpage_id})";
382
                } else {
383
                    $wentto = get_string('endoflesson', 'mod_lesson');
384
                }
385
                $carry[$take]->jumps[] = array_merge($pagebase, [
386
                    'went_to' => $wentto,
387
                    'timeseen' => transform::datetime($record->attempt_timeseen)
388
                ]);
389
            }
390
 
391
            return $carry;
392
 
393
        }, function($lessonid, $data) use ($lessonidstocmids) {
394
            $context = context_module::instance($lessonidstocmids[$lessonid]);
395
            writer::with_context($context)->export_related_data([], 'attempts', (object) [
396
                'attempts' => array_values($data)
397
            ]);
398
        });
399
    }
400
 
401
    /**
402
     * Export all user preferences for the plugin.
403
     *
404
     * @param int $userid The userid of the user whose data is to be exported.
405
     */
406
    public static function export_user_preferences(int $userid) {
407
        $lessonview = get_user_preferences('lesson_view', null, $userid);
408
        if ($lessonview !== null) {
409
            $value = $lessonview;
410
 
411
            // The code seems to indicate that there also is the option 'simple', but it's not
412
            // described nor accessible from anywhere so we won't describe it more than being 'simple'.
413
            if ($lessonview == 'full') {
414
                $value = get_string('full', 'mod_lesson');
415
            } else if ($lessonview == 'collapsed') {
416
                $value = get_string('collapsed', 'mod_lesson');
417
            }
418
 
419
            writer::export_user_preference('mod_lesson', 'lesson_view', $lessonview,
420
                get_string('privacy:metadata:userpref:lessonview', 'mod_lesson'));
421
        }
422
    }
423
 
424
    /**
425
     * Delete all data for all users in the specified context.
426
     *
427
     * @param context $context The specific context to delete data for.
428
     */
429
    public static function delete_data_for_all_users_in_context(context $context) {
430
        global $DB;
431
 
432
        if ($context->contextlevel != CONTEXT_MODULE) {
433
            return;
434
        }
435
 
436
        if (!$lessonid = static::get_lesson_id_from_context($context)) {
437
            return;
438
        }
439
 
440
        $DB->delete_records('lesson_attempts', ['lessonid' => $lessonid]);
441
        $DB->delete_records('lesson_branch', ['lessonid' => $lessonid]);
442
        $DB->delete_records('lesson_grades', ['lessonid' => $lessonid]);
443
        $DB->delete_records('lesson_timer', ['lessonid' => $lessonid]);
444
        $DB->delete_records_select('lesson_overrides', 'lessonid = :id AND userid IS NOT NULL', ['id' => $lessonid]);
445
 
446
        $fs = get_file_storage();
447
        $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses');
448
        $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers');
449
    }
450
 
451
    /**
452
     * Delete all user data for the specified user, in the specified contexts.
453
     *
454
     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
455
     */
456
    public static function delete_data_for_user(approved_contextlist $contextlist) {
457
        global $DB;
458
 
459
        $userid = $contextlist->get_user()->id;
460
        $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
461
            if ($context->contextlevel == CONTEXT_MODULE) {
462
                $carry[] = $context->instanceid;
463
            }
464
            return $carry;
465
        }, []);
466
        if (empty($cmids)) {
467
            return;
468
        }
469
 
470
        // Find the lesson IDs.
471
        $lessonidstocmids = static::get_lesson_ids_to_cmids_from_cmids($cmids);
472
        $lessonids = array_keys($lessonidstocmids);
473
        if (empty($lessonids)) {
474
            return;
475
        }
476
 
477
        // Prepare the SQL we'll need below.
478
        list($insql, $inparams) = $DB->get_in_or_equal($lessonids, SQL_PARAMS_NAMED);
479
        $sql = "lessonid $insql AND userid = :userid";
480
        $params = array_merge($inparams, ['userid' => $userid]);
481
 
482
        // Delete the attempt files.
483
        $fs = get_file_storage();
484
        $recordset = $DB->get_recordset_select('lesson_attempts', $sql, $params, '', 'id, lessonid');
485
        foreach ($recordset as $record) {
486
            $cmid = $lessonidstocmids[$record->lessonid];
487
            $context = context_module::instance($cmid);
488
            $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $record->id);
489
            $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $record->id);
490
        }
491
        $recordset->close();
492
 
493
        // Delete all the things.
494
        $DB->delete_records_select('lesson_attempts', $sql, $params);
495
        $DB->delete_records_select('lesson_branch', $sql, $params);
496
        $DB->delete_records_select('lesson_grades', $sql, $params);
497
        $DB->delete_records_select('lesson_timer', $sql, $params);
498
        $DB->delete_records_select('lesson_overrides', $sql, $params);
499
    }
500
 
501
    /**
502
     * Delete multiple users within a single context.
503
     *
504
     * @param   approved_userlist    $userlist The approved context and user information to delete information for.
505
     */
506
    public static function delete_data_for_users(approved_userlist $userlist) {
507
        global $DB;
508
 
509
        $context = $userlist->get_context();
510
        $lessonid = static::get_lesson_id_from_context($context);
511
        $userids = $userlist->get_userids();
512
 
513
        if (empty($lessonid)) {
514
            return;
515
        }
516
 
517
        // Prepare the SQL we'll need below.
518
        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
519
        $sql = "lessonid = :lessonid AND userid {$insql}";
520
        $params = array_merge($inparams, ['lessonid' => $lessonid]);
521
 
522
        // Delete the attempt files.
523
        $fs = get_file_storage();
524
        $recordset = $DB->get_recordset_select('lesson_attempts', $sql, $params, '', 'id, lessonid');
525
        foreach ($recordset as $record) {
526
            $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $record->id);
527
            $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $record->id);
528
        }
529
        $recordset->close();
530
 
531
        // Delete all the things.
532
        $DB->delete_records_select('lesson_attempts', $sql, $params);
533
        $DB->delete_records_select('lesson_branch', $sql, $params);
534
        $DB->delete_records_select('lesson_grades', $sql, $params);
535
        $DB->delete_records_select('lesson_timer', $sql, $params);
536
        $DB->delete_records_select('lesson_overrides', $sql, $params);
537
    }
538
 
539
    /**
540
     * Get a survey ID from its context.
541
     *
542
     * @param context_module $context The module context.
543
     * @return int
544
     */
545
    protected static function get_lesson_id_from_context(context_module $context) {
546
        $cm = get_coursemodule_from_id('lesson', $context->instanceid);
547
        return $cm ? (int) $cm->instance : 0;
548
    }
549
 
550
    /**
551
     * Return a dict of lesson IDs mapped to their course module ID.
552
     *
553
     * @param array $cmids The course module IDs.
554
     * @return array In the form of [$lessonid => $cmid].
555
     */
556
    protected static function get_lesson_ids_to_cmids_from_cmids(array $cmids) {
557
        global $DB;
558
        list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
559
        $sql = "
560
            SELECT l.id, cm.id AS cmid
561
              FROM {lesson} l
562
              JOIN {modules} m
563
                ON m.name = :lesson
564
              JOIN {course_modules} cm
565
                ON cm.instance = l.id
566
               AND cm.module = m.id
567
             WHERE cm.id $insql";
568
        $params = array_merge($inparams, ['lesson' => 'lesson']);
569
        return $DB->get_records_sql_menu($sql, $params);
570
    }
571
 
572
    /**
573
     * Loop and export from a recordset.
574
     *
575
     * @param moodle_recordset $recordset The recordset.
576
     * @param string $splitkey The record key to determine when to export.
577
     * @param mixed $initial The initial data to reduce from.
578
     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
579
     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
580
     * @return void
581
     */
582
    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
583
            callable $reducer, callable $export) {
584
 
585
        $data = $initial;
586
        $lastid = null;
587
 
588
        foreach ($recordset as $record) {
589
            if ($lastid && $record->{$splitkey} != $lastid) {
590
                $export($lastid, $data);
591
                $data = $initial;
592
            }
593
            $data = $reducer($data, $record);
594
            $lastid = $record->{$splitkey};
595
        }
596
        $recordset->close();
597
 
598
        if (!empty($lastid)) {
599
            $export($lastid, $data);
600
        }
601
    }
602
 
603
    /**
604
     * Transform an attempt.
605
     *
606
     * @param stdClass $data Data from the database, as per the exporting method.
607
     * @param context_module $context The module context.
608
     * @return array
609
     */
610
    protected static function transform_attempt(stdClass $data, context_module $context) {
611
        global $DB;
612
 
613
        $options = ['context' => $context];
614
        $answer = $data->attempt_useranswer;
615
        $response = null;
616
        $responsefilesfolder = null;
617
 
618
        if ($answer !== null) {
619
            if ($data->page_qtype == LESSON_PAGE_ESSAY) {
620
                // Essay questions serialise data in the answer field.
621
                $info = \lesson_page_type_essay::extract_useranswer($answer);
622
                $answerfilespath = [get_string('privacy:path:essayanswers', 'mod_lesson'), $data->attempt_id];
623
                $answer = format_text(
624
                    writer::with_context($context)->rewrite_pluginfile_urls(
625
                        $answerfilespath,
626
                        'mod_lesson',
627
                        'essay_answers',
628
                        $data->attempt_id,
629
                        $info->answer
630
                    ),
631
                    $info->answerformat,
632
                    $options
633
                );
634
                writer::with_context($context)->export_area_files($answerfilespath, 'mod_lesson',
635
                    'essay_answers', $data->page_id);
636
 
637
                if ($info->response !== null) {
638
                    // We export the files in a subfolder to avoid conflicting files, and tell the user
639
                    // where those files were exported. That is because we are not using a subfolder for
640
                    // every single essay response.
641
                    $responsefilespath = [get_string('privacy:path:essayresponses', 'mod_lesson'), $data->attempt_id];
642
                    $responsefilesfolder = implode('/', $responsefilespath);
643
                    $response = format_text(
644
                        writer::with_context($context)->rewrite_pluginfile_urls(
645
                            $responsefilespath,
646
                            'mod_lesson',
647
                            'essay_responses',
648
                            $data->attempt_id,
649
                            $info->response
650
                        ),
651
                        $info->responseformat,
652
                        $options
653
                    );
654
                    writer::with_context($context)->export_area_files($responsefilespath, 'mod_lesson',
655
                        'essay_responses', $data->page_id);
656
 
657
                }
658
 
659
            } else if ($data->page_qtype == LESSON_PAGE_MULTICHOICE && $data->page_qoption) {
660
                // Multiple choice quesitons with multiple answers encode the answers.
661
                list($insql, $inparams) = $DB->get_in_or_equal(explode(',', $answer), SQL_PARAMS_NAMED);
662
                $orderby = 'id, ' . $DB->sql_order_by_text('answer') . ', answerformat';
663
                $records = $DB->get_records_select('lesson_answers', "id $insql", $inparams, $orderby);
664
                $answer = array_values(array_map(function($record) use ($options) {
665
                    return format_text($record->answer, $record->answerformat, $options);
666
                }, empty($records) ? [] : $records));
667
 
668
            } else if ($data->page_qtype == LESSON_PAGE_MATCHING) {
669
                // Matching questions need sorting.
670
                $chosen = explode(',', $answer);
671
                $answers = $DB->get_records_select('lesson_answers', 'pageid = :pageid', ['pageid' => $data->page_id],
672
                    'id', 'id, answer, answerformat', 2); // The two first entries are not options.
673
                $i = -1;
674
                $answer = array_values(array_map(function($record) use (&$i, $chosen, $options) {
675
                    $i++;
676
                    return [
677
                        'label' => format_text($record->answer, $record->answerformat, $options),
678
                        'matched_with' => array_key_exists($i, $chosen) ? $chosen[$i] : null
679
                    ];
680
                }, empty($answers) ? [] : $answers));
681
            }
682
        }
683
 
684
        $result = [
685
            'answer' => $answer,
686
            'correct' => transform::yesno($data->attempt_correct),
687
            'timeseen' => transform::datetime($data->attempt_timeseen),
688
        ];
689
 
690
        if ($response !== null) {
691
            $result['response'] = $response;
692
            $result['response_files_folder'] = $responsefilesfolder;
693
        }
694
 
695
        return $result;
696
    }
697
 
698
}