Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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.idFROM {grade_outcomes} goJOIN {context} ctxON (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.idFROM {scale} sJOIN {context} ctxON (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.idFROM {context} ctxJOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND goh.courseid > 0AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1";$params = ['courselevel1' => CONTEXT_COURSE,'userid1' => $userid,];$contextlist->add_from_sql($sql, $params);$sql = "SELECT DISTINCT ctx.idFROM {context} ctxJOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1AND (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.idFROM {context} ctxJOIN {grade_categories_history} gch ON gch.loggeduser = :userid2AND gch.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel2";$params = ['courselevel2' => CONTEXT_COURSE,'userid2' => $userid,];$contextlist->add_from_sql($sql, $params);$sql = "SELECT DISTINCT ctx.idFROM {context} ctxJOIN {grade_items_history} gih ON gih.loggeduser = :userid3AND gih.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel3";$params = ['courselevel3' => CONTEXT_COURSE,'userid3' => $userid,];$contextlist->add_from_sql($sql, $params);$sql = "SELECT DISTINCT ctx.idFROM {context} ctxJOIN {scale_history} sh ON sh.userid = :userid4AND 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.idFROM {context} ctxJOIN {scale_history} sh ON sh.loggeduser = :userid5AND 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.idFROM {context} ctxJOIN {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.idFROM {context} ctxJOIN {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.idFROM {grade_items} giJOIN {context} ctxON ctx.instanceid = gi.courseidAND ctx.contextlevel = :courselevelJOIN {grade_grades} ggON gg.itemid = gi.idWHERE 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.idFROM {grade_items} giJOIN {context} ctxON ctx.instanceid = gi.courseidAND ctx.contextlevel = :courselevelJOIN {grade_grades_history} gghON ggh.itemid = gi.idWHERE ggh.userid = :userid1OR ggh.loggeduser = :userid2OR 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.idFROM {context} ctxJOIN {grade_grades_history} gghON ctx.contextlevel = :userlevelAND ggh.userid = ctx.instanceidAND (ggh.userid = :userid1OR ggh.usermodified = :userid2OR ggh.loggeduser = :userid3)LEFT JOIN {grade_items} giON ggh.itemid = gi.idWHERE 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 usermodifiedFROM {grade_outcomes}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('usermodified', $sql, $params);$sql = "SELECT loggeduserFROM {grade_outcomes_history}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('loggeduser', $sql, $params);$sql = "SELECT useridFROM {scale}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT loggeduser, useridFROM {scale_history}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('loggeduser', $sql, $params);$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT loggeduserFROM {grade_items_history}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('loggeduser', $sql, $params);$sql = "SELECT ggh.useridFROM {grade_grades_history} gghJOIN {grade_items} gi ON ggh.itemid = gi.idWHERE gi.courseid = :contextinstanceid";$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT gg.userid, gg.usermodifiedFROM {grade_grades} ggJOIN {grade_items} gi ON gg.itemid = gi.idWHERE gi.courseid = :contextinstanceid";$userlist->add_from_sql('userid', $sql, $params);$userlist->add_from_sql('usermodified', $sql, $params);$sql = "SELECT loggeduserFROM {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 usermodifiedFROM {grade_outcomes}WHERE (courseid IS NULL OR courseid < 1)";$userlist->add_from_sql('usermodified', $sql, []);$sql = "SELECT loggeduserFROM {grade_outcomes_history}WHERE (courseid IS NULL OR courseid < 1)";$userlist->add_from_sql('loggeduser', $sql, []);$sql = "SELECT useridFROM {scale}WHERE courseid = :contextinstanceid";$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT loggeduser, useridFROM {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.useridFROM {grade_grades_history} gghLEFT JOIN {grade_items} gi ON ggh.itemid = gi.idWHERE gi.id IS NULLAND 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 ctxidFROM {grade_grades_history} gghJOIN {context} ctxON ctx.instanceid = ggh.useridAND ctx.contextlevel = :userlevelLEFT JOIN {grade_items} giON gi.id = ggh.itemidWHERE gi.id IS NULLAND (ggh.userid $inuseridsqlOR ggh.usermodified $inusermodifiedsqlOR ggh.loggeduser $inloggedusersql)AND (ggh.userid = :userid1OR ggh.usermodified = :userid2OR 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, $scalefieldsFROM {grade_grades} ggJOIN {grade_items} giON gi.id = gg.itemidLEFT JOIN {scale} scON sc.id = gi.scaleidWHERE gi.courseid $incoursesqlAND gg.userid = :useridORDER 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, $scalefieldsFROM {grade_grades_history} gghJOIN {grade_items} giON gi.id = ggh.itemidLEFT JOIN {scale} scON sc.id = gi.scaleidWHERE gi.courseid $incoursesqlAND ggh.userid = :useridORDER 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.actionFROM {grade_categories_history} gchWHERE gch.courseid $incoursesqlAND gch.loggeduser = :useridORDER 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.actionFROM {grade_items_history} gihWHERE gih.courseid $incoursesqlAND gih.loggeduser = :useridORDER 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, $scalefieldsFROM {grade_grades} ggJOIN {grade_items} giON gg.itemid = gi.idLEFT JOIN {scale} scON sc.id = gi.scaleidWHERE gi.courseid $incoursesqlAND gg.userid <> :userid1 -- Our grades have already been exported.AND gg.usermodified = :userid2ORDER 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 loggeduserFROM {grade_grades_history} gghJOIN {grade_items} giON ggh.itemid = gi.idLEFT JOIN {scale} scON sc.id = gi.scaleidWHERE gi.courseid $incoursesqlAND ggh.userid <> :userid1 -- We've already exported our history.AND (ggh.loggeduser = :userid2OR 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.idFROM {grade_grades_history} gghLEFT JOIN {grade_items} giON ggh.itemid = gi.idWHERE gi.id IS NULLAND 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.idFROM {grade_grades_history} gghJOIN {grade_items} giON gi.id = ggh.itemidWHERE 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.timemodifiedFROM {grade_outcomes} goWHERE $sqlwhereAND go.usermodified = :useridORDER 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.actionFROM {grade_outcomes_history} gohWHERE $sqlwhereAND goh.loggeduser = :useridORDER 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.timemodifiedFROM {scale} sWHERE $sqlwhereAND s.userid = :useridORDER 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.loggeduserFROM {scale_history} shWHERE $sqlwhereAND sh.loggeduser = :userid1OR sh.userid = :userid2ORDER 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, $gifieldsFROM {{$gradetable}} $tableprefixJOIN {grade_items} giON gi.id = {$tableprefix}.itemidWHERE 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();}}