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_feedback
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_feedback\privacy;
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use context;
30
use context_helper;
31
use stdClass;
32
use core_privacy\local\metadata\collection;
33
use core_privacy\local\request\approved_contextlist;
34
use core_privacy\local\request\approved_userlist;
35
use core_privacy\local\request\contextlist;
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/feedback/lib.php');
42
 
43
/**
44
 * Data provider class.
45
 *
46
 * @package    mod_feedback
47
 * @copyright  2018 Frédéric Massart
48
 * @author     Frédéric Massart <fred@branchup.tech>
49
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50
 */
51
class provider implements
52
    \core_privacy\local\metadata\provider,
53
    \core_privacy\local\request\core_userlist_provider,
54
    \core_privacy\local\request\plugin\provider {
55
 
56
    /**
57
     * Returns metadata.
58
     *
59
     * @param collection $collection The initialised collection to add items to.
60
     * @return collection A listing of user data stored through this system.
61
     */
62
    public static function get_metadata(collection $collection): collection {
63
        $completedfields = [
64
            'userid' => 'privacy:metadata:completed:userid',
65
            'timemodified' => 'privacy:metadata:completed:timemodified',
66
            'anonymous_response' => 'privacy:metadata:completed:anonymousresponse',
67
        ];
68
 
69
        $collection->add_database_table('feedback_completed', $completedfields, 'privacy:metadata:completed');
70
        $collection->add_database_table('feedback_completedtmp', $completedfields, 'privacy:metadata:completedtmp');
71
 
72
        $valuefields = [
73
            'value' => 'privacy:metadata:value:value'
74
        ];
75
 
76
        $collection->add_database_table('feedback_value', $valuefields, 'privacy:metadata:value');
77
        $collection->add_database_table('feedback_valuetmp', $valuefields, 'privacy:metadata:valuetmp');
78
 
79
        return $collection;
80
    }
81
 
82
    /**
83
     * Get the list of contexts that contain user information for the specified user.
84
     *
85
     * @param int $userid The user to search.
86
     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
87
     */
88
    public static function get_contexts_for_userid(int $userid): contextlist {
89
        $sql = "
90
            SELECT DISTINCT ctx.id
91
              FROM {%s} fc
92
              JOIN {modules} m
93
                ON m.name = :feedback
94
              JOIN {course_modules} cm
95
                ON cm.instance = fc.feedback
96
               AND cm.module = m.id
97
              JOIN {context} ctx
98
                ON ctx.instanceid = cm.id
99
               AND ctx.contextlevel = :modlevel
100
             WHERE fc.userid = :userid";
101
        $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid];
102
        $contextlist = new contextlist();
103
        $contextlist->add_from_sql(sprintf($sql, 'feedback_completed'), $params);
104
        $contextlist->add_from_sql(sprintf($sql, 'feedback_completedtmp'), $params);
105
        return $contextlist;
106
    }
107
 
108
    /**
109
     * Get the list of users who have data within a context.
110
     *
111
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
112
     *
113
     */
114
    public static function get_users_in_context(userlist $userlist) {
115
        $context = $userlist->get_context();
116
 
117
        if (!is_a($context, \context_module::class)) {
118
            return;
119
        }
120
 
121
        // Find users with feedback entries.
122
        $sql = "
123
            SELECT fc.userid
124
              FROM {%s} fc
125
              JOIN {modules} m
126
                ON m.name = :feedback
127
              JOIN {course_modules} cm
128
                ON cm.instance = fc.feedback
129
               AND cm.module = m.id
130
              JOIN {context} ctx
131
                ON ctx.instanceid = cm.id
132
               AND ctx.contextlevel = :modlevel
133
             WHERE ctx.id = :contextid";
134
        $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
135
 
136
        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params);
137
        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $params);
138
    }
139
 
140
    /**
141
     * Export all user data for the specified user, in the specified contexts.
142
     *
143
     * @param approved_contextlist $contextlist The approved contexts to export information for.
144
     */
