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.
1281 lines
54 KiB
1281 lines
54 KiB
2 years ago
|
<?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/>.
|
||
|
|
||
|
/**
|
||
|
* Data provider.
|
||
|
*
|
||
|
* @package core_grades
|
||
|
* @copyright 2018 Frédéric Massart
|
||
|
* @author Frédéric Massart <fred@branchup.tech>
|
||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||
|
*/
|
||
|
|
||
|
namespace core_grades\privacy;
|
||
|
defined('MOODLE_INTERNAL') || die();
|
||
|
|
||
|
use context;
|
||
|
use context_course;
|
||
|
use context_system;
|
||
|
use grade_item;
|
||
|
use grade_grade;
|
||
|
use grade_scale;
|
||
|
use stdClass;
|
||
|
use core_privacy\local\metadata\collection;
|
||
|
use core_privacy\local\request\approved_contextlist;
|
||
|
use core_privacy\local\request\transform;
|
||
|
use core_privacy\local\request\writer;
|
||
|
|
||
|
require_once($CFG->libdir . '/gradelib.php');
|
||
|
|
||
|
/**
|
||
|
* Data provider class.
|
||
|
*
|
||
|
* @package core_grades
|
||
|
* @copyright 2018 Frédéric Massart
|
||
|
* @author Frédéric Massart <fred@branchup.tech>
|
||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||
|
*/
|
||
|
class provider implements
|
||
|
\core_privacy\local\metadata\provider,
|
||
|
\core_privacy\local\request\subsystem\provider,
|
||
|
\core_privacy\local\request\core_userlist_provider {
|
||
|
|
||
|
/**
|
||
|
* Returns metadata.
|
||
|
*
|
||
|
* @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 {
|
||
|
|
||
|
// Tables without 'real' user information.
|
||
|
$collection->add_database_table('grade_outcomes', [
|
||
|
'timemodified' => 'privacy:metadata:outcomes:timemodified',
|
||
|
'usermodified' => 'privacy:metadata:outcomes:usermodified',
|
||
|
], 'privacy:metadata:outcomes');
|
||
|
|
||
|
$collection->add_database_table('grade_outcomes_history', [
|
||
|
'timemodified' => 'privacy:metadata:history:timemodified',
|
||
|
'loggeduser' => 'privacy:metadata:history:loggeduser',
|
||
|
], 'privacy:metadata:outcomeshistory');
|
||
|
|
||
|
$collection->add_database_table('grade_categories_history', [
|
||
|
'timemodified' => 'privacy:metadata:history:timemodified',
|
||
|
'loggeduser' => 'privacy:metadata:history:loggeduser',
|
||
|
], 'privacy:metadata:categorieshistory');
|
||
|
|
||
|
$collection->add_database_table('grade_items_history', [
|
||
|
'timemodified' => 'privacy:metadata:history:timemodified',
|
||
|
'loggeduser' => 'privacy:metadata:history:loggeduser',
|
||
|
], 'privacy:metadata:itemshistory');
|
||
|
|
||
|
$collection->add_database_table('scale', [
|
||
|
'userid' => 'privacy:metadata:scale:userid',
|
||
|
'timemodified' => 'privacy:metadata:scale:timemodified',
|
||
|
], 'privacy:metadata:scale');
|
||
|
|
||
|
$collection->add_database_table('scale_history', [
|
||
|
'userid' => 'privacy:metadata:scale:userid',
|
||
|
'timemodified' => 'privacy:metadata:history:timemodified',
|
||
|
'loggeduser' => 'privacy:metadata:history:loggeduser',
|
||
|
], 'privacy:metadata:scalehistory');
|
||
|
|
||
|
// Table with user information.
|
||
|
$gradescommonfields = [
|
||
|
'userid' => 'privacy:metadata:grades:userid',
|
||
|
'usermodified' => 'privacy:metadata:grades:usermodified',
|
||
|
'finalgrade' => 'privacy:metadata:grades:finalgrade',
|
||
|
'feedback' => 'privacy:metadata:grades:feedback',
|
||
|
'information' => 'privacy:metadata:grades:information',
|
||
|
];
|
||
|
|
||
|
$collection->add_database_table('grade_grades', array_merge($gradescommonfields, [
|
||
|
'timemodified' => 'privacy:metadata:grades:timemodified',
|
||
|
]), 'privacy:metadata:grades');
|
||
|
|
||
|
$collection->add_database_table('grade_grades_history', array_merge($gradescommonfields, [
|
||
|
'timemodified' => 'privacy:metadata:history:timemodified',
|
||
|
'loggeduser' => 'privacy:metadata:history:loggeduser',
|
||
|
]), 'privacy:metadata:gradeshistory');
|
||
|
|
||
|
// The following tables are reported but not exported/deleted because their data is temporary and only
|
||
|
// used during an import. It's content is deleted after a successful, or failed, import.
|
||
|
|
||
|
$collection->add_database_table('grade_import_newitem', [
|
||
|
'itemname' => 'privacy:metadata:grade_import_newitem:itemname',
|
||
|
'importcode' => 'privacy:metadata:grade_import_newitem:importcode',
|
||
|
'importer' => 'privacy:metadata:grade_import_newitem:importer'
|
||
|
], 'privacy:metadata:grade_import_newitem');
|
||
|
|
||
|
$collection->add_database_table('grade_import_values', [
|
||
|
'userid' => 'privacy:metadata:grade_import_values:userid',
|
||
|
'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade',
|
||
|
'feedback' => 'privacy:metadata:grade_import_values:feedback',
|
||
|
'importcode' => 'privacy:metadata:grade_import_values:importcode',
|
||
|
'importer' => 'privacy:metadata:grade_import_values:importer',
|
||
|
'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback'
|
||
|
], 'privacy:metadata:grade_import_values');
|
||
|
|
||
|
$collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
|
||
|
|
||
|
return $collection;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the list of contexts that contain user information for the specified user.
|
||
|
*
|
||
|
* @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) : \core_privacy\local\request\contextlist {
|
||
|
$contextlist = new \core_privacy\local\request\contextlist();
|
||
|
|
||
|
// Add where we modified outcomes.
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {grade_outcomes} go
|
||
|
JOIN {context} ctx
|
||
|
ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
|
||
|
OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid)
|
||
|
WHERE go.usermodified = :userid";
|
||
|
$params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
// Add where we modified scales.
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {scale} s
|
||
|
JOIN {context} ctx
|
||
|
ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel)
|
||
|
OR (s.courseid = 0 AND ctx.id = :syscontextid)
|
||
|
WHERE s.userid = :userid";
|
||
|
$params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
// Add where appear in the history of outcomes, categories, scales or items.
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {context} ctx
|
||
|
LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND (
|
||
|
(goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1)
|
||
|
OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1)
|
||
|
)
|
||
|
LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND (
|
||
|
gch.courseid = ctx.instanceid
|
||
|
AND ctx.contextlevel = :courselevel2
|
||
|
)
|
||
|
LEFT JOIN {grade_items_history} gih ON gih.loggeduser = :userid3 AND (
|
||
|
gih.courseid = ctx.instanceid
|
||
|
AND ctx.contextlevel = :courselevel3
|
||
|
)
|
||
|
LEFT JOIN {scale_history} sh
|
||
|
ON (sh.userid = :userid4 OR sh.loggeduser = :userid5)
|
||
|
AND (
|
||
|
(sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4)
|
||
|
OR (sh.courseid = 0 AND ctx.id = :syscontextid2)
|
||
|
)
|
||
|
WHERE goh.id IS NOT NULL
|
||
|
OR gch.id IS NOT NULL
|
||
|
OR gih.id IS NOT NULL
|
||
|
OR sh.id IS NOT NULL";
|
||
|
$params = [
|
||
|
'syscontextid1' => SYSCONTEXTID,
|
||
|
'syscontextid2' => SYSCONTEXTID,
|
||
|
'courselevel1' => CONTEXT_COURSE,
|
||
|
'courselevel2' => CONTEXT_COURSE,
|
||
|
'courselevel3' => CONTEXT_COURSE,
|
||
|
'courselevel4' => CONTEXT_COURSE,
|
||
|
'userid1' => $userid,
|
||
|
'userid2' => $userid,
|
||
|
'userid3' => $userid,
|
||
|
'userid4' => $userid,
|
||
|
'userid5' => $userid,
|
||
|
];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
// Add where we were graded or modified grades, including in the history table.
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {grade_items} gi
|
||
|
JOIN {context} ctx
|
||
|
ON ctx.instanceid = gi.courseid
|
||
|
AND ctx.contextlevel = :courselevel
|
||
|
JOIN {grade_grades} gg
|
||
|
ON gg.itemid = gi.id
|
||
|
WHERE gg.userid = :userid1 OR gg.usermodified = :userid2";
|
||
|
$params = [
|
||
|
'courselevel' => CONTEXT_COURSE,
|
||
|
'userid1' => $userid,
|
||
|
'userid2' => $userid
|
||
|
];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {grade_items} gi
|
||
|
JOIN {context} ctx
|
||
|
ON ctx.instanceid = gi.courseid
|
||
|
AND ctx.contextlevel = :courselevel
|
||
|
JOIN {grade_grades_history} ggh
|
||
|
ON ggh.itemid = gi.id
|
||
|
WHERE ggh.userid = :userid1
|
||
|
OR ggh.loggeduser = :userid2
|
||
|
OR ggh.usermodified = :userid3";
|
||
|
$params = [
|
||
|
'courselevel' => CONTEXT_COURSE,
|
||
|
'userid1' => $userid,
|
||
|
'userid2' => $userid,
|
||
|
'userid3' => $userid
|
||
|
];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
// Historical grades can be made orphans when the corresponding itemid is deleted. When that happens
|
||
|
// we cannot tie the historical grade to a course context, so we report the user context as a last resort.
|
||
|
$sql = "
|
||
|
SELECT DISTINCT ctx.id
|
||
|
FROM {context} ctx
|
||
|
JOIN {grade_grades_history} ggh
|
||
|
ON ctx.contextlevel = :userlevel
|
||
|
AND ggh.userid = ctx.instanceid
|
||
|
AND (
|
||
|
ggh.userid = :userid1
|
||
|
OR ggh.usermodified = :userid2
|
||
|
OR ggh.loggeduser = :userid3
|
||
|
)
|
||
|
LEFT JOIN {grade_items} gi
|
||
|
ON ggh.itemid = gi.id
|
||
|
WHERE gi.id IS NULL";
|
||
|
$params = [
|
||
|
'userlevel' => CONTEXT_USER,
|
||
|
'userid1' => $userid,
|
||
|
'userid2' => $userid,
|
||
|
'userid3' => $userid
|
||
|
];
|
||
|
$contextlist->add_from_sql($sql, $params);
|
||
|
|
||
|
return $contextlist;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the list of contexts that contain user information for the specified user.
|
||
|
*
|
||
|
* @param \core_privacy\local\request\userlist $userlist The userlist containing the list of users who have data
|
||
|
* in this context/plugin combination.
|
||
|
*/
|
||
|
public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
|
||
|
$context = $userlist->get_context();
|
||
|
|
||
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
||
|
$params = ['contextinstanceid' => $context->instanceid];
|
||
|
|
||
|
$sql = "SELECT usermodified
|
||
|
FROM {grade_outcomes}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('usermodified', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT loggeduser
|
||
|
FROM {grade_outcomes_history}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT userid
|
||
|
FROM {scale}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT loggeduser, userid
|
||
|
FROM {scale_history}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, $params);
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT loggeduser
|
||
|
FROM {grade_items_history}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT ggh.userid
|
||
|
FROM {grade_grades_history} ggh
|
||
|
JOIN {grade_items} gi ON ggh.itemid = gi.id
|
||
|
WHERE gi.courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT gg.userid, gg.usermodified
|
||
|
FROM {grade_grades} gg
|
||
|
JOIN {grade_items} gi ON gg.itemid = gi.id
|
||
|
WHERE gi.courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
$userlist->add_from_sql('usermodified', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT loggeduser
|
||
|
FROM {grade_categories_history}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, $params);
|
||
|
}
|
||
|
|
||
|
// None of these are currently used (user deletion).
|
||
|
if ($context->contextlevel == CONTEXT_SYSTEM) {
|
||
|
$params = ['contextinstanceid' => 0];
|
||
|
|
||
|
$sql = "SELECT usermodified
|
||
|
FROM {grade_outcomes}
|
||
|
WHERE (courseid IS NULL OR courseid < 1)";
|
||
|
$userlist->add_from_sql('usermodified', $sql, []);
|
||
|
|
||
|
$sql = "SELECT loggeduser
|
||
|
FROM {grade_outcomes_history}
|
||
|
WHERE (courseid IS NULL OR courseid < 1)";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, []);
|
||
|
|
||
|
$sql = "SELECT userid
|
||
|
FROM {scale}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
|
||
|
$sql = "SELECT loggeduser, userid
|
||
|
FROM {scale_history}
|
||
|
WHERE courseid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('loggeduser', $sql, $params);
|
||
|
$userlist->add_from_sql('userid', $sql, $params);
|
||
|
}
|
||
|
|
||
|
if ($context->contextlevel == CONTEXT_USER) {
|
||
|
// If the grade item has been removed and we have an orphan entry then we link to the
|
||
|
// user context.
|
||
|
$sql = "SELECT ggh.userid
|
||
|
FROM {grade_grades_history} ggh
|
||
|
LEFT JOIN {grade_items} gi ON ggh.itemid = gi.id
|
||
|
WHERE gi.id IS NULL
|
||
|
AND ggh.userid = :contextinstanceid";
|
||
|
$userlist->add_from_sql('userid', $sql, ['contextinstanceid' => $context->instanceid]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
|
||
|
$user = $contextlist->get_user();
|
||
|
$userid = $user->id;
|
||
|
$contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
|
||
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
||
|
$carry[$context->contextlevel][] = $context;
|
||
|
|
||
|
} else if ($context->contextlevel == CONTEXT_USER) {
|
||
|
$carry[$context->contextlevel][] = $context;
|
||
|
|
||
|
}
|
||
|
|
||
|
return $carry;
|
||
|
}, [
|
||
|
CONTEXT_USER => [],
|
||
|
CONTEXT_COURSE => []
|
||
|
]);
|
||
|
|
||
|
$rootpath = [get_string('grades', 'core_grades')];
|
||
|
$relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
|
||
|
|
||
|
// Export the outcomes.
|
||
|
static::export_user_data_outcomes_in_contexts($contextlist);
|
||
|
|
||
|
// Export the scales.
|
||
|
static::export_user_data_scales_in_contexts($contextlist);
|
||
|
|
||
|
// Export the historical grades which have become orphans (their grade items were deleted).
|
||
|
// We place those in ther user context of the graded user.
|
||
|
$userids = array_values(array_map(function($context) {
|
||
|
return $context->instanceid;
|
||
|
}, $contexts[CONTEXT_USER]));
|
||
|
if (!empty($userids)) {
|
||
|
|
||
|
// Export own historical grades and related ones.
|
||
|
list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||
|
list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||
|
list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||
|
$usercontext = $contexts[CONTEXT_USER];
|
||
|
$gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
|
||
|
$sql = "
|
||
|
SELECT $gghfields, ctx.id as ctxid
|
||
|
FROM {grade_grades_history} ggh
|
||
|
JOIN {context} ctx
|
||
|
ON ctx.instanceid = ggh.userid
|
||
|
AND ctx.contextlevel = :userlevel
|
||
|
LEFT JOIN {grade_items} gi
|
||
|
ON gi.id = ggh.itemid
|
||
|
WHERE gi.id IS NULL
|
||
|
AND (ggh.userid $inuseridsql
|
||
|
OR ggh.usermodified $inusermodifiedsql
|
||
|
OR ggh.loggeduser $inloggedusersql)
|
||
|
AND (ggh.userid = :userid1
|
||
|
OR ggh.usermodified = :userid2
|
||
|
OR ggh.loggeduser = :userid3)
|
||
|
ORDER BY ggh.userid, ggh.timemodified, ggh.id";
|
||
|
$params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams,
|
||
|
['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER]);
|
||
|
|
||
|
$deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades');
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) {
|
||
|
$context = context::instance_by_id($record->ctxid);
|
||
|
$gghrecord = static::extract_record($record, 'ggh_');
|
||
|
|
||
|
// Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades.
|
||
|
$carry[] = [
|
||
|
'name' => $deletedstr,
|
||
|
'graded_user_was_you' => transform::yesno($userid == $gghrecord->userid),
|
||
|
'grade' => $gghrecord->finalgrade,
|
||
|
'feedback' => format_text($gghrecord->feedback, $gghrecord->feedbackformat, ['context' => $context]),
|
||
|
'information' => format_text($gghrecord->information, $gghrecord->informationformat, ['context' => $context]),
|
||
|
'timemodified' => transform::datetime($gghrecord->timemodified),
|
||
|
'logged_in_user_was_you' => transform::yesno($userid == $gghrecord->loggeduser),
|
||
|
'author_of_change_was_you' => transform::yesno($userid == $gghrecord->usermodified),
|
||
|
'action' => static::transform_history_action($gghrecord->action)
|
||
|
];
|
||
|
|
||
|
return $carry;
|
||
|
|
||
|
}, function($ctxid, $data) use ($rootpath) {
|
||
|
$context = context::instance_by_id($ctxid);
|
||
|
writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Find out the course IDs.
|
||
|
$courseids = array_values(array_map(function($context) {
|
||
|
return $context->instanceid;
|
||
|
}, $contexts[CONTEXT_COURSE]));
|
||
|
if (empty($courseids)) {
|
||
|
return;
|
||
|
}
|
||
|
list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
|
||
|
|
||
|
// Ensure that the grades are final and do not need regrading.
|
||
|
array_walk($courseids, function($courseid) {
|
||
|
grade_regrade_final_grades($courseid);
|
||
|
});
|
||
|
|
||
|
// Export own grades.
|
||
|
$ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
|
||
|
$gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
|
||
|
$scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_');
|
||
|
$sql = "
|
||
|
SELECT $ggfields, $gifields, $scalefields
|
||
|
FROM {grade_grades} gg
|
||
|
JOIN {grade_items} gi
|
||
|
ON gi.id = gg.itemid
|
||
|
LEFT JOIN {scale} sc
|
||
|
ON sc.id = gi.scaleid
|
||
|
WHERE gi.courseid $incoursesql
|
||
|
AND gg.userid = :userid
|
||
|
ORDER BY gi.courseid, gi.id, gg.id";
|
||
|
$params = array_merge($incourseparams, ['userid' => $userid]);
|
||
|
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
|
||
|
$context = context_course::instance($record->gi_courseid);
|
||
|
$gg = static::extract_grade_grade_from_record($record);
|
||
|
$carry[] = static::transform_grade($gg, $context, false);
|
||
|
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($rootpath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
|
||
|
$pathtofiles = [
|
||
|
get_string('grades', 'core_grades'),
|
||
|
get_string('feedbackfiles', 'core_grades')
|
||
|
];
|
||
|
foreach ($data as $key => $grades) {
|
||
|
$gg = $grades['gradeobject'];
|
||
|
writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
|
||
|
GRADE_FEEDBACK_FILEAREA, $gg->id);
|
||
|
unset($data[$key]['gradeobject']); // Do not want to export this later.
|
||
|
}
|
||
|
|
||
|
writer::with_context($context)->export_data($rootpath, (object) ['grades' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export own historical grades in courses.
|
||
|
$gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
|
||
|
$sql = "
|
||
|
SELECT $gghfields, $gifields, $scalefields
|
||
|
FROM {grade_grades_history} ggh
|
||
|
JOIN {grade_items} gi
|
||
|
ON gi.id = ggh.itemid
|
||
|
LEFT JOIN {scale} sc
|
||
|
ON sc.id = gi.scaleid
|
||
|
WHERE gi.courseid $incoursesql
|
||
|
AND ggh.userid = :userid
|
||
|
ORDER BY gi.courseid, ggh.timemodified, ggh.id";
|
||
|
$params = array_merge($incourseparams, ['userid' => $userid]);
|
||
|
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
|
||
|
$context = context_course::instance($record->gi_courseid);
|
||
|
$gg = static::extract_grade_grade_from_record($record, true);
|
||
|
$carry[] = array_merge(static::transform_grade($gg, $context, true), [
|
||
|
'action' => static::transform_history_action($record->ggh_action)
|
||
|
]);
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($rootpath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
|
||
|
$pathtofiles = [
|
||
|
get_string('grades', 'core_grades'),
|
||
|
get_string('feedbackhistoryfiles', 'core_grades')
|
||
|
];
|
||
|
foreach ($data as $key => $grades) {
|
||
|
$gg = $grades['gradeobject'];
|
||
|
writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
|
||
|
GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
|
||
|
unset($data[$key]['gradeobject']); // Do not want to export this later.
|
||
|
}
|
||
|
|
||
|
writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of categories history.
|
||
|
$sql = "
|
||
|
SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action
|
||
|
FROM {grade_categories_history} gch
|
||
|
WHERE gch.courseid $incoursesql
|
||
|
AND gch.loggeduser = :userid
|
||
|
ORDER BY gch.courseid, gch.timemodified, gch.id";
|
||
|
$params = array_merge($incourseparams, ['userid' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
|
||
|
$carry[] = [
|
||
|
'name' => $record->fullname,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'logged_in_user_was_you' => transform::yesno(true),
|
||
|
'action' => static::transform_history_action($record->action),
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'categories_history',
|
||
|
(object) ['modified_records' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of items history.
|
||
|
$sql = "
|
||
|
SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action
|
||
|
FROM {grade_items_history} gih
|
||
|
WHERE gih.courseid $incoursesql
|
||
|
AND gih.loggeduser = :userid
|
||
|
ORDER BY gih.courseid, gih.timemodified, gih.id";
|
||
|
$params = array_merge($incourseparams, ['userid' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
|
||
|
$carry[] = [
|
||
|
'name' => $record->itemname,
|
||
|
'module' => $record->itemmodule,
|
||
|
'info' => $record->iteminfo,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'logged_in_user_was_you' => transform::yesno(true),
|
||
|
'action' => static::transform_history_action($record->action),
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'items_history',
|
||
|
(object) ['modified_records' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of grades in course.
|
||
|
$sql = "
|
||
|
SELECT $ggfields, $gifields, $scalefields
|
||
|
FROM {grade_grades} gg
|
||
|
JOIN {grade_items} gi
|
||
|
ON gg.itemid = gi.id
|
||
|
LEFT JOIN {scale} sc
|
||
|
ON sc.id = gi.scaleid
|
||
|
WHERE gi.courseid $incoursesql
|
||
|
AND gg.userid <> :userid1 -- Our grades have already been exported.
|
||
|
AND gg.usermodified = :userid2
|
||
|
ORDER BY gi.courseid, gg.timemodified, gg.id";
|
||
|
$params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
|
||
|
$context = context_course::instance($record->gi_courseid);
|
||
|
$gg = static::extract_grade_grade_from_record($record);
|
||
|
$carry[] = array_merge(static::transform_grade($gg, $context, false), [
|
||
|
'userid' => transform::user($gg->userid),
|
||
|
'created_or_modified_by_you' => transform::yesno(true),
|
||
|
]);
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
|
||
|
$pathtofiles = [
|
||
|
get_string('grades', 'core_grades'),
|
||
|
get_string('feedbackfiles', 'core_grades')
|
||
|
];
|
||
|
foreach ($data as $key => $grades) {
|
||
|
$gg = $grades['gradeobject'];
|
||
|
writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
|
||
|
GRADE_FEEDBACK_FILEAREA, $gg->id);
|
||
|
unset($data[$key]['gradeobject']); // Do not want to export this later.
|
||
|
}
|
||
|
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of grades history in course.
|
||
|
$sql = "
|
||
|
SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser
|
||
|
FROM {grade_grades_history} ggh
|
||
|
JOIN {grade_items} gi
|
||
|
ON ggh.itemid = gi.id
|
||
|
LEFT JOIN {scale} sc
|
||
|
ON sc.id = gi.scaleid
|
||
|
WHERE gi.courseid $incoursesql
|
||
|
AND ggh.userid <> :userid1 -- We've already exported our history.
|
||
|
AND (ggh.loggeduser = :userid2
|
||
|
OR ggh.usermodified = :userid3)
|
||
|
ORDER BY gi.courseid, ggh.timemodified, ggh.id";
|
||
|
$params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) {
|
||
|
$context = context_course::instance($record->gi_courseid);
|
||
|
$gg = static::extract_grade_grade_from_record($record, true);
|
||
|
$carry[] = array_merge(static::transform_grade($gg, $context, true), [
|
||
|
'userid' => transform::user($gg->userid),
|
||
|
'logged_in_user_was_you' => transform::yesno($userid == $record->loggeduser),
|
||
|
'author_of_change_was_you' => transform::yesno($userid == $gg->usermodified),
|
||
|
'action' => static::transform_history_action($record->ggh_action),
|
||
|
]);
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = context_course::instance($courseid);
|
||
|
|
||
|
$pathtofiles = [
|
||
|
get_string('grades', 'core_grades'),
|
||
|
get_string('feedbackhistoryfiles', 'core_grades')
|
||
|
];
|
||
|
foreach ($data as $key => $grades) {
|
||
|
$gg = $grades['gradeobject'];
|
||
|
writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
|
||
|
GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
|
||
|
unset($data[$key]['gradeobject']); // Do not want to export this later.
|
||
|
}
|
||
|
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'grades_history',
|
||
|
(object) ['modified_records' => $data]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
|
||
|
switch ($context->contextlevel) {
|
||
|
case CONTEXT_USER:
|
||
|
// The user context is only reported when there are orphan historical grades, so we only delete those.
|
||
|
static::delete_orphan_historical_grades($context->instanceid);
|
||
|
break;
|
||
|
|
||
|
case CONTEXT_COURSE:
|
||
|
// We must not change the structure of the course, so we only delete user content.
|
||
|
$itemids = static::get_item_ids_from_course_ids([$context->instanceid]);
|
||
|
if (empty($itemids)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self::delete_files($itemids, true);
|
||
|
self::delete_files($itemids, false);
|
||
|
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
|
||
|
$DB->delete_records_select('grade_grades', "itemid $insql", $inparams);
|
||
|
$DB->delete_records_select('grade_grades_history', "itemid $insql", $inparams);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 = $contextlist->get_user()->id;
|
||
|
|
||
|
$courseids = [];
|
||
|
foreach ($contextlist->get_contexts() as $context) {
|
||
|
if ($context->contextlevel == CONTEXT_USER && $userid == $context->instanceid) {
|
||
|
// User attempts to delete data in their own context.
|
||
|
static::delete_orphan_historical_grades($userid);
|
||
|
|
||
|
} else if ($context->contextlevel == CONTEXT_COURSE) {
|
||
|
// Log the list of course IDs.
|
||
|
$courseids[] = $context->instanceid;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$itemids = static::get_item_ids_from_course_ids($courseids);
|
||
|
if (empty($itemids)) {
|
||
|
// Our job here is done!
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Delete all the files.
|
||
|
self::delete_files($itemids, true, [$userid]);
|
||
|
self::delete_files($itemids, false, [$userid]);
|
||
|
|
||
|
// Delete all the grades.
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
|
||
|
$params = array_merge($inparams, ['userid' => $userid]);
|
||
|
|
||
|
$DB->delete_records_select('grade_grades', "itemid $insql AND userid = :userid", $params);
|
||
|
$DB->delete_records_select('grade_grades_history', "itemid $insql AND userid = :userid", $params);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Delete multiple users within a single context.
|
||
|
*
|
||
|
* @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to
|
||
|
* delete information for.
|
||
|
*/
|
||
|
public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
|
||
|
global $DB;
|
||
|
|
||
|
$context = $userlist->get_context();
|
||
|
$userids = $userlist->get_userids();
|
||
|
if ($context->contextlevel == CONTEXT_USER) {
|
||
|
if (array_search($context->instanceid, $userids) !== false) {
|
||
|
static::delete_orphan_historical_grades($context->instanceid);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($context->contextlevel != CONTEXT_COURSE) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$itemids = static::get_item_ids_from_course_ids([$context->instanceid]);
|
||
|
if (empty($itemids)) {
|
||
|
// Our job here is done!
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Delete all the files.
|
||
|
self::delete_files($itemids, true, $userids);
|
||
|
self::delete_files($itemids, false, $userids);
|
||
|
|
||
|
// Delete all the grades.
|
||
|
list($itemsql, $itemparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
|
||
|
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||
|
$params = array_merge($itemparams, $userparams);
|
||
|
|
||
|
$DB->delete_records_select('grade_grades', "itemid $itemsql AND userid $usersql", $params);
|
||
|
$DB->delete_records_select('grade_grades_history', "itemid $itemsql AND userid $usersql", $params);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Delete orphan historical grades.
|
||
|
*
|
||
|
* @param int $userid The user ID.
|
||
|
* @return void
|
||
|
*/
|
||
|
protected static function delete_orphan_historical_grades($userid) {
|
||
|
global $DB;
|
||
|
$sql = "
|
||
|
SELECT ggh.id
|
||
|
FROM {grade_grades_history} ggh
|
||
|
LEFT JOIN {grade_items} gi
|
||
|
ON ggh.itemid = gi.id
|
||
|
WHERE gi.id IS NULL
|
||
|
AND ggh.userid = :userid";
|
||
|
$ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
|
||
|
if (empty($ids)) {
|
||
|
return;
|
||
|
}
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
|
||
|
|
||
|
// First, let's delete their files.
|
||
|
$sql = "
|
||
|
SELECT gi.id
|
||
|
FROM {grade_grades_history} ggh
|
||
|
JOIN {grade_items} gi
|
||
|
ON gi.id = ggh.itemid
|
||
|
WHERE ggh.userid = :userid";
|
||
|
$params = ['userid' => $userid];
|
||
|
$gradeitems = $DB->get_records_sql($sql, $params);
|
||
|
if ($gradeitems) {
|
||
|
$itemids = array_keys($gradeitems);
|
||
|
self::delete_files($itemids, true, [$userid]);
|
||
|
}
|
||
|
|
||
|
$DB->delete_records_select('grade_grades_history', "id $insql", $inparams);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Export the user data related to outcomes.
|
||
|
*
|
||
|
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||
|
* @return void
|
||
|
*/
|
||
|
protected static function export_user_data_outcomes_in_contexts(approved_contextlist $contextlist) {
|
||
|
global $DB;
|
||
|
|
||
|
$rootpath = [get_string('grades', 'core_grades')];
|
||
|
$relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
|
||
|
$userid = $contextlist->get_user()->id;
|
||
|
|
||
|
// Reorganise the contexts.
|
||
|
$reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
|
||
|
if ($context->contextlevel == CONTEXT_SYSTEM) {
|
||
|
$carry['in_system'] = true;
|
||
|
} else if ($context->contextlevel == CONTEXT_COURSE) {
|
||
|
$carry['courseids'][] = $context->instanceid;
|
||
|
}
|
||
|
return $carry;
|
||
|
}, [
|
||
|
'in_system' => false,
|
||
|
'courseids' => []
|
||
|
]);
|
||
|
|
||
|
// Construct SQL.
|
||
|
$sqltemplateparts = [];
|
||
|
$templateparams = [];
|
||
|
if ($reduced['in_system']) {
|
||
|
$sqltemplateparts[] = '{prefix}.courseid IS NULL';
|
||
|
}
|
||
|
if (!empty($reduced['courseids'])) {
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
|
||
|
$sqltemplateparts[] = "{prefix}.courseid $insql";
|
||
|
$templateparams = array_merge($templateparams, $inparams);
|
||
|
}
|
||
|
if (empty($sqltemplateparts)) {
|
||
|
return;
|
||
|
}
|
||
|
$sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
|
||
|
|
||
|
// Export edited outcomes.
|
||
|
$sqlwhere = str_replace('{prefix}', 'go', $sqltemplate);
|
||
|
$sql = "
|
||
|
SELECT go.id, COALESCE(go.courseid, 0) AS courseid, go.shortname, go.fullname, go.timemodified
|
||
|
FROM {grade_outcomes} go
|
||
|
WHERE $sqlwhere
|
||
|
AND go.usermodified = :userid
|
||
|
ORDER BY go.courseid, go.timemodified, go.id";
|
||
|
$params = array_merge($templateparams, ['userid' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
|
||
|
$carry[] = [
|
||
|
'shortname' => $record->shortname,
|
||
|
'fullname' => $record->fullname,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'created_or_modified_by_you' => transform::yesno(true)
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = $courseid ? context_course::instance($courseid) : context_system::instance();
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes',
|
||
|
(object) ['outcomes' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of outcomes history.
|
||
|
$sqlwhere = str_replace('{prefix}', 'goh', $sqltemplate);
|
||
|
$sql = "
|
||
|
SELECT goh.id, COALESCE(goh.courseid, 0) AS courseid, goh.shortname, goh.fullname, goh.timemodified, goh.action
|
||
|
FROM {grade_outcomes_history} goh
|
||
|
WHERE $sqlwhere
|
||
|
AND goh.loggeduser = :userid
|
||
|
ORDER BY goh.courseid, goh.timemodified, goh.id";
|
||
|
$params = array_merge($templateparams, ['userid' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
|
||
|
$carry[] = [
|
||
|
'shortname' => $record->shortname,
|
||
|
'fullname' => $record->fullname,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'logged_in_user_was_you' => transform::yesno(true),
|
||
|
'action' => static::transform_history_action($record->action)
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = $courseid ? context_course::instance($courseid) : context_system::instance();
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes_history',
|
||
|
(object) ['modified_records' => $data]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Export the user data related to scales.
|
||
|
*
|
||
|
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||
|
* @return void
|
||
|
*/
|
||
|
protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) {
|
||
|
global $DB;
|
||
|
|
||
|
$rootpath = [get_string('grades', 'core_grades')];
|
||
|
$relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
|
||
|
$userid = $contextlist->get_user()->id;
|
||
|
|
||
|
// Reorganise the contexts.
|
||
|
$reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
|
||
|
if ($context->contextlevel == CONTEXT_SYSTEM) {
|
||
|
$carry['in_system'] = true;
|
||
|
} else if ($context->contextlevel == CONTEXT_COURSE) {
|
||
|
$carry['courseids'][] = $context->instanceid;
|
||
|
}
|
||
|
return $carry;
|
||
|
}, [
|
||
|
'in_system' => false,
|
||
|
'courseids' => []
|
||
|
]);
|
||
|
|
||
|
// Construct SQL.
|
||
|
$sqltemplateparts = [];
|
||
|
$templateparams = [];
|
||
|
if ($reduced['in_system']) {
|
||
|
$sqltemplateparts[] = '{prefix}.courseid = 0';
|
||
|
}
|
||
|
if (!empty($reduced['courseids'])) {
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
|
||
|
$sqltemplateparts[] = "{prefix}.courseid $insql";
|
||
|
$templateparams = array_merge($templateparams, $inparams);
|
||
|
}
|
||
|
if (empty($sqltemplateparts)) {
|
||
|
return;
|
||
|
}
|
||
|
$sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
|
||
|
|
||
|
// Export edited scales.
|
||
|
$sqlwhere = str_replace('{prefix}', 's', $sqltemplate);
|
||
|
$sql = "
|
||
|
SELECT s.id, s.courseid, s.name, s.timemodified
|
||
|
FROM {scale} s
|
||
|
WHERE $sqlwhere
|
||
|
AND s.userid = :userid
|
||
|
ORDER BY s.courseid, s.timemodified, s.id";
|
||
|
$params = array_merge($templateparams, ['userid' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
|
||
|
$carry[] = [
|
||
|
'name' => $record->name,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'created_or_modified_by_you' => transform::yesno(true)
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = $courseid ? context_course::instance($courseid) : context_system::instance();
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'scales',
|
||
|
(object) ['scales' => $data]);
|
||
|
});
|
||
|
|
||
|
// Export edits of scales history.
|
||
|
$sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate);
|
||
|
$sql = "
|
||
|
SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser
|
||
|
FROM {scale_history} sh
|
||
|
WHERE $sqlwhere
|
||
|
AND sh.loggeduser = :userid1
|
||
|
OR sh.userid = :userid2
|
||
|
ORDER BY sh.courseid, sh.timemodified, sh.id";
|
||
|
$params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]);
|
||
|
$recordset = $DB->get_recordset_sql($sql, $params);
|
||
|
static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) {
|
||
|
$carry[] = [
|
||
|
'name' => $record->name,
|
||
|
'timemodified' => transform::datetime($record->timemodified),
|
||
|
'author_of_change_was_you' => transform::yesno($record->userid == $userid),
|
||
|
'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid),
|
||
|
'action' => static::transform_history_action($record->action)
|
||
|
];
|
||
|
return $carry;
|
||
|
|
||
|
}, function($courseid, $data) use ($relatedtomepath) {
|
||
|
$context = $courseid ? context_course::instance($courseid) : context_system::instance();
|
||
|
writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history',
|
||
|
(object) ['modified_records' => $data]);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extract grade_grade from a record.
|
||
|
*
|
||
|
* @param stdClass $record The record.
|
||
|
* @param bool $ishistory Whether we're extracting a historical grade.
|
||
|
* @return grade_grade
|
||
|
*/
|
||
|
protected static function extract_grade_grade_from_record(stdClass $record, $ishistory = false) {
|
||
|
$prefix = $ishistory ? 'ggh_' : 'gg_';
|
||
|
$ggrecord = static::extract_record($record, $prefix);
|
||
|
if ($ishistory) {
|
||
|
// The grade history is not a real grade_grade so we remove the ID.
|
||
|
$historyid = $ggrecord->id;
|
||
|
unset($ggrecord->id);
|
||
|
}
|
||
|
$gg = new grade_grade($ggrecord, false);
|
||
|
|
||
|
// There is a grade item in the record.
|
||
|
if (!empty($record->gi_id)) {
|
||
|
$gi = new grade_item(static::extract_record($record, 'gi_'), false);
|
||
|
$gg->grade_item = $gi; // This is a common hack throughout the grades API.
|
||
|
}
|
||
|
|
||
|
// Load the scale, when it still exists.
|
||
|
if (!empty($gi->scaleid) && !empty($record->sc_id)) {
|
||
|
$scalerec = static::extract_record($record, 'sc_');
|
||
|
$gi->scale = new grade_scale($scalerec, false);
|
||
|
$gi->scale->load_items();
|
||
|
}
|
||
|
|
||
|
if ($ishistory) {
|
||
|
$gg->historyid = $historyid;
|
||
|
}
|
||
|
|
||
|
return $gg;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extract a record from another one.
|
||
|
*
|
||
|
* @param object $record The record to extract from.
|
||
|
* @param string $prefix The prefix used.
|
||
|
* @return object
|
||
|
*/
|
||
|
protected static function extract_record($record, $prefix) {
|
||
|
$result = [];
|
||
|
$prefixlength = strlen($prefix);
|
||
|
foreach ($record as $key => $value) {
|
||
|
if (strpos($key, $prefix) === 0) {
|
||
|
$result[substr($key, $prefixlength)] = $value;
|
||
|
}
|
||
|
}
|
||
|
return (object) $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get fields SQL for a grade related object.
|
||
|
*
|
||
|
* @param string $target The related object.
|
||
|
* @param string $alias The table alias.
|
||
|
* @param string $prefix A prefix.
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function get_fields_sql($target, $alias, $prefix) {
|
||
|
switch ($target) {
|
||
|
case 'grade_category':
|
||
|
case 'grade_grade':
|
||
|
case 'grade_item':
|
||
|
case 'grade_outcome':
|
||
|
case 'grade_scale':
|
||
|
$obj = new $target([], false);
|
||
|
$fields = array_merge(array_keys($obj->optional_fields), $obj->required_fields);
|
||
|
break;
|
||
|
|
||
|
case 'grade_grades_history':
|
||
|
$fields = ['id', 'action', 'oldid', 'source', 'timemodified', 'loggeduser', 'itemid', 'userid', 'rawgrade',
|
||
|
'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime',
|
||
|
'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat'];
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new \coding_exception('Unrecognised target: ' . $target);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return implode(', ', array_map(function($field) use ($alias, $prefix) {
|
||
|
return "{$alias}.{$field} AS {$prefix}{$field}";
|
||
|
}, $fields));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all the items IDs from course IDs.
|
||
|
*
|
||
|
* @param array $courseids The course IDs.
|
||
|
* @return array
|
||
|
*/
|
||
|
protected static function get_item_ids_from_course_ids($courseids) {
|
||
|
global $DB;
|
||
|
if (empty($courseids)) {
|
||
|
return [];
|
||
|
}
|
||
|
list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
|
||
|
return $DB->get_fieldset_select('grade_items', 'id', "courseid $insql", $inparams);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Loop and export from a recordset.
|
||
|
*
|
||
|
* @param moodle_recordset $recordset The recordset.
|
||
|
* @param string $splitkey The record key to determine when to export.
|
||
|
* @param mixed $initial The initial data to reduce from.
|
||
|
* @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
|
||
|
* @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
|
||
|
* @return void
|
||
|
*/
|
||
|
protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
|
||
|
callable $reducer, callable $export) {
|
||
|
|
||
|
$data = $initial;
|
||
|
$lastid = null;
|
||
|
|
||
|
foreach ($recordset as $record) {
|
||
|
if ($lastid !== null && $record->{$splitkey} != $lastid) {
|
||
|
$export($lastid, $data);
|
||
|
$data = $initial;
|
||
|
}
|
||
|
$data = $reducer($data, $record);
|
||
|
$lastid = $record->{$splitkey};
|
||
|
}
|
||
|
$recordset->close();
|
||
|
|
||
|
if ($lastid !== null) {
|
||
|
$export($lastid, $data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transform an history action.
|
||
|
*
|
||
|
* @param int $action The action.
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function transform_history_action($action) {
|
||
|
switch ($action) {
|
||
|
case GRADE_HISTORY_INSERT:
|
||
|
return get_string('privacy:request:historyactioninsert', 'core_grades');
|
||
|
break;
|
||
|
case GRADE_HISTORY_UPDATE:
|
||
|
return get_string('privacy:request:historyactionupdate', 'core_grades');
|
||
|
break;
|
||
|
case GRADE_HISTORY_DELETE:
|
||
|
return get_string('privacy:request:historyactiondelete', 'core_grades');
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return '?';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transform a grade.
|
||
|
*
|
||
|
* @param grade_grade $gg The grade object.
|
||
|
* @param context $context The context.
|
||
|
* @param bool $ishistory Whether we're extracting a historical grade.
|
||
|
* @return array
|
||
|
*/
|
||
|
protected static function transform_grade(grade_grade $gg, context $context, bool $ishistory) {
|
||
|
$gi = $gg->load_grade_item();
|
||
|
$timemodified = $gg->timemodified ? transform::datetime($gg->timemodified) : null;
|
||
|
$timecreated = $gg->timecreated ? transform::datetime($gg->timecreated) : $timemodified; // When null we use timemodified.
|
||
|
|
||
|
$filearea = $ishistory ? GRADE_HISTORY_FEEDBACK_FILEAREA : GRADE_FEEDBACK_FILEAREA;
|
||
|
$itemid = $ishistory ? $gg->historyid : $gg->id;
|
||
|
$subpath = $ishistory ? get_string('feedbackhistoryfiles', 'core_grades') : get_string('feedbackfiles', 'core_grades');
|
||
|
|
||
|
$pathtofiles = [
|
||
|
get_string('grades', 'core_grades'),
|
||
|
$subpath
|
||
|
];
|
||
|
$gg->feedback = writer::with_context($gg->get_context())->rewrite_pluginfile_urls(
|
||
|
$pathtofiles,
|
||
|
GRADE_FILE_COMPONENT,
|
||
|
$filearea,
|
||
|
$itemid,
|
||
|
$gg->feedback
|
||
|
);
|
||
|
|
||
|
return [
|
||
|
'gradeobject' => $gg,
|
||
|
'item' => $gi->get_name(),
|
||
|
'grade' => $gg->finalgrade,
|
||
|
'grade_formatted' => grade_format_gradevalue($gg->finalgrade, $gi),
|
||
|
'feedback' => format_text($gg->feedback, $gg->feedbackformat, ['context' => $context]),
|
||
|
'information' => format_text($gg->information, $gg->informationformat, ['context' => $context]),
|
||
|
'timecreated' => $timecreated,
|
||
|
'timemodified' => $timemodified,
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handles deleting files for a given list of grade items.
|
||
|
*
|
||
|
* If an array of userids if given then it handles deleting files for those users.
|
||
|
*
|
||
|
* @param array $itemids
|
||
|
* @param bool $ishistory
|
||
|
* @param array|null $userids
|
||
|
* @throws \coding_exception
|
||
|
* @throws \dml_exception
|
||
|
*/
|
||
|
protected static function delete_files(array $itemids, bool $ishistory, array $userids = null) {
|
||
|
global $DB;
|
||
|
|
||
|
list($iteminnsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
|
||
|
if (!is_null($userids)) {
|
||
|
list($userinnsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||
|
$params = array_merge($params, $userparams);
|
||
|
}
|
||
|
|
||
|
if ($ishistory) {
|
||
|
$gradefields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
|
||
|
$gradetable = 'grade_grades_history';
|
||
|
$tableprefix = 'ggh';
|
||
|
$filearea = GRADE_HISTORY_FEEDBACK_FILEAREA;
|
||
|
} else {
|
||
|
$gradefields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
|
||
|
$gradetable = 'grade_grades';
|
||
|
$tableprefix = 'gg';
|
||
|
$filearea = GRADE_FEEDBACK_FILEAREA;
|
||
|
}
|
||
|
|
||
|
$gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
|
||
|
|
||
|
$fs = new \file_storage();
|
||
|
$sql = "SELECT $gradefields, $gifields
|
||
|
FROM {{$gradetable}} $tableprefix
|
||
|
JOIN {grade_items} gi
|
||
|
ON gi.id = {$tableprefix}.itemid
|
||
|
WHERE gi.id $iteminnsql ";
|
||
|
if (!is_null($userids)) {
|
||
|
$sql .= "AND {$tableprefix}.userid $userinnsql";
|
||
|
}
|
||
|
|
||
|
$grades = $DB->get_recordset_sql($sql, $params);
|
||
|
foreach ($grades as $grade) {
|
||
|
$gg = static::extract_grade_grade_from_record($grade, $ishistory);
|
||
|
$fileitemid = ($ishistory) ? $gg->historyid : $gg->id;
|
||
|
$fs->delete_area_files($gg->get_context()->id, GRADE_FILE_COMPONENT, $filearea, $fileitemid);
|
||
|
}
|
||
|
$grades->close();
|
||
|
}
|
||
|
}
|