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