145
    public static function export_user_data(approved_contextlist $contextlist) {
146
        global $DB;
147
 
148
        $user = $contextlist->get_user();
149
        $userid = $user->id;
150
        $contextids = array_map(function($context) {
151
            return $context->id;
152
        }, array_filter($contextlist->get_contexts(), function($context) {
153
            return $context->contextlevel == CONTEXT_MODULE;
154
        }));
155
 
156
        if (empty($contextids)) {
157
            return;
158
        }
159
 
160
        $flushdata = function($context, $data) use ($user) {
161
            $contextdata = helper::get_context_data($context, $user);
162
            helper::export_context_files($context, $user);
163
            $mergeddata = array_merge((array) $contextdata, (array) $data);
164
 
165
            // Drop the temporary keys.
166
            if (array_key_exists('submissions', $mergeddata)) {
167
                $mergeddata['submissions'] = array_values($mergeddata['submissions']);
168
            }
169
 
170
            writer::with_context($context)->export_data([], (object) $mergeddata);
171
        };
172
 
173
        $lastctxid = null;
174
        $data = (object) [];
175
        list($sql, $params) = static::prepare_export_query($contextids, $userid);
176
        $recordset = $DB->get_recordset_sql($sql, $params);
177
        foreach ($recordset as $record) {
178
            if ($lastctxid && $lastctxid != $record->contextid) {
179
                $flushdata(context::instance_by_id($lastctxid), $data);
180
                $data = (object) [];
181
            }
182
 
183
            context_helper::preload_from_record($record);
184
            $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid;
185
 
186
            if (!isset($data->submissions)) {
187
                $data->submissions = [];
188
            }
189
 
190
            if (!isset($data->submissions[$id])) {
191
                $data->submissions[$id] = [
192
                    'inprogress' => transform::yesno($record->istmp),
193
                    'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES),
194
                    'timemodified' => transform::datetime($record->timemodified),
195
                    'answers' => []
196
                ];
197
            }
198
            $item = static::extract_item_record_from_record($record);
199
            $value = static::extract_value_record_from_record($record);
200
            $itemobj = feedback_get_item_class($record->itemtyp);
201
            $data->submissions[$id]['answers'][] = [
202
                'question' => format_text($record->itemname, FORMAT_HTML, [
203
                    'context' => context::instance_by_id($record->contextid),
204
                    'para' => false,
205
                    'noclean' => true,
206
                ]),
207
                'answer' => $itemobj->get_printval($item, $value)
208
            ];
209
 
210
            $lastctxid = $record->contextid;
211
        }
212
 
213
        if (!empty($lastctxid)) {
214
            $flushdata(context::instance_by_id($lastctxid), $data);
215
        }
216
 
217
        $recordset->close();
218
    }
219
 
220
    /**
221
     * Delete all data for all users in the specified context.
222
     *
223
     * @param context $context The specific context to delete data for.
224
     */
225
    public static function delete_data_for_all_users_in_context(\context $context) {
226
        global $DB;
227
 
228
        // This should not happen, but just in case.
229
        if ($context->contextlevel != CONTEXT_MODULE) {
230
            return;
231
        }
232
 
233
        // Prepare SQL to gather all completed IDs.
234
 
235
        $completedsql = "
236
            SELECT fc.id
237
              FROM {%s} fc
238
              JOIN {modules} m
239
                ON m.name = :feedback
240
              JOIN {course_modules} cm
241
                ON cm.instance = fc.feedback
242
               AND cm.module = m.id
243
             WHERE cm.id = :cmid";
244
        $completedparams = ['cmid' => $context->instanceid, 'feedback' => 'feedback'];
245
 
246
        // Delete temp answers and submissions.
247
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
248
        if (!empty($completedtmpids)) {
249
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
250
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
251
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
252
        }
253
 
254
        // Delete answers and submissions.
255
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
256
        if (!empty($completedids)) {
257
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
258
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
259
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
260
        }
261
    }
