Cameron Ball
7 years ago
committed by
Dan Marsden
2 changed files with 518 additions and 1 deletions
@ -0,0 +1,495 @@ |
|||||
|
<?php |
||||
|
// This file is part of Moodle - http://moodle.org/ |
||||
|
// |
||||
|
// Moodle is free software: you can redistribute it and/or modify |
||||
|
// it under the terms of the GNU General Public License as published by |
||||
|
// the Free Software Foundation, either version 3 of the License, or |
||||
|
// (at your option) any later version. |
||||
|
// |
||||
|
// Moodle is distributed in the hope that it will be useful, |
||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
// GNU General Public License for more details. |
||||
|
// |
||||
|
// You should have received a copy of the GNU General Public License |
||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>. |
||||
|
|
||||
|
/** |
||||
|
* mod_attendance Data provider. |
||||
|
* |
||||
|
* @package mod_attendance |
||||
|
* @copyright 2018 Cameron Ball <cameron@cameron1729.xyz> |
||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
||||
|
*/ |
||||
|
|
||||
|
namespace mod_attendance\privacy; |
||||
|
defined('MOODLE_INTERNAL') || die(); |
||||
|
|
||||
|
use context; |
||||
|
use context_module; |
||||
|
use core_privacy\local\metadata\collection; |
||||
|
use core_privacy\local\request\{writer, transform, helper, contextlist, approved_contextlist}; |
||||
|
use stdClass; |
||||
|
|
||||
|
/** |
||||
|
* Data provider for mod_attendance. |
||||
|
* |
||||
|
* @copyright 2018 Cameron Ball <cameron@cameron1729.xyz> |
||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
||||
|
*/ |
||||
|
final class provider implements |
||||
|
\core_privacy\local\request\plugin\provider, |
||||
|
\core_privacy\local\metadata\provider |
||||
|
{ |
||||
|
|
||||
|
/** |
||||
|
* Returns meta data about this system. |
||||
|
* |
||||
|
* @param collection $collection The initialised collection to add items to. |
||||
|
* @return collection A listing of user data stored through this system. |
||||
|
*/ |
||||
|
public static function get_metadata(collection $collection) : collection { |
||||
|
$collection->add_database_table( |
||||
|
'attendance_log', |
||||
|
[ |
||||
|
'sessionid' => 'privacy:metadata:sessionid', |
||||
|
'studentid' => 'privacy:metadata:studentid', |
||||
|
'statusid' => 'privacy:metadata:statusid', |
||||
|
'statusset' => 'privacy:metadata:statusset', |
||||
|
'timetaken' => 'privacy:metadata:timetaken', |
||||
|
'takenby' => 'privacy:metadata:takenby', |
||||
|
'remarks' => 'privacy:metadata:remarks', |
||||
|
'ipaddress' => 'privacy:metadata:ipaddress' |
||||
|
], |
||||
|
'privacy:metadata:attendancelog' |
||||
|
); |
||||
|
|
||||
|
$collection->add_database_table( |
||||
|
'attendance_sessions', |
||||
|
[ |
||||
|
'groupid' => 'privacy:metadata:groupid', |
||||
|
'sessdate' => 'privacy:metadata:sessdate', |
||||
|
'duration' => 'privacy:metadata:duration', |
||||
|
'lasttaken' => 'privacy:metadata:lasttaken', |
||||
|
'lasttakenby' => 'privacy:metadata:lasttakenby', |
||||
|
'timemodified' => 'privacy:metadata:timemodified' |
||||
|
], |
||||
|
'privacy:metadata:attendancesessions' |
||||
|
); |
||||
|
|
||||
|
$collection->add_database_table( |
||||
|
'attendance_warning_done', |
||||
|
[ |
||||
|
'notifyid' => 'privacy:metadata:notifyid', |
||||
|
'userid' => 'privacy:metadata:userid', |
||||
|
'timesent' => 'privacy:metadata:timesent' |
||||
|
], |
||||
|
'privacy:metadata:attendancewarningdone' |
||||
|
); |
||||
|
|
||||
|
return $collection; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the list of contexts that contain user information for the specified user. |
||||
|
* |
||||
|
* In the case of attendance, that is any attendance where a student has had their |
||||
|
* attendance taken or has taken attendance for someone else. |
||||
|
* |
||||
|
* @param int $userid The user to search. |
||||
|
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. |
||||
|
*/ |
||||
|
public static function get_contexts_for_userid(int $userid) : contextlist { |
||||
|
return (new contextlist)->add_from_sql( |
||||
|
"SELECT ctx.id |
||||
|
FROM {course_modules} cm |
||||
|
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename |
||||
|
JOIN {attendance} a ON cm.instance = a.id |
||||
|
JOIN {attendance_sessions} asess ON asess.attendanceid = a.id |
||||
|
JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel |
||||
|
JOIN {attendance_log} al ON asess.id = al.sessionid AND (al.studentid = :userid OR al.takenby = :takenbyid)", |
||||
|
[ |
||||
|
'modulename' => 'attendance', |
||||
|
'contextlevel' => CONTEXT_MODULE, |
||||
|
'userid' => $userid, |
||||
|
'takenbyid' => $userid |
||||
|
] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Delete all data for all users in the specified context. |
||||
|
* |
||||
|
* @param context $context The specific context to delete data for. |
||||
|
*/ |
||||
|
public static function delete_data_for_all_users_in_context(context $context) { |
||||
|
global $DB; |
||||
|
|
||||
|
if (!$context instanceof context_module) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Delete all information recorded against sessions associated with this module. |
||||
|
$DB->delete_records_select( |
||||
|
'attendance_log', |
||||
|
"sessionid IN (SELECT id FROM {attendance_sessions} WHERE attendanceid = :attendanceid", |
||||
|
[ |
||||
|
'attendanceid' => $cm->instance |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
// Delete all completed warnings associated with a warning associated with this module. |
||||
|
$DB->delete_records_select( |
||||
|
'attendance_warning_done', |
||||
|
"notifyid IN (SELECT id from {attendance_warning} WHERE idnumber = :attendanceid)", |
||||
|
['attendanceid' => $cm->instance] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Delete all user data for the specified user, in the specified contexts. |
||||
|
* |
||||
|
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for. |
||||
|
*/ |
||||
|
public static function delete_data_for_user(approved_contextlist $contextlist) { |
||||
|
global $DB; |
||||
|
$userid = (int)$contextlist->get_user()->id; |
||||
|
|
||||
|
foreach ($contextlist as $context) { |
||||
|
if (!$context instanceof context_module) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
$attendanceid = (int)$DB->get_record('attendance', ['id' => $cm->instance])->id; |
||||
|
$sessionids = array_keys( |
||||
|
$DB->get_records('attendance_sessions', ['attendanceid' => $attendanceid]) |
||||
|
); |
||||
|
|
||||
|
self::delete_user_from_session_attendance_log($userid, $sessionids); |
||||
|
self::delete_user_from_sessions($userid, $sessionids); |
||||
|
self::delete_user_from_attendance_warnings_log($userid, $attendanceid); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Export all user data for the specified user, in the specified contexts. |
||||
|
* |
||||
|
* @param approved_contextlist $contextlist The approved contexts to export information for. |
||||
|
*/ |
||||
|
public static function export_user_data(approved_contextlist $contextlist) { |
||||
|
global $DB; |
||||
|
|
||||
|
$params = [ |
||||
|
'modulename' => 'attendance', |
||||
|
'contextlevel' => CONTEXT_MODULE, |
||||
|
'studentid' => $contextlist->get_user()->id, |
||||
|
'takenby' => $contextlist->get_user()->id |
||||
|
]; |
||||
|
|
||||
|
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); |
||||
|
|
||||
|
$sql = "SELECT |
||||
|
al.*, |
||||
|
asess.id as session, |
||||
|
asess.description, |
||||
|
ctx.id as contextid, |
||||
|
a.name as attendancename, |
||||
|
a.id as attendanceid, |
||||
|
statuses.description as statusdesc, statuses.grade as statusgrade |
||||
|
FROM {course_modules} cm |
||||
|
JOIN {attendance} a ON cm.instance = a.id |
||||
|
JOIN {attendance_sessions} asess ON asess.attendanceid = a.id |
||||
|
JOIN {attendance_log} al on (al.sessionid = asess.id AND (studentid = :studentid OR al.takenby = :takenby)) |
||||
|
JOIN {context} ctx ON cm.id = ctx.instanceid |
||||
|
JOIN {attendance_statuses} statuses ON statuses.id = al.statusid |
||||
|
WHERE (ctx.id {$contextsql})"; |
||||
|
|
||||
|
$attendances = $DB->get_records_sql($sql, $params + $contextparams); |
||||
|
|
||||
|
self::export_attendance_logs( |
||||
|
get_string('attendancestaken', 'mod_attendance'), |
||||
|
array_filter( |
||||
|
$attendances, |
||||
|
function(stdClass $attendance) use ($contextlist) : bool { |
||||
|
return $attendance->takenby == $contextlist->get_user()->id; |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
self::export_attendance_logs( |
||||
|
get_string('attendanceslogged', 'mod_attendance'), |
||||
|
array_filter( |
||||
|
$attendances, |
||||
|
function(stdClass $attendance) use ($contextlist) : bool { |
||||
|
return $attendance->studentid == $contextlist->get_user()->id; |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
self::export_attendances( |
||||
|
$contextlist->get_user(), |
||||
|
$attendances, |
||||
|
self::group_by_property( |
||||
|
$DB->get_records_sql( |
||||
|
"SELECT |
||||
|
*, |
||||
|
a.id as attendanceid |
||||
|
FROM {attendance_warning_done} awd |
||||
|
JOIN {attendance_warning} aw ON awd.notifyid = aw.id |
||||
|
JOIN {attendance} a on aw.idnumber = a.id |
||||
|
WHERE userid = :userid", |
||||
|
['userid' => $contextlist->get_user()->id] |
||||
|
), |
||||
|
'notifyid' |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Delete a user from session logs. |
||||
|
* |
||||
|
* @param int $userid The id of the user to remove. |
||||
|
* @param array $sessionids Array of session ids from which to remove the student from the relevant logs. |
||||
|
*/ |
||||
|
private static function delete_user_from_session_attendance_log(int $userid, array $sessionids) { |
||||
|
global $DB; |
||||
|
|
||||
|
// Delete records where user was marked as attending. |
||||
|
list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED); |
||||
|
$DB->delete_records_select( |
||||
|
'attendance_log', |
||||
|
"(studentid = :studentid) AND sessionid $sessionsql", |
||||
|
['studentid' => $userid] + $sessionparams |
||||
|
); |
||||
|
|
||||
|
// Get every log record where user took the attendance. |
||||
|
$attendancetakenids = array_keys( |
||||
|
$DB->get_records_sql( |
||||
|
"SELECT * from {attendance_log} |
||||
|
WHERE takenby = :takenbyid AND sessionid $sessionsql", |
||||
|
['takenbyid' => $userid] + $sessionparams |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
if (!$attendancetakenids) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Don't delete the record from the log, but update to site admin taking attendance. |
||||
|
list($attendancetakensql, $attendancetakenparams) = $DB->get_in_or_equal($attendancetakenids, SQL_PARAMS_NAMED); |
||||
|
$DB->set_field_select( |
||||
|
'attendance_log', |
||||
|
'takenby', |
||||
|
2, |
||||
|
"id $attendancetakensql", |
||||
|
$attendancetakenparams |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Delete a user from sessions. |
||||
|
* |
||||
|
* Not much user data is stored in a session, but it's possible that a user id is saved |
||||
|
* in the "lasttakenby" field. |
||||
|
* |
||||
|
* @param int $userid The id of the user to remove. |
||||
|
* @param array $sessionids Array of session ids from which to remove the student. |
||||
|
*/ |
||||
|
private static function delete_user_from_sessions(int $userid, array $sessionids) { |
||||
|
global $DB; |
||||
|
|
||||
|
// Get all sessions where user was last to mark attendance. |
||||
|
list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED); |
||||
|
$sessionstaken = $DB->get_records_sql( |
||||
|
"SELECT * from {attendance_sessions} |
||||
|
WHERE lasttakenby = :lasttakenbyid AND id $sessionsql", |
||||
|
['lasttakenbyid' => $userid] + $sessionparams |
||||
|
); |
||||
|
|
||||
|
if (!$sessionstaken) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Don't delete the session, but update last taken by to the site admin. |
||||
|
list($sessionstakensql, $sessionstakenparams) = $DB->get_in_or_equal(array_keys($sessionstaken), SQL_PARAMS_NAMED); |
||||
|
$DB->set_field_select( |
||||
|
'attendance_sessions', |
||||
|
'lasttakenby', |
||||
|
2, |
||||
|
"id $sessionstakensql", |
||||
|
$sessionstakenparams |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Delete a user from the attendance waring log. |
||||
|
* |
||||
|
* @param int $userid The id of the user to remove. |
||||
|
* @param int $attendanceid The id of the attendance instance to remove the relevant warnings from. |
||||
|
*/ |
||||
|
private static function delete_user_from_attendance_warnings_log(int $userid, int $attendanceid) { |
||||
|
global $DB; |
||||
|
|
||||
|
// Get all warnings because the user could have their ID listed in the thirdpartyemails column as a comma delimited string. |
||||
|
$warnings = $DB->get_records( |
||||
|
'attendance_warning', |
||||
|
['idnumber' => $attendanceid] |
||||
|
); |
||||
|
|
||||
|
if (!$warnings) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Update the third party emails list for all the relevant warnings. |
||||
|
$updatedwarnings = array_map( |
||||
|
function(stdClass $warning) use ($userid) : stdClass { |
||||
|
$warning->thirdpartyemails = implode(',', array_diff(explode(',', $warning->thirdpartyemails), [$userid])); |
||||
|
return $warning; |
||||
|
}, |
||||
|
array_filter( |
||||
|
$warnings, |
||||
|
function (stdClass $warning) use ($userid) : bool { |
||||
|
return in_array($userid, explode(',', $warning->thirdpartyemails)); |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
// Sadly need to update each individually, no way to bulk update as all the thirdpartyemails field can be different. |
||||
|
foreach ($updatedwarnings as $updatedwarning) { |
||||
|
$DB->update_record('attendance_warning', $updatedwarning); |
||||
|
} |
||||
|
|
||||
|
// Delete any record of the user being notified. |
||||
|
list($warningssql, $warningsparams) = $DB->get_in_or_equal(array_keys($warnings), SQL_PARAMS_NAMED); |
||||
|
$DB->delete_records_select( |
||||
|
'attendance_warning_done', |
||||
|
"userid = :userid AND notifyid $warningssql", |
||||
|
['userid' => $userid] + $warningsparams |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function to group an array of stdClasses by a common property. |
||||
|
* |
||||
|
* @param array $classes An array of classes to group. |
||||
|
* @param string $property A common property to group the classes by. |
||||
|
*/ |
||||
|
private static function group_by_property(array $classes, string $property) : array { |
||||
|
return array_reduce( |
||||
|
$classes, |
||||
|
function (array $classes, stdClass $class) use ($property) : array { |
||||
|
$classes[$class->{$property}][] = $class; |
||||
|
return $classes; |
||||
|
}, |
||||
|
[] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function to transform a row from the database in to session data to export. |
||||
|
* |
||||
|
* The properties of the "dbrow" are very specific to the result of the SQL from |
||||
|
* the export_user_data function. |
||||
|
* |
||||
|
* @param stdClass $dbrow A row from the database containing session information. |
||||
|
* @return stdClass The transformed row. |
||||
|
*/ |
||||
|
private static function transform_db_row_to_session_data(stdClass $dbrow) : stdClass { |
||||
|
return (object) [ |
||||
|
'name' => $dbrow->attendancename, |
||||
|
'session' => $dbrow->session, |
||||
|
'takenbyid' => $dbrow->takenby, |
||||
|
'studentid' => $dbrow->studentid, |
||||
|
'status' => $dbrow->statusdesc, |
||||
|
'grade' => $dbrow->statusgrade, |
||||
|
'sessiondescription' => $dbrow->description, |
||||
|
'timetaken' => transform::datetime($dbrow->timetaken), |
||||
|
'remarks' => $dbrow->remarks, |
||||
|
'ipaddress' => $dbrow->ipaddress |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function to transform a row from the database in to warning data to export. |
||||
|
* |
||||
|
* The properties of the "dbrow" are very specific to the result of the SQL from |
||||
|
* the export_user_data function. |
||||
|
* |
||||
|
* @param stdClass $warning A row from the database containing warning information. |
||||
|
* @return stdClass The transformed row. |
||||
|
*/ |
||||
|
private static function transform_warning_data(stdClass $warning) : stdClass { |
||||
|
return (object) [ |
||||
|
'timesent' => transform::datetime($warning->timesent), |
||||
|
'thirdpartyemails' => $warning->thirdpartyemails, |
||||
|
'subject' => $warning->emailsubject, |
||||
|
'body' => $warning->emailcontent |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function to export attendance logs. |
||||
|
* |
||||
|
* The array of "attendances" is actually the result returned by the SQL in export_user_data. |
||||
|
* It is more of a list of sessions. Which is why it needs to be grouped by context id. |
||||
|
* |
||||
|
* @param string $path The path in the export (relative to the current context). |
||||
|
* @param array $attendances Array of attendances to export the logs for. |
||||
|
*/ |
||||
|
private static function export_attendance_logs(string $path, array $attendances) { |
||||
|
$attendancesbycontextid = self::group_by_property($attendances, 'contextid'); |
||||
|
|
||||
|
foreach ($attendancesbycontextid as $contextid => $sessions) { |
||||
|
$context = context::instance_by_id($contextid); |
||||
|
$sessionsbyid = self::group_by_property($sessions, 'sessionid'); |
||||
|
|
||||
|
foreach ($sessionsbyid as $sessionid => $sessions) { |
||||
|
writer::with_context($context)->export_data( |
||||
|
[get_string('session', 'attendance') . ' ' . $sessionid, $path], |
||||
|
(object)[array_map([self::class, 'transform_db_row_to_session_data'], $sessions)] |
||||
|
); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function to export attendances (and associated warnings for the user). |
||||
|
* |
||||
|
* The array of "attendances" is actually the result returned by the SQL in export_user_data. |
||||
|
* It is more of a list of sessions. Which is why it needs to be grouped by context id. |
||||
|
* |
||||
|
* @param stdClass $user The user to export attendances for. This is needed to retrieve context data. |
||||
|
* @param array $attendances Array of attendances to export. |
||||
|
* @param array $warningsmap Mapping between an attendance id and warnings. |
||||
|
*/ |
||||
|
private static function export_attendances(stdClass $user, array $attendances, array $warningsmap) { |
||||
|
$attendancesbycontextid = self::group_by_property($attendances, 'contextid'); |
||||
|
|
||||
|
foreach ($attendancesbycontextid as $contextid => $attendance) { |
||||
|
$context = context::instance_by_id($contextid); |
||||
|
|
||||
|
// It's "safe" to get the attendanceid from the first element in the array - since they're grouped by context. |
||||
|
// i.e., module context. |
||||
|
// The reason there can be more than one "attendance" is that the attendances array will contain multiple records |
||||
|
// for the same attendance instance if there are multiple sessions. It is not the same as a raw record from the |
||||
|
// attendances table. See the SQL in export_user_data. |
||||
|
$warnings = array_map([self::class, 'transform_warning_data'], $warningsmap[$attendance[0]->attendanceid] ?? []); |
||||
|
|
||||
|
writer::with_context($context)->export_data( |
||||
|
[], |
||||
|
(object)array_merge( |
||||
|
(array) helper::get_context_data($context, $user), |
||||
|
['warnings' => $warnings] |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue