You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

581 lines
22 KiB

<?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
]
);
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!is_a($context, \context_module::class)) {
return;
}
$sql = "SELECT al.studentid
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = 'attendance'
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
WHERE ctx.id = :contextid";
$params = [
'contextlevel' => CONTEXT_MODULE,
'contextid' => $context->id,
];
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT al.takenby
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = 'attendance'
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
WHERE ctx.id = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* 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);
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if (!is_a($context, \context_module::class)) {
return;
}
// Prepare SQL to gather all completed IDs.
$userids = $userlist->get_userids();
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
// Delete records where user was marked as attending.
$DB->delete_records_select(
'attendance_log',
"studentid $insql",
$inparams
);
// Delete all warnings.
$DB->delete_records_select(
'attendance_warning_done',
"notifyid $insql",
$inparams
);
$DB->delete_records_select(
'attendance_warning_done',
"userid $insql",
$inparams
);
// Now for teachers remove relation for marking.
$DB->set_field_select(
'attendance_log',
'takenby',
2,
"takenby $insql",
$inparams);
}
/**
* 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]
)
);
}
}
}