262
 
263
    /**
264
     * Delete all user data for the specified user, in the specified contexts.
265
     *
266
     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
267
     */
268
    public static function delete_data_for_user(approved_contextlist $contextlist) {
269
        global $DB;
270
        $userid = $contextlist->get_user()->id;
271
 
272
        // Ensure that we only act on module contexts.
273
        $contextids = array_map(function($context) {
274
            return $context->instanceid;
275
        }, array_filter($contextlist->get_contexts(), function($context) {
276
            return $context->contextlevel == CONTEXT_MODULE;
277
        }));
278
 
279
        // Prepare SQL to gather all completed IDs.
280
        list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
281
        $completedsql = "
282
            SELECT fc.id
283
              FROM {%s} fc
284
              JOIN {modules} m
285
                ON m.name = :feedback
286
              JOIN {course_modules} cm
287
                ON cm.instance = fc.feedback
288
               AND cm.module = m.id
289
             WHERE fc.userid = :userid
290
               AND cm.id $insql";
291
        $completedparams = array_merge($inparams, ['userid' => $userid, 'feedback' => 'feedback']);
292
 
293
        // Delete all submissions in progress.
294
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
295
        if (!empty($completedtmpids)) {
296
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
297
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
298
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
299
        }
300
 
301
        // Delete all final submissions.
302
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
303
        if (!empty($completedids)) {
304
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
305
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
306
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
307
        }
308
    }
309
 
310
    /**
311
     * Delete multiple users within a single context.
312
     *
313
     * @param   approved_userlist    $userlist The approved context and user information to delete information for.
314
     */
315
    public static function delete_data_for_users(approved_userlist $userlist) {
316
        global $DB;
317
 
318
        $context = $userlist->get_context();
319
        $userids = $userlist->get_userids();
320
 
321
        // Prepare SQL to gather all completed IDs.
322
        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
323
        $completedsql = "
324
            SELECT fc.id
325
              FROM {%s} fc
326
              JOIN {modules} m
327
                ON m.name = :feedback
328
              JOIN {course_modules} cm
329
                ON cm.instance = fc.feedback
330
               AND cm.module = m.id
331
             WHERE cm.id = :instanceid
332
               AND fc.userid $insql";
333
        $completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']);
334
 
335
        // Delete all submissions in progress.
336
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
337
        if (!empty($completedtmpids)) {
338
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
339
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
340
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
341
        }
342
 
343
        // Delete all final submissions.
344
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
345
        if (!empty($completedids)) {
346
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
347
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
348
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
349
        }
350
    }
351
 
352
    /**
353
     * Extract an item record from a database record.
354
     *
355
     * @param stdClass $record The record.
356
     * @return The item record.
357
     */
358
    protected static function extract_item_record_from_record(stdClass $record) {
359
        $newrec = new stdClass();
360
        foreach ($record as $key => $value) {
361
            if (strpos($key, 'item') !== 0) {
362
                continue;
363
            }
364
            $key = substr($key, 4);
365
            $newrec->{$key} = $value;
366
        }
367
        return $newrec;
368
    }
369
 
370
    /**
371
     * Extract a value record from a database record.
372
     *
373
     * @param stdClass $record The record.
374
     * @return The value record.
375
     */
376
    protected static function extract_value_record_from_record(stdClass $record) {
377
        $newrec = new stdClass();
378
        foreach ($record as $key => $value) {
379
            if (strpos($key, 'value') !== 0) {
380
                continue;
381
            }
382
            $key = substr($key, 5);
383
            $newrec->{$key} = $value;
384
        }
385
        return $newrec;
386
    }
387
 
388
    /**
389
     * Prepare the query to export all data.
390
     *
391
     * Doing it this way allows for merging all records from both the temporary and final tables
392
     * as most of their columns are shared. It is a lot easier to deal with the records when
393
     * exporting as we do not need to try to manually group the two types of submissions in the
394
     * same reported dataset.
395
     *
396
     * The ordering may affect performance on large datasets.
397
     *
398
     * @param array $contextids The context IDs.
399
     * @param int $userid The user ID.
400
     * @return array With SQL and params.
401
     */
402
    protected static function prepare_export_query(array $contextids, $userid) {
403
        global $DB;
404
 
405
        $makefetchsql = function($istmp) use ($DB, $contextids, $userid) {
406
            $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
407
            list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
408
 
409
            $i = $istmp ? 0 : 1;
410
            $istmpsqlval = $istmp ? 1 : 0;
411
            $prefix = $istmp ? 'idtmp' : 'id';
412
            $uniqid = $DB->sql_concat("'$prefix'", 'fc.id');
413
 
414
            $sql = "
415
                SELECT $uniqid AS uniqid,
416
                       f.id AS feedbackid,
417
                       ctx.id AS contextid,
418
 
419
                       $istmpsqlval AS istmp,
420
                       fc.id AS submissionid,
421
                       fc.anonymous_response AS anonymousresponse,
422
                       fc.timemodified AS timemodified,
423
 
424
                       fv.id AS valueid,
425
                       fv.course_id AS valuecourse_id,
426
                       fv.item AS valueitem,
427
                       fv.completed AS valuecompleted,
428
                       fv.tmp_completed AS valuetmp_completed,
429
 
430
                       $ctxfields
431
                  FROM {context} ctx
432
                  JOIN {course_modules} cm
433
                    ON cm.id = ctx.instanceid
434
                  JOIN {feedback} f
435
                    ON f.id = cm.instance
436
                  JOIN {%s} fc
437
                    ON fc.feedback = f.id
438
                  JOIN {%s} fv
439
                    ON fv.completed = fc.id
440
                 WHERE ctx.id $insql
441
                   AND fc.userid = :userid{$i}";
442
 
443
            $params = array_merge($inparams, [
444
                'userid' . $i => $userid,
445
            ]);
446
 
447
            $completedtbl = $istmp ? 'feedback_completedtmp' : 'feedback_completed';
448
            $valuetbl = $istmp ? 'feedback_valuetmp' : 'feedback_value';
449
            return [sprintf($sql, $completedtbl, $valuetbl), $params];
450
        };
451
 
452
        list($nontmpsql, $nontmpparams) = $makefetchsql(false);
453
        list($tmpsql, $tmpparams) = $makefetchsql(true);
454
 
455
        // Oracle does not support UNION on text fields, therefore we must get the itemdescription
456
        // and valuevalue after doing the union by joining on the result.
457
        $sql = "
458
            SELECT q.*,
459
 
460
                   COALESCE(fv.value, fvt.value) AS valuevalue,
461
 
462
                   fi.id AS itemid,
463
                   fi.feedback AS itemfeedback,
464
                   fi.template AS itemtemplate,
465
                   fi.name AS itemname,
466
                   fi.label AS itemlabel,
467
                   fi.presentation AS itempresentation,
468
                   fi.typ AS itemtyp,
469
                   fi.hasvalue AS itemhasvalue,
470
                   fi.position AS itemposition,
471
                   fi.required AS itemrequired,
472
                   fi.dependitem AS itemdependitem,
473
                   fi.dependvalue AS itemdependvalue,
474
                   fi.options AS itemoptions
475
 
476
              FROM ($nontmpsql UNION $tmpsql) q
477
         LEFT JOIN {feedback_value} fv
478
                ON fv.id = q.valueid AND q.istmp = 0
479
         LEFT JOIN {feedback_valuetmp} fvt
480
                ON fvt.id = q.valueid AND q.istmp = 1
481
              JOIN {feedback_item} fi
482
                ON (fi.id = fv.item OR fi.id = fvt.item)
483
          ORDER BY q.contextid, q.istmp, q.submissionid, q.valueid";
484
        $params = array_merge($nontmpparams, $tmpparams);
485
 
486
        return [$sql, $params];
487
    }
488
}