Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.97% covered (warning)
68.97%
60 / 87
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
provider
68.97% covered (warning)
68.97%
60 / 87
50.00% covered (danger)
50.00%
4 / 8
51.43
0.00% covered (danger)
0.00%
0 / 1
 get_metadata
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 get_contexts_for_userid
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 get_users_in_context
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 export_user_data
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
8
 delete_data_for_all_users_in_context
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 delete_data_for_user
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 delete_data_for_users
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 delete_user
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
17namespace local_doublemark\privacy;
18
19use core_privacy\local\metadata\collection;
20use core_privacy\local\request\approved_contextlist;
21use core_privacy\local\request\approved_userlist;
22use core_privacy\local\request\contextlist;
23use core_privacy\local\request\transform;
24use core_privacy\local\request\userlist;
25use core_privacy\local\request\writer;
26use local_doublemark\local\double_marking;
27
28/**
29 * Privacy provider: marks and comments about students, and who gave them.
30 *
31 * Deleting a marker's data keeps the marks they gave, without their id, because removing
32 * a mark would change a student's agreed grade.
33 *
34 * @package    local_doublemark
35 * @copyright  2026 DevLMS <contact@devlms.com>
36 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class provider implements
39    \core_privacy\local\metadata\provider,
40    \core_privacy\local\request\core_userlist_provider,
41    \core_privacy\local\request\plugin\provider {
42    /** @var array Columns that point at a user, per table. */
43    private const USERCOLUMNS = [
44        'local_doublemark_config' => ['usermodified'],
45        'local_doublemark_alloc' => ['userid', 'marker1', 'marker2'],
46        'local_doublemark_mark' => ['userid', 'markerid'],
47        'local_doublemark_agreed' => ['userid', 'agreedby', 'releasedby'],
48    ];
49
50    /**
51     * Describe the stored data.
52     *
53     * @param collection $collection
54     * @return collection
55     */
56    public static function get_metadata(collection $collection): collection {
57        $collection->add_database_table('local_doublemark_alloc', [
58            'userid' => 'privacy:metadata:alloc:userid',
59            'marker1' => 'privacy:metadata:alloc:marker1',
60            'marker2' => 'privacy:metadata:alloc:marker2',
61        ], 'privacy:metadata:alloc');
62        $collection->add_database_table('local_doublemark_mark', [
63            'userid' => 'privacy:metadata:mark:userid',
64            'markerid' => 'privacy:metadata:mark:markerid',
65            'grade' => 'privacy:metadata:mark:grade',
66            'comment' => 'privacy:metadata:mark:comment',
67            'timemodified' => 'privacy:metadata:mark:timemodified',
68        ], 'privacy:metadata:mark');
69        $collection->add_database_table('local_doublemark_agreed', [
70            'userid' => 'privacy:metadata:agreed:userid',
71            'grade' => 'privacy:metadata:agreed:grade',
72            'feedback' => 'privacy:metadata:agreed:feedback',
73            'note' => 'privacy:metadata:agreed:note',
74            'agreedby' => 'privacy:metadata:agreed:agreedby',
75            'releasedby' => 'privacy:metadata:agreed:releasedby',
76        ], 'privacy:metadata:agreed');
77        $collection->add_database_table('local_doublemark_config', [
78            'usermodified' => 'privacy:metadata:config:usermodified',
79        ], 'privacy:metadata:config');
80        return $collection;
81    }
82
83    /**
84     * Module contexts where the user appears in any role.
85     *
86     * @param int $userid
87     * @return contextlist
88     */
89    public static function get_contexts_for_userid(int $userid): contextlist {
90        $list = new contextlist();
91        foreach (self::USERCOLUMNS as $table => $columns) {
92            $where = implode(' OR ', array_map(fn($c) => "t.$c = :$c", $columns));
93            $params = ['level' => CONTEXT_MODULE] + array_fill_keys($columns, $userid);
94            $list->add_from_sql("SELECT ctx.id FROM {{$table}} t
95                JOIN {context} ctx ON ctx.instanceid = t.cmid AND ctx.contextlevel = :level
96                WHERE $where", $params);
97        }
98        return $list;
99    }
100
101    /**
102     * Users with data in a module context.
103     *
104     * @param userlist $userlist
105     */
106    public static function get_users_in_context(userlist $userlist) {
107        $context = $userlist->get_context();
108        if (!$context instanceof \context_module) {
109            return;
110        }
111        foreach (self::USERCOLUMNS as $table => $columns) {
112            foreach ($columns as $column) {
113                $userlist->add_from_sql(
114                    $column,
115                    "SELECT $column FROM {{$table}} WHERE cmid = :cmid AND $column > 0",
116                    ['cmid' => $context->instanceid]
117                );
118            }
119        }
120    }
121
122    /**
123     * Export marks about the user as a student, and marks they gave as a marker.
124     *
125     * @param approved_contextlist $contextlist
126     */
127    public static function export_user_data(approved_contextlist $contextlist) {
128        global $DB;
129        $userid = (int) $contextlist->get_user()->id;
130        foreach ($contextlist->get_contexts() as $context) {
131            if (!$context instanceof \context_module) {
132                continue;
133            }
134            $cmid = (int) $context->instanceid;
135            $marks = array_values(array_map(fn($m) => [
136                'position' => (int) $m->position,
137                'grade' => (float) $m->grade,
138                'comment' => (string) $m->comment,
139                'timemodified' => transform::datetime($m->timemodified),
140            ], $DB->get_records('local_doublemark_mark', ['cmid' => $cmid, 'userid' => $userid], 'position')));
141            $given = array_values(array_map(fn($m) => [
142                'grade' => (float) $m->grade,
143                'comment' => (string) $m->comment,
144                'timemodified' => transform::datetime($m->timemodified),
145            ], $DB->get_records('local_doublemark_mark', ['cmid' => $cmid, 'markerid' => $userid], 'id')));
146            $agreed = $DB->get_record('local_doublemark_agreed', ['cmid' => $cmid, 'userid' => $userid]);
147            $data = (object) [
148                'marks' => $marks,
149                'marksgiven' => $given,
150                'agreed' => $agreed ? [
151                    'grade' => (float) $agreed->grade,
152                    'feedback' => (string) $agreed->feedback,
153                    'note' => (string) $agreed->note,
154                    'timeagreed' => transform::datetime($agreed->timeagreed),
155                    'timereleased' => $agreed->timereleased ? transform::datetime($agreed->timereleased) : '',
156                ] : null,
157            ];
158            if ($marks || $given || $agreed) {
159                writer::with_context($context)->export_data([get_string('pluginname', 'local_doublemark')], $data);
160            }
161        }
162    }
163
164    /**
165     * Delete everything stored for a module context.
166     *
167     * @param \context $context
168     */
169    public static function delete_data_for_all_users_in_context(\context $context) {
170        if ($context instanceof \context_module) {
171            double_marking::delete_for_cmid((int) $context->instanceid);
172        }
173    }
174
175    /**
176     * Delete one user's data in the approved contexts.
177     *
178     * @param approved_contextlist $contextlist
179     */
180    public static function delete_data_for_user(approved_contextlist $contextlist) {
181        $userid = (int) $contextlist->get_user()->id;
182        foreach ($contextlist->get_contexts() as $context) {
183            if ($context instanceof \context_module) {
184                self::delete_user((int) $context->instanceid, $userid);
185            }
186        }
187    }
188
189    /**
190     * Delete several users' data in one context.
191     *
192     * @param approved_userlist $userlist
193     */
194    public static function delete_data_for_users(approved_userlist $userlist) {
195        $context = $userlist->get_context();
196        if (!$context instanceof \context_module) {
197            return;
198        }
199        foreach ($userlist->get_userids() as $userid) {
200            self::delete_user((int) $context->instanceid, (int) $userid);
201        }
202    }
203
204    /**
205     * Remove a student's rows, and the user's id from rows where they were marker or lead.
206     *
207     * @param int $cmid
208     * @param int $userid
209     */
210    private static function delete_user(int $cmid, int $userid): void {
211        global $DB;
212        foreach (['alloc', 'mark', 'agreed'] as $table) {
213            $DB->delete_records("local_doublemark_$table", ['cmid' => $cmid, 'userid' => $userid]);
214        }
215        foreach (self::USERCOLUMNS as $table => $columns) {
216            foreach ($columns as $column) {
217                if ($column !== 'userid') {
218                    $DB->set_field($table, $column, 0, ['cmid' => $cmid, $column => $userid]);
219                }
220            }
221        }
222    }
223}