fe76a2562e
The existing query was inefficient using an index scan and index merges. This change splits the query up into many individually much more efficient queries. This should significantly improve performance on large sites.
1330 lines
56 KiB
PHP
1330 lines
56 KiB
PHP
<?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_grades\privacy\grade_grade_with_history;
|
|
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
|
|
JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND goh.courseid > 0
|
|
AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1";
|
|
$params = [
|
|
'courselevel1' => CONTEXT_COURSE,
|
|
'userid1' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1
|
|
AND (goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1";
|
|
$params = [
|
|
'syscontextid1' => SYSCONTEXTID,
|
|
'courselevel1' => CONTEXT_COURSE,
|
|
'userid1' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2
|
|
AND gch.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel2";
|
|
$params = [
|
|
'courselevel2' => CONTEXT_COURSE,
|
|
'userid2' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {grade_items_history} gih ON gih.loggeduser = :userid3
|
|
AND gih.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel3";
|
|
$params = [
|
|
'courselevel3' => CONTEXT_COURSE,
|
|
'userid3' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {scale_history} sh ON sh.userid = :userid4
|
|
AND sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4";
|
|
$params = [
|
|
'courselevel4' => CONTEXT_COURSE,
|
|
'userid4' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {scale_history} sh ON sh.loggeduser = :userid5
|
|
AND sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4";
|
|
$params = [
|
|
'courselevel4' => CONTEXT_COURSE,
|
|
'userid5' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {scale_history} sh ON sh.userid = :userid4 AND sh.courseid = 0 AND ctx.id = :syscontextid2";
|
|
$params = [
|
|
'syscontextid2' => SYSCONTEXTID,
|
|
'userid4' => $userid,
|
|
];
|
|
$contextlist->add_from_sql($sql, $params);
|
|
$sql = "
|
|
SELECT DISTINCT ctx.id
|
|
FROM {context} ctx
|
|
JOIN {scale_history} sh ON sh.loggeduser = :userid5 AND sh.courseid = 0 AND ctx.id = :syscontextid2";
|
|
$params = [
|
|
'syscontextid2' => SYSCONTEXTID,
|
|
'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) {
|
|
/** @var grade_grade_with_history */
|
|
$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) {
|
|
/** @var grade_grade_with_history */
|
|
$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) {
|
|
$gg = new grade_grade_with_history($ggrecord, false);
|
|
} else {
|
|
$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();
|
|
}
|
|
|
|
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.
|
|
|
|
if ($gg instanceof grade_grade_with_history) {
|
|
$filearea = GRADE_HISTORY_FEEDBACK_FILEAREA;
|
|
$itemid = $gg->historyid;
|
|
$subpath = get_string('feedbackhistoryfiles', 'core_grades');
|
|
} else {
|
|
$filearea = GRADE_FEEDBACK_FILEAREA;
|
|
$itemid = $gg->id;
|
|
$subpath = 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);
|
|
if ($gg instanceof grade_grade_with_history) {
|
|
$fileitemid = $gg->historyid;
|
|
} else {
|
|
$fileitemid = $gg->id;
|
|
}
|
|
$fs->delete_area_files($gg->get_context()->id, GRADE_FILE_COMPONENT, $filearea, $fileitemid);
|
|
}
|
|
$grades->close();
|
|
}
|
|
}
|