See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Data provider. 19 * 20 * @package core_grades 21 * @copyright 2018 Frédéric Massart 22 * @author Frédéric Massart <fred@branchup.tech> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core_grades\privacy; 27 defined('MOODLE_INTERNAL') || die(); 28 29 use context; 30 use context_course; 31 use context_system; 32 use grade_item; 33 use grade_grade; 34 use grade_scale; 35 use stdClass; 36 use core_privacy\local\metadata\collection; 37 use core_privacy\local\request\approved_contextlist; 38 use core_privacy\local\request\transform; 39 use core_privacy\local\request\writer; 40 41 require_once($CFG->libdir . '/gradelib.php'); 42 43 /** 44 * Data provider class. 45 * 46 * @package core_grades 47 * @copyright 2018 Frédéric Massart 48 * @author Frédéric Massart <fred@branchup.tech> 49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 50 */ 51 class provider implements 52 \core_privacy\local\metadata\provider, 53 \core_privacy\local\request\subsystem\provider, 54 \core_privacy\local\request\core_userlist_provider { 55 56 /** 57 * Returns metadata. 58 * 59 * @param collection $collection The initialised collection to add items to. 60 * @return collection A listing of user data stored through this system. 61 */ 62 public static function get_metadata(collection $collection) : collection { 63 64 // Tables without 'real' user information. 65 $collection->add_database_table('grade_outcomes', [ 66 'timemodified' => 'privacy:metadata:outcomes:timemodified', 67 'usermodified' => 'privacy:metadata:outcomes:usermodified', 68 ], 'privacy:metadata:outcomes'); 69 70 $collection->add_database_table('grade_outcomes_history', [ 71 'timemodified' => 'privacy:metadata:history:timemodified', 72 'loggeduser' => 'privacy:metadata:history:loggeduser', 73 ], 'privacy:metadata:outcomeshistory'); 74 75 $collection->add_database_table('grade_categories_history', [ 76 'timemodified' => 'privacy:metadata:history:timemodified', 77 'loggeduser' => 'privacy:metadata:history:loggeduser', 78 ], 'privacy:metadata:categorieshistory'); 79 80 $collection->add_database_table('grade_items_history', [ 81 'timemodified' => 'privacy:metadata:history:timemodified', 82 'loggeduser' => 'privacy:metadata:history:loggeduser', 83 ], 'privacy:metadata:itemshistory'); 84 85 $collection->add_database_table('scale', [ 86 'userid' => 'privacy:metadata:scale:userid', 87 'timemodified' => 'privacy:metadata:scale:timemodified', 88 ], 'privacy:metadata:scale'); 89 90 $collection->add_database_table('scale_history', [ 91 'userid' => 'privacy:metadata:scale:userid', 92 'timemodified' => 'privacy:metadata:history:timemodified', 93 'loggeduser' => 'privacy:metadata:history:loggeduser', 94 ], 'privacy:metadata:scalehistory'); 95 96 // Table with user information. 97 $gradescommonfields = [ 98 'userid' => 'privacy:metadata:grades:userid', 99 'usermodified' => 'privacy:metadata:grades:usermodified', 100 'finalgrade' => 'privacy:metadata:grades:finalgrade', 101 'feedback' => 'privacy:metadata:grades:feedback', 102 'information' => 'privacy:metadata:grades:information', 103 ]; 104 105 $collection->add_database_table('grade_grades', array_merge($gradescommonfields, [ 106 'timemodified' => 'privacy:metadata:grades:timemodified', 107 ]), 'privacy:metadata:grades'); 108 109 $collection->add_database_table('grade_grades_history', array_merge($gradescommonfields, [ 110 'timemodified' => 'privacy:metadata:history:timemodified', 111 'loggeduser' => 'privacy:metadata:history:loggeduser', 112 ]), 'privacy:metadata:gradeshistory'); 113 114 // The following tables are reported but not exported/deleted because their data is temporary and only 115 // used during an import. It's content is deleted after a successful, or failed, import. 116 117 $collection->add_database_table('grade_import_newitem', [ 118 'itemname' => 'privacy:metadata:grade_import_newitem:itemname', 119 'importcode' => 'privacy:metadata:grade_import_newitem:importcode', 120 'importer' => 'privacy:metadata:grade_import_newitem:importer' 121 ], 'privacy:metadata:grade_import_newitem'); 122 123 $collection->add_database_table('grade_import_values', [ 124 'userid' => 'privacy:metadata:grade_import_values:userid', 125 'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade', 126 'feedback' => 'privacy:metadata:grade_import_values:feedback', 127 'importcode' => 'privacy:metadata:grade_import_values:importcode', 128 'importer' => 'privacy:metadata:grade_import_values:importer', 129 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback' 130 ], 'privacy:metadata:grade_import_values'); 131 132 $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); 133 134 return $collection; 135 } 136 137 /** 138 * Get the list of contexts that contain user information for the specified user. 139 * 140 * @param int $userid The user to search. 141 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. 142 */ 143 public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { 144 $contextlist = new \core_privacy\local\request\contextlist(); 145 146 // Add where we modified outcomes. 147 $sql = " 148 SELECT DISTINCT ctx.id 149 FROM {grade_outcomes} go 150 JOIN {context} ctx 151 ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel) 152 OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid) 153 WHERE go.usermodified = :userid"; 154 $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; 155 $contextlist->add_from_sql($sql, $params); 156 157 // Add where we modified scales. 158 $sql = " 159 SELECT DISTINCT ctx.id 160 FROM {scale} s 161 JOIN {context} ctx 162 ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel) 163 OR (s.courseid = 0 AND ctx.id = :syscontextid) 164 WHERE s.userid = :userid"; 165 $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; 166 $contextlist->add_from_sql($sql, $params); 167 168 // Add where appear in the history of outcomes, categories, scales or items. 169 $sql = " 170 SELECT DISTINCT ctx.id 171 FROM {context} ctx 172 LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND ( 173 (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1) 174 OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1) 175 ) 176 LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND ( 177 gch.courseid = ctx.instanceid 178 AND ctx.contextlevel = :courselevel2 179 ) 180 LEFT JOIN {grade_items_history} gih ON gih.loggeduser = :userid3 AND ( 181 gih.courseid = ctx.instanceid 182 AND ctx.contextlevel = :courselevel3 183 ) 184 LEFT JOIN {scale_history} sh 185 ON (sh.userid = :userid4 OR sh.loggeduser = :userid5) 186 AND ( 187 (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4) 188 OR (sh.courseid = 0 AND ctx.id = :syscontextid2) 189 ) 190 WHERE goh.id IS NOT NULL 191 OR gch.id IS NOT NULL 192 OR gih.id IS NOT NULL 193 OR sh.id IS NOT NULL"; 194 $params = [ 195 'syscontextid1' => SYSCONTEXTID, 196 'syscontextid2' => SYSCONTEXTID, 197 'courselevel1' => CONTEXT_COURSE, 198 'courselevel2' => CONTEXT_COURSE, 199 'courselevel3' => CONTEXT_COURSE, 200 'courselevel4' => CONTEXT_COURSE, 201 'userid1' => $userid, 202 'userid2' => $userid, 203 'userid3' => $userid, 204 'userid4' => $userid, 205 'userid5' => $userid, 206 ]; 207 $contextlist->add_from_sql($sql, $params); 208 209 // Add where we were graded or modified grades, including in the history table. 210 $sql = " 211 SELECT DISTINCT ctx.id 212 FROM {grade_items} gi 213 JOIN {context} ctx 214 ON ctx.instanceid = gi.courseid 215 AND ctx.contextlevel = :courselevel 216 JOIN {grade_grades} gg 217 ON gg.itemid = gi.id 218 WHERE gg.userid = :userid1 OR gg.usermodified = :userid2"; 219 $params = [ 220 'courselevel' => CONTEXT_COURSE, 221 'userid1' => $userid, 222 'userid2' => $userid 223 ]; 224 $contextlist->add_from_sql($sql, $params); 225 226 $sql = " 227 SELECT DISTINCT ctx.id 228 FROM {grade_items} gi 229 JOIN {context} ctx 230 ON ctx.instanceid = gi.courseid 231 AND ctx.contextlevel = :courselevel 232 JOIN {grade_grades_history} ggh 233 ON ggh.itemid = gi.id 234 WHERE ggh.userid = :userid1 235 OR ggh.loggeduser = :userid2 236 OR ggh.usermodified = :userid3"; 237 $params = [ 238 'courselevel' => CONTEXT_COURSE, 239 'userid1' => $userid, 240 'userid2' => $userid, 241 'userid3' => $userid 242 ]; 243 $contextlist->add_from_sql($sql, $params); 244 245 // Historical grades can be made orphans when the corresponding itemid is deleted. When that happens 246 // we cannot tie the historical grade to a course context, so we report the user context as a last resort. 247 $sql = " 248 SELECT DISTINCT ctx.id 249 FROM {context} ctx 250 JOIN {grade_grades_history} ggh 251 ON ctx.contextlevel = :userlevel 252 AND ggh.userid = ctx.instanceid 253 AND ( 254 ggh.userid = :userid1 255 OR ggh.usermodified = :userid2 256 OR ggh.loggeduser = :userid3 257 ) 258 LEFT JOIN {grade_items} gi 259 ON ggh.itemid = gi.id 260 WHERE gi.id IS NULL"; 261 $params = [ 262 'userlevel' => CONTEXT_USER, 263 'userid1' => $userid, 264 'userid2' => $userid, 265 'userid3' => $userid 266 ]; 267 $contextlist->add_from_sql($sql, $params); 268 269 return $contextlist; 270 } 271 272 /** 273 * Get the list of contexts that contain user information for the specified user. 274 * 275 * @param \core_privacy\local\request\userlist $userlist The userlist containing the list of users who have data 276 * in this context/plugin combination. 277 */ 278 public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) { 279 $context = $userlist->get_context(); 280 281 if ($context->contextlevel == CONTEXT_COURSE) { 282 $params = ['contextinstanceid' => $context->instanceid]; 283 284 $sql = "SELECT usermodified 285 FROM {grade_outcomes} 286 WHERE courseid = :contextinstanceid"; 287 $userlist->add_from_sql('usermodified', $sql, $params); 288 289 $sql = "SELECT loggeduser 290 FROM {grade_outcomes_history} 291 WHERE courseid = :contextinstanceid"; 292 $userlist->add_from_sql('loggeduser', $sql, $params); 293 294 $sql = "SELECT userid 295 FROM {scale} 296 WHERE courseid = :contextinstanceid"; 297 $userlist->add_from_sql('userid', $sql, $params); 298 299 $sql = "SELECT loggeduser, userid 300 FROM {scale_history} 301 WHERE courseid = :contextinstanceid"; 302 $userlist->add_from_sql('loggeduser', $sql, $params); 303 $userlist->add_from_sql('userid', $sql, $params); 304 305 $sql = "SELECT loggeduser 306 FROM {grade_items_history} 307 WHERE courseid = :contextinstanceid"; 308 $userlist->add_from_sql('loggeduser', $sql, $params); 309 310 $sql = "SELECT ggh.userid 311 FROM {grade_grades_history} ggh 312 JOIN {grade_items} gi ON ggh.itemid = gi.id 313 WHERE gi.courseid = :contextinstanceid"; 314 $userlist->add_from_sql('userid', $sql, $params); 315 316 $sql = "SELECT gg.userid, gg.usermodified 317 FROM {grade_grades} gg 318 JOIN {grade_items} gi ON gg.itemid = gi.id 319 WHERE gi.courseid = :contextinstanceid"; 320 $userlist->add_from_sql('userid', $sql, $params); 321 $userlist->add_from_sql('usermodified', $sql, $params); 322 323 $sql = "SELECT loggeduser 324 FROM {grade_categories_history} 325 WHERE courseid = :contextinstanceid"; 326 $userlist->add_from_sql('loggeduser', $sql, $params); 327 } 328 329 // None of these are currently used (user deletion). 330 if ($context->contextlevel == CONTEXT_SYSTEM) { 331 $params = ['contextinstanceid' => 0]; 332 333 $sql = "SELECT usermodified 334 FROM {grade_outcomes} 335 WHERE (courseid IS NULL OR courseid < 1)"; 336 $userlist->add_from_sql('usermodified', $sql, []); 337 338 $sql = "SELECT loggeduser 339 FROM {grade_outcomes_history} 340 WHERE (courseid IS NULL OR courseid < 1)"; 341 $userlist->add_from_sql('loggeduser', $sql, []); 342 343 $sql = "SELECT userid 344 FROM {scale} 345 WHERE courseid = :contextinstanceid"; 346 $userlist->add_from_sql('userid', $sql, $params); 347 348 $sql = "SELECT loggeduser, userid 349 FROM {scale_history} 350 WHERE courseid = :contextinstanceid"; 351 $userlist->add_from_sql('loggeduser', $sql, $params); 352 $userlist->add_from_sql('userid', $sql, $params); 353 } 354 355 if ($context->contextlevel == CONTEXT_USER) { 356 // If the grade item has been removed and we have an orphan entry then we link to the 357 // user context. 358 $sql = "SELECT ggh.userid 359 FROM {grade_grades_history} ggh 360 LEFT JOIN {grade_items} gi ON ggh.itemid = gi.id 361 WHERE gi.id IS NULL 362 AND ggh.userid = :contextinstanceid"; 363 $userlist->add_from_sql('userid', $sql, ['contextinstanceid' => $context->instanceid]); 364 } 365 } 366 367 /** 368 * Export all user data for the specified user, in the specified contexts. 369 * 370 * @param approved_contextlist $contextlist The approved contexts to export information for. 371 */ 372 public static function export_user_data(approved_contextlist $contextlist) { 373 global $DB; 374 375 $user = $contextlist->get_user(); 376 $userid = $user->id; 377 $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) { 378 if ($context->contextlevel == CONTEXT_COURSE) { 379 $carry[$context->contextlevel][] = $context; 380 381 } else if ($context->contextlevel == CONTEXT_USER) { 382 $carry[$context->contextlevel][] = $context; 383 384 } 385 386 return $carry; 387 }, [ 388 CONTEXT_USER => [], 389 CONTEXT_COURSE => [] 390 ]); 391 392 $rootpath = [get_string('grades', 'core_grades')]; 393 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]); 394 395 // Export the outcomes. 396 static::export_user_data_outcomes_in_contexts($contextlist); 397 398 // Export the scales. 399 static::export_user_data_scales_in_contexts($contextlist); 400 401 // Export the historical grades which have become orphans (their grade items were deleted). 402 // We place those in ther user context of the graded user. 403 $userids = array_values(array_map(function($context) { 404 return $context->instanceid; 405 }, $contexts[CONTEXT_USER])); 406 if (!empty($userids)) { 407 408 // Export own historical grades and related ones. 409 list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 410 list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 411 list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 412 $usercontext = $contexts[CONTEXT_USER]; 413 $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_'); 414 $sql = " 415 SELECT $gghfields, ctx.id as ctxid 416 FROM {grade_grades_history} ggh 417 JOIN {context} ctx 418 ON ctx.instanceid = ggh.userid 419 AND ctx.contextlevel = :userlevel 420 LEFT JOIN {grade_items} gi 421 ON gi.id = ggh.itemid 422 WHERE gi.id IS NULL 423 AND (ggh.userid $inuseridsql 424 OR ggh.usermodified $inusermodifiedsql 425 OR ggh.loggeduser $inloggedusersql) 426 AND (ggh.userid = :userid1 427 OR ggh.usermodified = :userid2 428 OR ggh.loggeduser = :userid3) 429 ORDER BY ggh.userid, ggh.timemodified, ggh.id"; 430 $params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams, 431 ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER]); 432 433 $deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'); 434 $recordset = $DB->get_recordset_sql($sql, $params); 435 static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) { 436 $context = context::instance_by_id($record->ctxid); 437 $gghrecord = static::extract_record($record, 'ggh_'); 438 439 // Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades. 440 $carry[] = [ 441 'name' => $deletedstr, 442 'graded_user_was_you' => transform::yesno($userid == $gghrecord->userid), 443 'grade' => $gghrecord->finalgrade, 444 'feedback' => format_text($gghrecord->feedback, $gghrecord->feedbackformat, ['context' => $context]), 445 'information' => format_text($gghrecord->information, $gghrecord->informationformat, ['context' => $context]), 446 'timemodified' => transform::datetime($gghrecord->timemodified), 447 'logged_in_user_was_you' => transform::yesno($userid == $gghrecord->loggeduser), 448 'author_of_change_was_you' => transform::yesno($userid == $gghrecord->usermodified), 449 'action' => static::transform_history_action($gghrecord->action) 450 ]; 451 452 return $carry; 453 454 }, function($ctxid, $data) use ($rootpath) { 455 $context = context::instance_by_id($ctxid); 456 writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]); 457 }); 458 } 459 460 // Find out the course IDs. 461 $courseids = array_values(array_map(function($context) { 462 return $context->instanceid; 463 }, $contexts[CONTEXT_COURSE])); 464 if (empty($courseids)) { 465 return; 466 } 467 list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); 468 469 // Ensure that the grades are final and do not need regrading. 470 array_walk($courseids, function($courseid) { 471 grade_regrade_final_grades($courseid); 472 }); 473 474 // Export own grades. 475 $ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_'); 476 $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_'); 477 $scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_'); 478 $sql = " 479 SELECT $ggfields, $gifields, $scalefields 480 FROM {grade_grades} gg 481 JOIN {grade_items} gi 482 ON gi.id = gg.itemid 483 LEFT JOIN {scale} sc 484 ON sc.id = gi.scaleid 485 WHERE gi.courseid $incoursesql 486 AND gg.userid = :userid 487 ORDER BY gi.courseid, gi.id, gg.id"; 488 $params = array_merge($incourseparams, ['userid' => $userid]); 489 490 $recordset = $DB->get_recordset_sql($sql, $params); 491 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) { 492 $context = context_course::instance($record->gi_courseid); 493 $gg = static::extract_grade_grade_from_record($record); 494 $carry[] = static::transform_grade($gg, $context, false); 495 496 return $carry; 497 498 }, function($courseid, $data) use ($rootpath) { 499 $context = context_course::instance($courseid); 500 501 $pathtofiles = [ 502 get_string('grades', 'core_grades'), 503 get_string('feedbackfiles', 'core_grades') 504 ]; 505 foreach ($data as $key => $grades) { 506 $gg = $grades['gradeobject']; 507 writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT, 508 GRADE_FEEDBACK_FILEAREA, $gg->id); 509 unset($data[$key]['gradeobject']); // Do not want to export this later. 510 } 511 512 writer::with_context($context)->export_data($rootpath, (object) ['grades' => $data]); 513 }); 514 515 // Export own historical grades in courses. 516 $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_'); 517 $sql = " 518 SELECT $gghfields, $gifields, $scalefields 519 FROM {grade_grades_history} ggh 520 JOIN {grade_items} gi 521 ON gi.id = ggh.itemid 522 LEFT JOIN {scale} sc 523 ON sc.id = gi.scaleid 524 WHERE gi.courseid $incoursesql 525 AND ggh.userid = :userid 526 ORDER BY gi.courseid, ggh.timemodified, ggh.id"; 527 $params = array_merge($incourseparams, ['userid' => $userid]); 528 529 $recordset = $DB->get_recordset_sql($sql, $params); 530 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) { 531 $context = context_course::instance($record->gi_courseid); 532 $gg = static::extract_grade_grade_from_record($record, true); 533 $carry[] = array_merge(static::transform_grade($gg, $context, true), [ 534 'action' => static::transform_history_action($record->ggh_action) 535 ]); 536 return $carry; 537 538 }, function($courseid, $data) use ($rootpath) { 539 $context = context_course::instance($courseid); 540 541 $pathtofiles = [ 542 get_string('grades', 'core_grades'), 543 get_string('feedbackhistoryfiles', 'core_grades') 544 ]; 545 foreach ($data as $key => $grades) { 546 $gg = $grades['gradeobject']; 547 writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT, 548 GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid); 549 unset($data[$key]['gradeobject']); // Do not want to export this later. 550 } 551 552 writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]); 553 }); 554 555 // Export edits of categories history. 556 $sql = " 557 SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action 558 FROM {grade_categories_history} gch 559 WHERE gch.courseid $incoursesql 560 AND gch.loggeduser = :userid 561 ORDER BY gch.courseid, gch.timemodified, gch.id"; 562 $params = array_merge($incourseparams, ['userid' => $userid]); 563 $recordset = $DB->get_recordset_sql($sql, $params); 564 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { 565 $carry[] = [ 566 'name' => $record->fullname, 567 'timemodified' => transform::datetime($record->timemodified), 568 'logged_in_user_was_you' => transform::yesno(true), 569 'action' => static::transform_history_action($record->action), 570 ]; 571 return $carry; 572 573 }, function($courseid, $data) use ($relatedtomepath) { 574 $context = context_course::instance($courseid); 575 writer::with_context($context)->export_related_data($relatedtomepath, 'categories_history', 576 (object) ['modified_records' => $data]); 577 }); 578 579 // Export edits of items history. 580 $sql = " 581 SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action 582 FROM {grade_items_history} gih 583 WHERE gih.courseid $incoursesql 584 AND gih.loggeduser = :userid 585 ORDER BY gih.courseid, gih.timemodified, gih.id"; 586 $params = array_merge($incourseparams, ['userid' => $userid]); 587 $recordset = $DB->get_recordset_sql($sql, $params); 588 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { 589 $carry[] = [ 590 'name' => $record->itemname, 591 'module' => $record->itemmodule, 592 'info' => $record->iteminfo, 593 'timemodified' => transform::datetime($record->timemodified), 594 'logged_in_user_was_you' => transform::yesno(true), 595 'action' => static::transform_history_action($record->action), 596 ]; 597 return $carry; 598 599 }, function($courseid, $data) use ($relatedtomepath) { 600 $context = context_course::instance($courseid); 601 writer::with_context($context)->export_related_data($relatedtomepath, 'items_history', 602 (object) ['modified_records' => $data]); 603 }); 604 605 // Export edits of grades in course. 606 $sql = " 607 SELECT $ggfields, $gifields, $scalefields 608 FROM {grade_grades} gg 609 JOIN {grade_items} gi 610 ON gg.itemid = gi.id 611 LEFT JOIN {scale} sc 612 ON sc.id = gi.scaleid 613 WHERE gi.courseid $incoursesql 614 AND gg.userid <> :userid1 -- Our grades have already been exported. 615 AND gg.usermodified = :userid2 616 ORDER BY gi.courseid, gg.timemodified, gg.id"; 617 $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]); 618 $recordset = $DB->get_recordset_sql($sql, $params); 619 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) { 620 $context = context_course::instance($record->gi_courseid); 621 $gg = static::extract_grade_grade_from_record($record); 622 $carry[] = array_merge(static::transform_grade($gg, $context, false), [ 623 'userid' => transform::user($gg->userid), 624 'created_or_modified_by_you' => transform::yesno(true), 625 ]); 626 return $carry; 627 628 }, function($courseid, $data) use ($relatedtomepath) { 629 $context = context_course::instance($courseid); 630 631 $pathtofiles = [ 632 get_string('grades', 'core_grades'), 633 get_string('feedbackfiles', 'core_grades') 634 ]; 635 foreach ($data as $key => $grades) { 636 $gg = $grades['gradeobject']; 637 writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT, 638 GRADE_FEEDBACK_FILEAREA, $gg->id); 639 unset($data[$key]['gradeobject']); // Do not want to export this later. 640 } 641 642 writer::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]); 643 }); 644 645 // Export edits of grades history in course. 646 $sql = " 647 SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser 648 FROM {grade_grades_history} ggh 649 JOIN {grade_items} gi 650 ON ggh.itemid = gi.id 651 LEFT JOIN {scale} sc 652 ON sc.id = gi.scaleid 653 WHERE gi.courseid $incoursesql 654 AND ggh.userid <> :userid1 -- We've already exported our history. 655 AND (ggh.loggeduser = :userid2 656 OR ggh.usermodified = :userid3) 657 ORDER BY gi.courseid, ggh.timemodified, ggh.id"; 658 $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]); 659 $recordset = $DB->get_recordset_sql($sql, $params); 660 static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) { 661 $context = context_course::instance($record->gi_courseid); 662 $gg = static::extract_grade_grade_from_record($record, true); 663 $carry[] = array_merge(static::transform_grade($gg, $context, true), [ 664 'userid' => transform::user($gg->userid), 665 'logged_in_user_was_you' => transform::yesno($userid == $record->loggeduser), 666 'author_of_change_was_you' => transform::yesno($userid == $gg->usermodified), 667 'action' => static::transform_history_action($record->ggh_action), 668 ]); 669 return $carry; 670 671 }, function($courseid, $data) use ($relatedtomepath) { 672 $context = context_course::instance($courseid); 673 674 $pathtofiles = [ 675 get_string('grades', 'core_grades'), 676 get_string('feedbackhistoryfiles', 'core_grades') 677 ]; 678 foreach ($data as $key => $grades) { 679 $gg = $grades['gradeobject']; 680 writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT, 681 GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid); 682 unset($data[$key]['gradeobject']); // Do not want to export this later. 683 } 684 685 writer::with_context($context)->export_related_data($relatedtomepath, 'grades_history', 686 (object) ['modified_records' => $data]); 687 }); 688 } 689 690 /** 691 * Delete all data for all users in the specified context. 692 * 693 * @param context $context The specific context to delete data for. 694 */ 695 public static function delete_data_for_all_users_in_context(context $context) { 696 global $DB; 697 698 switch ($context->contextlevel) { 699 case CONTEXT_USER: 700 // The user context is only reported when there are orphan historical grades, so we only delete those. 701 static::delete_orphan_historical_grades($context->instanceid); 702 break; 703 704 case CONTEXT_COURSE: 705 // We must not change the structure of the course, so we only delete user content. 706 $itemids = static::get_item_ids_from_course_ids([$context->instanceid]); 707 if (empty($itemids)) { 708 return; 709 } 710 711 self::delete_files($itemids, true); 712 self::delete_files($itemids, false); 713 714 list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 715 $DB->delete_records_select('grade_grades', "itemid $insql", $inparams); 716 $DB->delete_records_select('grade_grades_history', "itemid $insql", $inparams); 717 break; 718 } 719 720 } 721 722 /** 723 * Delete all user data for the specified user, in the specified contexts. 724 * 725 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. 726 */ 727 public static function delete_data_for_user(approved_contextlist $contextlist) { 728 global $DB; 729 $userid = $contextlist->get_user()->id; 730 731 $courseids = []; 732 foreach ($contextlist->get_contexts() as $context) { 733 if ($context->contextlevel == CONTEXT_USER && $userid == $context->instanceid) { 734 // User attempts to delete data in their own context. 735 static::delete_orphan_historical_grades($userid); 736 737 } else if ($context->contextlevel == CONTEXT_COURSE) { 738 // Log the list of course IDs. 739 $courseids[] = $context->instanceid; 740 } 741 } 742 743 $itemids = static::get_item_ids_from_course_ids($courseids); 744 if (empty($itemids)) { 745 // Our job here is done! 746 return; 747 } 748 749 // Delete all the files. 750 self::delete_files($itemids, true, [$userid]); 751 self::delete_files($itemids, false, [$userid]); 752 753 // Delete all the grades. 754 list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 755 $params = array_merge($inparams, ['userid' => $userid]); 756 757 $DB->delete_records_select('grade_grades', "itemid $insql AND userid = :userid", $params); 758 $DB->delete_records_select('grade_grades_history', "itemid $insql AND userid = :userid", $params); 759 } 760 761 762 /** 763 * Delete multiple users within a single context. 764 * 765 * @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to 766 * delete information for. 767 */ 768 public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) { 769 global $DB; 770 771 $context = $userlist->get_context(); 772 $userids = $userlist->get_userids(); 773 if ($context->contextlevel == CONTEXT_USER) { 774 if (array_search($context->instanceid, $userids) !== false) { 775 static::delete_orphan_historical_grades($context->instanceid); 776 } 777 return; 778 } 779 780 if ($context->contextlevel != CONTEXT_COURSE) { 781 return; 782 } 783 784 $itemids = static::get_item_ids_from_course_ids([$context->instanceid]); 785 if (empty($itemids)) { 786 // Our job here is done! 787 return; 788 } 789 790 // Delete all the files. 791 self::delete_files($itemids, true, $userids); 792 self::delete_files($itemids, false, $userids); 793 794 // Delete all the grades. 795 list($itemsql, $itemparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 796 list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 797 $params = array_merge($itemparams, $userparams); 798 799 $DB->delete_records_select('grade_grades', "itemid $itemsql AND userid $usersql", $params); 800 $DB->delete_records_select('grade_grades_history', "itemid $itemsql AND userid $usersql", $params); 801 } 802 803 /** 804 * Delete orphan historical grades. 805 * 806 * @param int $userid The user ID. 807 * @return void 808 */ 809 protected static function delete_orphan_historical_grades($userid) { 810 global $DB; 811 $sql = " 812 SELECT ggh.id 813 FROM {grade_grades_history} ggh 814 LEFT JOIN {grade_items} gi 815 ON ggh.itemid = gi.id 816 WHERE gi.id IS NULL 817 AND ggh.userid = :userid"; 818 $ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]); 819 if (empty($ids)) { 820 return; 821 } 822 list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); 823 824 // First, let's delete their files. 825 $sql = " 826 SELECT gi.id 827 FROM {grade_grades_history} ggh 828 JOIN {grade_items} gi 829 ON gi.id = ggh.itemid 830 WHERE ggh.userid = :userid"; 831 $params = ['userid' => $userid]; 832 $gradeitems = $DB->get_records_sql($sql, $params); 833 if ($gradeitems) { 834 $itemids = array_keys($gradeitems); 835 self::delete_files($itemids, true, [$userid]); 836 } 837 838 $DB->delete_records_select('grade_grades_history', "id $insql", $inparams); 839 } 840 841 /** 842 * Export the user data related to outcomes. 843 * 844 * @param approved_contextlist $contextlist The approved contexts to export information for. 845 * @return void 846 */ 847 protected static function export_user_data_outcomes_in_contexts(approved_contextlist $contextlist) { 848 global $DB; 849 850 $rootpath = [get_string('grades', 'core_grades')]; 851 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]); 852 $userid = $contextlist->get_user()->id; 853 854 // Reorganise the contexts. 855 $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) { 856 if ($context->contextlevel == CONTEXT_SYSTEM) { 857 $carry['in_system'] = true; 858 } else if ($context->contextlevel == CONTEXT_COURSE) { 859 $carry['courseids'][] = $context->instanceid; 860 } 861 return $carry; 862 }, [ 863 'in_system' => false, 864 'courseids' => [] 865 ]); 866 867 // Construct SQL. 868 $sqltemplateparts = []; 869 $templateparams = []; 870 if ($reduced['in_system']) { 871 $sqltemplateparts[] = '{prefix}.courseid IS NULL'; 872 } 873 if (!empty($reduced['courseids'])) { 874 list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED); 875 $sqltemplateparts[] = "{prefix}.courseid $insql"; 876 $templateparams = array_merge($templateparams, $inparams); 877 } 878 if (empty($sqltemplateparts)) { 879 return; 880 } 881 $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')'; 882 883 // Export edited outcomes. 884 $sqlwhere = str_replace('{prefix}', 'go', $sqltemplate); 885 $sql = " 886 SELECT go.id, COALESCE(go.courseid, 0) AS courseid, go.shortname, go.fullname, go.timemodified 887 FROM {grade_outcomes} go 888 WHERE $sqlwhere 889 AND go.usermodified = :userid 890 ORDER BY go.courseid, go.timemodified, go.id"; 891 $params = array_merge($templateparams, ['userid' => $userid]); 892 $recordset = $DB->get_recordset_sql($sql, $params); 893 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { 894 $carry[] = [ 895 'shortname' => $record->shortname, 896 'fullname' => $record->fullname, 897 'timemodified' => transform::datetime($record->timemodified), 898 'created_or_modified_by_you' => transform::yesno(true) 899 ]; 900 return $carry; 901 902 }, function($courseid, $data) use ($relatedtomepath) { 903 $context = $courseid ? context_course::instance($courseid) : context_system::instance(); 904 writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes', 905 (object) ['outcomes' => $data]); 906 }); 907 908 // Export edits of outcomes history. 909 $sqlwhere = str_replace('{prefix}', 'goh', $sqltemplate); 910 $sql = " 911 SELECT goh.id, COALESCE(goh.courseid, 0) AS courseid, goh.shortname, goh.fullname, goh.timemodified, goh.action 912 FROM {grade_outcomes_history} goh 913 WHERE $sqlwhere 914 AND goh.loggeduser = :userid 915 ORDER BY goh.courseid, goh.timemodified, goh.id"; 916 $params = array_merge($templateparams, ['userid' => $userid]); 917 $recordset = $DB->get_recordset_sql($sql, $params); 918 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { 919 $carry[] = [ 920 'shortname' => $record->shortname, 921 'fullname' => $record->fullname, 922 'timemodified' => transform::datetime($record->timemodified), 923 'logged_in_user_was_you' => transform::yesno(true), 924 'action' => static::transform_history_action($record->action) 925 ]; 926 return $carry; 927 928 }, function($courseid, $data) use ($relatedtomepath) { 929 $context = $courseid ? context_course::instance($courseid) : context_system::instance(); 930 writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes_history', 931 (object) ['modified_records' => $data]); 932 }); 933 } 934 935 /** 936 * Export the user data related to scales. 937 * 938 * @param approved_contextlist $contextlist The approved contexts to export information for. 939 * @return void 940 */ 941 protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) { 942 global $DB; 943 944 $rootpath = [get_string('grades', 'core_grades')]; 945 $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]); 946 $userid = $contextlist->get_user()->id; 947 948 // Reorganise the contexts. 949 $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) { 950 if ($context->contextlevel == CONTEXT_SYSTEM) { 951 $carry['in_system'] = true; 952 } else if ($context->contextlevel == CONTEXT_COURSE) { 953 $carry['courseids'][] = $context->instanceid; 954 } 955 return $carry; 956 }, [ 957 'in_system' => false, 958 'courseids' => [] 959 ]); 960 961 // Construct SQL. 962 $sqltemplateparts = []; 963 $templateparams = []; 964 if ($reduced['in_system']) { 965 $sqltemplateparts[] = '{prefix}.courseid = 0'; 966 } 967 if (!empty($reduced['courseids'])) { 968 list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED); 969 $sqltemplateparts[] = "{prefix}.courseid $insql"; 970 $templateparams = array_merge($templateparams, $inparams); 971 } 972 if (empty($sqltemplateparts)) { 973 return; 974 } 975 $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')'; 976 977 // Export edited scales. 978 $sqlwhere = str_replace('{prefix}', 's', $sqltemplate); 979 $sql = " 980 SELECT s.id, s.courseid, s.name, s.timemodified 981 FROM {scale} s 982 WHERE $sqlwhere 983 AND s.userid = :userid 984 ORDER BY s.courseid, s.timemodified, s.id"; 985 $params = array_merge($templateparams, ['userid' => $userid]); 986 $recordset = $DB->get_recordset_sql($sql, $params); 987 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { 988 $carry[] = [ 989 'name' => $record->name, 990 'timemodified' => transform::datetime($record->timemodified), 991 'created_or_modified_by_you' => transform::yesno(true) 992 ]; 993 return $carry; 994 995 }, function($courseid, $data) use ($relatedtomepath) { 996 $context = $courseid ? context_course::instance($courseid) : context_system::instance(); 997 writer::with_context($context)->export_related_data($relatedtomepath, 'scales', 998 (object) ['scales' => $data]); 999 }); 1000 1001 // Export edits of scales history. 1002 $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate); 1003 $sql = " 1004 SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser 1005 FROM {scale_history} sh 1006 WHERE $sqlwhere 1007 AND sh.loggeduser = :userid1 1008 OR sh.userid = :userid2 1009 ORDER BY sh.courseid, sh.timemodified, sh.id"; 1010 $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]); 1011 $recordset = $DB->get_recordset_sql($sql, $params); 1012 static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) { 1013 $carry[] = [ 1014 'name' => $record->name, 1015 'timemodified' => transform::datetime($record->timemodified), 1016 'author_of_change_was_you' => transform::yesno($record->userid == $userid), 1017 'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid), 1018 'action' => static::transform_history_action($record->action) 1019 ]; 1020 return $carry; 1021 1022 }, function($courseid, $data) use ($relatedtomepath) { 1023 $context = $courseid ? context_course::instance($courseid) : context_system::instance(); 1024 writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history', 1025 (object) ['modified_records' => $data]); 1026 }); 1027 } 1028 1029 /** 1030 * Extract grade_grade from a record. 1031 * 1032 * @param stdClass $record The record. 1033 * @param bool $ishistory Whether we're extracting a historical grade. 1034 * @return grade_grade 1035 */ 1036 protected static function extract_grade_grade_from_record(stdClass $record, $ishistory = false) { 1037 $prefix = $ishistory ? 'ggh_' : 'gg_'; 1038 $ggrecord = static::extract_record($record, $prefix); 1039 if ($ishistory) { 1040 // The grade history is not a real grade_grade so we remove the ID. 1041 $historyid = $ggrecord->id; 1042 unset($ggrecord->id); 1043 } 1044 $gg = new grade_grade($ggrecord, false); 1045 1046 // There is a grade item in the record. 1047 if (!empty($record->gi_id)) { 1048 $gi = new grade_item(static::extract_record($record, 'gi_'), false); 1049 $gg->grade_item = $gi; // This is a common hack throughout the grades API. 1050 } 1051 1052 // Load the scale, when it still exists. 1053 if (!empty($gi->scaleid) && !empty($record->sc_id)) { 1054 $scalerec = static::extract_record($record, 'sc_'); 1055 $gi->scale = new grade_scale($scalerec, false); 1056 $gi->scale->load_items(); 1057 } 1058 1059 if ($ishistory) { 1060 $gg->historyid = $historyid; 1061 } 1062 1063 return $gg; 1064 } 1065 1066 /** 1067 * Extract a record from another one. 1068 * 1069 * @param object $record The record to extract from. 1070 * @param string $prefix The prefix used. 1071 * @return object 1072 */ 1073 protected static function extract_record($record, $prefix) { 1074 $result = []; 1075 $prefixlength = strlen($prefix); 1076 foreach ($record as $key => $value) { 1077 if (strpos($key, $prefix) === 0) { 1078 $result[substr($key, $prefixlength)] = $value; 1079 } 1080 } 1081 return (object) $result; 1082 } 1083 1084 /** 1085 * Get fields SQL for a grade related object. 1086 * 1087 * @param string $target The related object. 1088 * @param string $alias The table alias. 1089 * @param string $prefix A prefix. 1090 * @return string 1091 */ 1092 protected static function get_fields_sql($target, $alias, $prefix) { 1093 switch ($target) { 1094 case 'grade_category': 1095 case 'grade_grade': 1096 case 'grade_item': 1097 case 'grade_outcome': 1098 case 'grade_scale': 1099 $obj = new $target([], false); 1100 $fields = array_merge(array_keys($obj->optional_fields), $obj->required_fields); 1101 break; 1102 1103 case 'grade_grades_history': 1104 $fields = ['id', 'action', 'oldid', 'source', 'timemodified', 'loggeduser', 'itemid', 'userid', 'rawgrade', 1105 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 1106 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat']; 1107 break; 1108 1109 default: 1110 throw new \coding_exception('Unrecognised target: ' . $target); 1111 break; 1112 } 1113 1114 return implode(', ', array_map(function($field) use ($alias, $prefix) { 1115 return "{$alias}.{$field} AS {$prefix}{$field}"; 1116 }, $fields)); 1117 } 1118 1119 /** 1120 * Get all the items IDs from course IDs. 1121 * 1122 * @param array $courseids The course IDs. 1123 * @return array 1124 */ 1125 protected static function get_item_ids_from_course_ids($courseids) { 1126 global $DB; 1127 if (empty($courseids)) { 1128 return []; 1129 } 1130 list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); 1131 return $DB->get_fieldset_select('grade_items', 'id', "courseid $insql", $inparams); 1132 } 1133 1134 /** 1135 * Loop and export from a recordset. 1136 * 1137 * @param moodle_recordset $recordset The recordset. 1138 * @param string $splitkey The record key to determine when to export. 1139 * @param mixed $initial The initial data to reduce from. 1140 * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. 1141 * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. 1142 * @return void 1143 */ 1144 protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, 1145 callable $reducer, callable $export) { 1146 1147 $data = $initial; 1148 $lastid = null; 1149 1150 foreach ($recordset as $record) { 1151 if ($lastid !== null && $record->{$splitkey} != $lastid) { 1152 $export($lastid, $data); 1153 $data = $initial; 1154 } 1155 $data = $reducer($data, $record); 1156 $lastid = $record->{$splitkey}; 1157 } 1158 $recordset->close(); 1159 1160 if ($lastid !== null) { 1161 $export($lastid, $data); 1162 } 1163 } 1164 1165 /** 1166 * Transform an history action. 1167 * 1168 * @param int $action The action. 1169 * @return string 1170 */ 1171 protected static function transform_history_action($action) { 1172 switch ($action) { 1173 case GRADE_HISTORY_INSERT: 1174 return get_string('privacy:request:historyactioninsert', 'core_grades'); 1175 break; 1176 case GRADE_HISTORY_UPDATE: 1177 return get_string('privacy:request:historyactionupdate', 'core_grades'); 1178 break; 1179 case GRADE_HISTORY_DELETE: 1180 return get_string('privacy:request:historyactiondelete', 'core_grades'); 1181 break; 1182 } 1183 1184 return '?'; 1185 } 1186 1187 /** 1188 * Transform a grade. 1189 * 1190 * @param grade_grade $gg The grade object. 1191 * @param context $context The context. 1192 * @param bool $ishistory Whether we're extracting a historical grade. 1193 * @return array 1194 */ 1195 protected static function transform_grade(grade_grade $gg, context $context, bool $ishistory) { 1196 $gi = $gg->load_grade_item(); 1197 $timemodified = $gg->timemodified ? transform::datetime($gg->timemodified) : null; 1198 $timecreated = $gg->timecreated ? transform::datetime($gg->timecreated) : $timemodified; // When null we use timemodified. 1199 1200 $filearea = $ishistory ? GRADE_HISTORY_FEEDBACK_FILEAREA : GRADE_FEEDBACK_FILEAREA; 1201 $itemid = $ishistory ? $gg->historyid : $gg->id; 1202 $subpath = $ishistory ? get_string('feedbackhistoryfiles', 'core_grades') : get_string('feedbackfiles', 'core_grades'); 1203 1204 $pathtofiles = [ 1205 get_string('grades', 'core_grades'), 1206 $subpath 1207 ]; 1208 $gg->feedback = writer::with_context($gg->get_context())->rewrite_pluginfile_urls( 1209 $pathtofiles, 1210 GRADE_FILE_COMPONENT, 1211 $filearea, 1212 $itemid, 1213 $gg->feedback 1214 ); 1215 1216 return [ 1217 'gradeobject' => $gg, 1218 'item' => $gi->get_name(), 1219 'grade' => $gg->finalgrade, 1220 'grade_formatted' => grade_format_gradevalue($gg->finalgrade, $gi), 1221 'feedback' => format_text($gg->feedback, $gg->feedbackformat, ['context' => $context]), 1222 'information' => format_text($gg->information, $gg->informationformat, ['context' => $context]), 1223 'timecreated' => $timecreated, 1224 'timemodified' => $timemodified, 1225 ]; 1226 } 1227 1228 /** 1229 * Handles deleting files for a given list of grade items. 1230 * 1231 * If an array of userids if given then it handles deleting files for those users. 1232 * 1233 * @param array $itemids 1234 * @param bool $ishistory 1235 * @param array|null $userids 1236 * @throws \coding_exception 1237 * @throws \dml_exception 1238 */ 1239 protected static function delete_files(array $itemids, bool $ishistory, array $userids = null) { 1240 global $DB; 1241 1242 list($iteminnsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 1243 if (!is_null($userids)) { 1244 list($userinnsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 1245 $params = array_merge($params, $userparams); 1246 } 1247 1248 if ($ishistory) { 1249 $gradefields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_'); 1250 $gradetable = 'grade_grades_history'; 1251 $tableprefix = 'ggh'; 1252 $filearea = GRADE_HISTORY_FEEDBACK_FILEAREA; 1253 } else { 1254 $gradefields = static::get_fields_sql('grade_grade', 'gg', 'gg_'); 1255 $gradetable = 'grade_grades'; 1256 $tableprefix = 'gg'; 1257 $filearea = GRADE_FEEDBACK_FILEAREA; 1258 } 1259 1260 $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_'); 1261 1262 $fs = new \file_storage(); 1263 $sql = "SELECT $gradefields, $gifields 1264 FROM {{$gradetable}} $tableprefix 1265 JOIN {grade_items} gi 1266 ON gi.id = {$tableprefix}.itemid 1267 WHERE gi.id $iteminnsql "; 1268 if (!is_null($userids)) { 1269 $sql .= "AND {$tableprefix}.userid $userinnsql"; 1270 } 1271 1272 $grades = $DB->get_recordset_sql($sql, $params); 1273 foreach ($grades as $grade) { 1274 $gg = static::extract_grade_grade_from_record($grade, $ishistory); 1275 $fileitemid = ($ishistory) ? $gg->historyid : $gg->id; 1276 $fs->delete_area_files($gg->get_context()->id, GRADE_FILE_COMPONENT, $filearea, $fileitemid); 1277 } 1278 $grades->close(); 1279 } 1280 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body