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 * Defines {@link \mod_workshop\privacy\provider} class. 19 * 20 * @package mod_workshop 21 * @category privacy 22 * @copyright 2018 David Mudrák <david@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_workshop\privacy; 27 28 use core_privacy\local\metadata\collection; 29 use core_privacy\local\request\approved_contextlist; 30 use core_privacy\local\request\approved_userlist; 31 use core_privacy\local\request\contextlist; 32 use core_privacy\local\request\deletion_criteria; 33 use core_privacy\local\request\helper; 34 use core_privacy\local\request\transform; 35 use core_privacy\local\request\userlist; 36 use core_privacy\local\request\writer; 37 38 defined('MOODLE_INTERNAL') || die(); 39 40 require_once($CFG->dirroot.'/mod/workshop/locallib.php'); 41 42 /** 43 * Privacy API implementation for the Workshop activity module. 44 * 45 * @copyright 2018 David Mudrák <david@moodle.com> 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 class provider implements 49 \core_privacy\local\metadata\provider, 50 \core_privacy\local\request\core_userlist_provider, 51 \core_privacy\local\request\user_preference_provider, 52 \core_privacy\local\request\plugin\provider { 53 54 /** 55 * Describe all the places where the Workshop module stores some personal data. 56 * 57 * @param collection $collection Collection of items to add metadata to. 58 * @return collection Collection with our added items. 59 */ 60 public static function get_metadata(collection $collection) : collection { 61 62 $collection->add_database_table('workshop_submissions', [ 63 'workshopid' => 'privacy:metadata:workshopid', 64 'authorid' => 'privacy:metadata:authorid', 65 'example' => 'privacy:metadata:example', 66 'timecreated' => 'privacy:metadata:timecreated', 67 'timemodified' => 'privacy:metadata:timemodified', 68 'title' => 'privacy:metadata:submissiontitle', 69 'content' => 'privacy:metadata:submissioncontent', 70 'contentformat' => 'privacy:metadata:submissioncontentformat', 71 'grade' => 'privacy:metadata:submissiongrade', 72 'gradeover' => 'privacy:metadata:submissiongradeover', 73 'feedbackauthor' => 'privacy:metadata:feedbackauthor', 74 'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat', 75 'published' => 'privacy:metadata:published', 76 'late' => 'privacy:metadata:late', 77 ], 'privacy:metadata:workshopsubmissions'); 78 79 $collection->add_database_table('workshop_assessments', [ 80 'submissionid' => 'privacy:metadata:submissionid', 81 'reviewerid' => 'privacy:metadata:reviewerid', 82 'weight' => 'privacy:metadata:weight', 83 'timecreated' => 'privacy:metadata:timecreated', 84 'timemodified' => 'privacy:metadata:timemodified', 85 'grade' => 'privacy:metadata:assessmentgrade', 86 'gradinggrade' => 'privacy:metadata:assessmentgradinggrade', 87 'gradinggradeover' => 'privacy:metadata:assessmentgradinggradeover', 88 'feedbackauthor' => 'privacy:metadata:feedbackauthor', 89 'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat', 90 'feedbackreviewer' => 'privacy:metadata:feedbackreviewer', 91 'feedbackreviewerformat' => 'privacy:metadata:feedbackreviewerformat', 92 ], 'privacy:metadata:workshopassessments'); 93 94 $collection->add_database_table('workshop_grades', [ 95 'assessmentid' => 'privacy:metadata:assessmentid', 96 'strategy' => 'privacy:metadata:strategy', 97 'dimensionid' => 'privacy:metadata:dimensionid', 98 'grade' => 'privacy:metadata:dimensiongrade', 99 'peercomment' => 'privacy:metadata:peercomment', 100 'peercommentformat' => 'privacy:metadata:peercommentformat', 101 ], 'privacy:metadata:workshopgrades'); 102 103 $collection->add_database_table('workshop_aggregations', [ 104 'workshopid' => 'privacy:metadata:workshopid', 105 'userid' => 'privacy:metadata:userid', 106 'gradinggrade' => 'privacy:metadata:aggregatedgradinggrade', 107 'timegraded' => 'privacy:metadata:timeaggregated', 108 ], 'privacy:metadata:workshopaggregations'); 109 110 $collection->add_subsystem_link('core_files', [], 'privacy:metadata:subsystem:corefiles'); 111 $collection->add_subsystem_link('core_plagiarism', [], 'privacy:metadata:subsystem:coreplagiarism'); 112 113 $userprefs = self::get_user_prefs(); 114 foreach ($userprefs as $userpref) { 115 if ($userpref === 'workshop_perpage') { 116 $collection->add_user_preference('workshop_perpage', 'privacy:metadata:preference:perpage'); 117 } else { 118 $summary = str_replace('workshop-', '', $userpref); 119 $collection->add_user_preference($userpref, "privacy:metadata:preference:$summary"); 120 } 121 } 122 123 return $collection; 124 } 125 126 /** 127 * Get the list of contexts that contain personal data for the specified user. 128 * 129 * User has personal data in the workshop if any of the following cases happens: 130 * 131 * - the user has submitted in the workshop 132 * - the user has overridden a submission grade 133 * - the user has been assigned as a reviewer of a submission 134 * - the user has overridden a grading grade 135 * - the user has a grading grade (existing or to be calculated) 136 * 137 * @param int $userid ID of the user. 138 * @return contextlist List of contexts containing the user's personal data. 139 */ 140 public static function get_contexts_for_userid(int $userid) : contextlist { 141 142 $contextlist = new contextlist(); 143 $sql = "SELECT ctx.id 144 FROM {course_modules} cm 145 JOIN {modules} m ON cm.module = m.id AND m.name = :module 146 JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id 147 JOIN {workshop} w ON cm.instance = w.id 148 LEFT JOIN {workshop_submissions} ws ON ws.workshopid = w.id 149 LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND ( 150 wa.reviewerid = :wareviewerid 151 OR 152 wa.gradinggradeoverby = :wagradinggradeoverby 153 ) 154 LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id AND wr.userid = :wruserid 155 WHERE ws.authorid = :wsauthorid 156 OR ws.gradeoverby = :wsgradeoverby 157 OR wa.id IS NOT NULL 158 OR wr.id IS NOT NULL"; 159 160 $params = [ 161 'module' => 'workshop', 162 'contextlevel' => CONTEXT_MODULE, 163 'wsauthorid' => $userid, 164 'wsgradeoverby' => $userid, 165 'wareviewerid' => $userid, 166 'wagradinggradeoverby' => $userid, 167 'wruserid' => $userid, 168 ]; 169 170 $contextlist->add_from_sql($sql, $params); 171 172 return $contextlist; 173 } 174 175 /** 176 * Get the list of users within a specific context. 177 * 178 * @param userlist $userlist To be filled list of users who have data in this context/plugin combination. 179 */ 180 public static function get_users_in_context(userlist $userlist) { 181 global $DB; 182 183 $context = $userlist->get_context(); 184 185 if (!$context instanceof \context_module) { 186 return; 187 } 188 189 $params = [ 190 'instanceid' => $context->instanceid, 191 'module' => 'workshop', 192 ]; 193 194 // One query to fetch them all, one query to find them, one query to bring them all and into the userlist add them. 195 $sql = "SELECT ws.authorid, ws.gradeoverby, wa.reviewerid, wa.gradinggradeoverby, wr.userid 196 FROM {course_modules} cm 197 JOIN {modules} m ON cm.module = m.id AND m.name = :module 198 JOIN {workshop} w ON cm.instance = w.id 199 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 200 LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id 201 LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id 202 WHERE cm.id = :instanceid"; 203 204 $userids = []; 205 $rs = $DB->get_recordset_sql($sql, $params); 206 207 foreach ($rs as $r) { 208 if ($r->authorid) { 209 $userids[$r->authorid] = true; 210 } 211 if ($r->gradeoverby) { 212 $userids[$r->gradeoverby] = true; 213 } 214 if ($r->reviewerid) { 215 $userids[$r->reviewerid] = true; 216 } 217 if ($r->gradinggradeoverby) { 218 $userids[$r->gradinggradeoverby] = true; 219 } 220 if ($r->userid) { 221 $userids[$r->userid] = true; 222 } 223 } 224 225 $rs->close(); 226 227 if ($userids) { 228 $userlist->add_users(array_keys($userids)); 229 } 230 } 231 232 /** 233 * Export personal data stored in the given contexts. 234 * 235 * @param approved_contextlist $contextlist List of contexts approved for export. 236 */ 237 public static function export_user_data(approved_contextlist $contextlist) { 238 global $DB; 239 240 if (!count($contextlist)) { 241 return; 242 } 243 244 $user = $contextlist->get_user(); 245 246 // Export general information about all workshops. 247 foreach ($contextlist->get_contexts() as $context) { 248 if ($context->contextlevel != CONTEXT_MODULE) { 249 continue; 250 } 251 $data = helper::get_context_data($context, $user); 252 static::append_extra_workshop_data($context, $user, $data, []); 253 writer::with_context($context)->export_data([], $data); 254 helper::export_context_files($context, $user); 255 } 256 257 // Export the user's own submission and all example submissions he/she created. 258 static::export_submissions($contextlist); 259 260 // Export all given assessments. 261 static::export_assessments($contextlist); 262 } 263 264 /** 265 * Export user preferences controlled by this plugin. 266 * 267 * @param int $userid ID of the user we are exporting data for 268 */ 269 public static function export_user_preferences(int $userid) { 270 $userprefs = self::get_user_prefs(); 271 $expandstr = get_string('expand'); 272 $collapsestr = get_string('collapse'); 273 foreach ($userprefs as $userpref) { 274 $userprefval = get_user_preferences($userpref, null, $userid); 275 if ($userprefval !== null) { 276 $langid = str_replace('workshop-', '', $userpref); 277 $description = get_string("privacy:metadata:preference:$langid", 'mod_workshop'); 278 if ($userpref === 'workshop_perpage') { 279 writer::export_user_preference('mod_workshop', $userpref, $userprefval, 280 get_string('privacy:metadata:preference:perpage', 'mod_workshop')); 281 } else { 282 writer::export_user_preference('mod_workshop', $userpref, 283 $userprefval == 1 ? $collapsestr : $expandstr, $description); 284 } 285 } 286 } 287 } 288 289 /** 290 * Append additional relevant data into the base data about the workshop instance. 291 * 292 * Relevant are data that are important for interpreting or evaluating the performance of the user expressed in 293 * his/her exported personal data. For example, we need to know what were the instructions for submissions or what 294 * was the phase of the workshop when it was exported. 295 * 296 * @param context $context Workshop module content. 297 * @param stdClass $user User for which we are exporting data. 298 * @param stdClass $data Base data about the workshop instance to append to. 299 * @param array $subcontext Subcontext path items to eventually write files into. 300 */ 301 protected static function append_extra_workshop_data(\context $context, \stdClass $user, \stdClass $data, array $subcontext) { 302 global $DB; 303 304 if ($context->contextlevel != CONTEXT_MODULE) { 305 throw new \coding_exception('Unexpected context provided'); 306 } 307 308 $sql = "SELECT w.instructauthors, w.instructauthorsformat, w.instructreviewers, w.instructreviewersformat, w.phase, 309 w.strategy, w.evaluation, w.latesubmissions, w.submissionstart, w.submissionend, w.assessmentstart, 310 w.assessmentend, w.conclusion, w.conclusionformat 311 FROM {course_modules} cm 312 JOIN {workshop} w ON cm.instance = w.id 313 WHERE cm.id = :cmid"; 314 315 $params = [ 316 'cmid' => $context->instanceid, 317 ]; 318 319 $record = $DB->get_record_sql($sql, $params, MUST_EXIST); 320 $writer = writer::with_context($context); 321 322 if ($record->phase >= \workshop::PHASE_SUBMISSION) { 323 $data->instructauthors = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructauthors', 0, 324 $record->instructauthors); 325 $data->instructauthorsformat = $record->instructauthorsformat; 326 } 327 328 if ($record->phase >= \workshop::PHASE_ASSESSMENT) { 329 $data->instructreviewers = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructreviewers', 0, 330 $record->instructreviewers); 331 $data->instructreviewersformat = $record->instructreviewersformat; 332 } 333 334 if ($record->phase >= \workshop::PHASE_CLOSED) { 335 $data->conclusion = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'conclusion', 0, $record->conclusion); 336 $data->conclusionformat = $record->conclusionformat; 337 } 338 339 $data->strategy = \workshop::available_strategies_list()[$record->strategy]; 340 $data->evaluation = \workshop::available_evaluators_list()[$record->evaluation]; 341 $data->latesubmissions = transform::yesno($record->latesubmissions); 342 $data->submissionstart = $record->submissionstart ? transform::datetime($record->submissionstart) : null; 343 $data->submissionend = $record->submissionend ? transform::datetime($record->submissionend) : null; 344 $data->assessmentstart = $record->assessmentstart ? transform::datetime($record->assessmentstart) : null; 345 $data->assessmentend = $record->assessmentend ? transform::datetime($record->assessmentend) : null; 346 347 switch ($record->phase) { 348 case \workshop::PHASE_SETUP: 349 $data->phase = get_string('phasesetup', 'mod_workshop'); 350 break; 351 case \workshop::PHASE_SUBMISSION: 352 $data->phase = get_string('phasesubmission', 'mod_workshop'); 353 break; 354 case \workshop::PHASE_ASSESSMENT: 355 $data->phase = get_string('phaseassessment', 'mod_workshop'); 356 break; 357 case \workshop::PHASE_EVALUATION: 358 $data->phase = get_string('phaseevaluation', 'mod_workshop'); 359 break; 360 case \workshop::PHASE_CLOSED: 361 $data->phase = get_string('phaseclosed', 'mod_workshop'); 362 break; 363 } 364 365 $writer->export_area_files($subcontext, 'mod_workshop', 'instructauthors', 0); 366 $writer->export_area_files($subcontext, 'mod_workshop', 'instructreviewers', 0); 367 $writer->export_area_files($subcontext, 'mod_workshop', 'conclusion', 0); 368 } 369 370 /** 371 * Export all user's submissions and example submissions he/she created in the given contexts. 372 * 373 * @param approved_contextlist $contextlist List of contexts approved for export. 374 */ 375 protected static function export_submissions(approved_contextlist $contextlist) { 376 global $DB; 377 378 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 379 $user = $contextlist->get_user(); 380 381 $sql = "SELECT ws.id, ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title, 382 ws.content, ws.contentformat, ws.grade, ws.gradeover, ws.feedbackauthor, ws.feedbackauthorformat, 383 ws.published, ws.late, 384 w.phase, w.course, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')." 385 FROM {course_modules} cm 386 JOIN {modules} m ON cm.module = m.id AND m.name = :module 387 JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id 388 JOIN {workshop} w ON cm.instance = w.id 389 JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid 390 WHERE ctx.id {$contextsql}"; 391 392 $params = $contextparams + [ 393 'module' => 'workshop', 394 'contextlevel' => CONTEXT_MODULE, 395 'authorid' => $user->id, 396 ]; 397 398 $rs = $DB->get_recordset_sql($sql, $params); 399 400 foreach ($rs as $record) { 401 \context_helper::preload_from_record($record); 402 $context = \context_module::instance($record->cmid); 403 $writer = \core_privacy\local\request\writer::with_context($context); 404 405 if ($record->example) { 406 $subcontext = [get_string('examplesubmissions', 'mod_workshop'), $record->id]; 407 $mysubmission = null; 408 } else { 409 $subcontext = [get_string('mysubmission', 'mod_workshop')]; 410 $mysubmission = $record; 411 } 412 413 $phase = $record->phase; 414 $courseid = $record->course; 415 416 $data = (object) [ 417 'example' => transform::yesno($record->example), 418 'timecreated' => transform::datetime($record->timecreated), 419 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, 420 'title' => $record->title, 421 'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 422 'submission_content', $record->id, $record->content), 423 'contentformat' => $record->contentformat, 424 'grade' => $record->grade, 425 'gradeover' => $record->gradeover, 426 'feedbackauthor' => $record->feedbackauthor, 427 'feedbackauthorformat' => $record->feedbackauthorformat, 428 'published' => transform::yesno($record->published), 429 'late' => transform::yesno($record->late), 430 ]; 431 432 $writer->export_data($subcontext, $data); 433 $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->id); 434 $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->id); 435 436 // Export peer-assessments of my submission if the workshop was closed. We do not export received 437 // assessments from peers before they were actually effective. Before the workshop is closed, grades are not 438 // pushed into the gradebook. So peer assessments did not affect evaluation of the user's performance and 439 // they should not be considered as their personal data. This is different from assessments given by the 440 // user that are always exported. 441 if ($mysubmission && $phase == \workshop::PHASE_CLOSED) { 442 $assessments = $DB->get_records('workshop_assessments', ['submissionid' => $mysubmission->id], '', 443 'id, reviewerid, weight, timecreated, timemodified, grade, feedbackauthor, feedbackauthorformat'); 444 445 foreach ($assessments as $assessment) { 446 $assid = $assessment->id; 447 $assessment->selfassessment = transform::yesno($assessment->reviewerid == $user->id); 448 $assessment->timecreated = transform::datetime($assessment->timecreated); 449 $assessment->timemodified = $assessment->timemodified ? transform::datetime($assessment->timemodified) : null; 450 $assessment->feedbackauthor = $writer->rewrite_pluginfile_urls($subcontext, 451 'mod_workshop', 'overallfeedback_content', $assid, $assessment->feedbackauthor); 452 453 $assessmentsubcontext = array_merge($subcontext, [get_string('assessments', 'mod_workshop'), $assid]); 454 455 unset($assessment->id); 456 unset($assessment->reviewerid); 457 458 $writer->export_data($assessmentsubcontext, $assessment); 459 $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_content', $assid); 460 $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_attachment', $assid); 461 462 // Export details of how the assessment forms were filled. 463 static::export_assessment_forms($user, $context, $assessmentsubcontext, $assid); 464 } 465 } 466 467 // Export plagiarism data related to the submission content. 468 // The last $linkarray argument consistent with how we call {@link plagiarism_get_links()} in the renderer. 469 \core_plagiarism\privacy\provider::export_plagiarism_user_data($user->id, $context, $subcontext, [ 470 'userid' => $user->id, 471 'content' => format_text($data->content, $data->contentformat, ['overflowdiv' => true]), 472 'cmid' => $context->instanceid, 473 'course' => $courseid, 474 ]); 475 } 476 477 $rs->close(); 478 } 479 480 /** 481 * Export all assessments given by the user. 482 * 483 * @param approved_contextlist $contextlist List of contexts approved for export. 484 */ 485 protected static function export_assessments(approved_contextlist $contextlist) { 486 global $DB; 487 488 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 489 $user = $contextlist->get_user(); 490 491 $sql = "SELECT ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title, ws.content, ws.contentformat, 492 wa.id, wa.submissionid, wa.reviewerid, wa.weight, wa.timecreated, wa.timemodified, wa.grade, 493 wa.gradinggrade, wa.gradinggradeover, wa.feedbackauthor, wa.feedbackauthorformat, wa.feedbackreviewer, 494 wa.feedbackreviewerformat, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')." 495 FROM {course_modules} cm 496 JOIN {modules} m ON cm.module = m.id AND m.name = :module 497 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel 498 JOIN {workshop} w ON cm.instance = w.id 499 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 500 JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND wa.reviewerid = :reviewerid 501 WHERE ctx.id {$contextsql}"; 502 503 $params = $contextparams + [ 504 'module' => 'workshop', 505 'contextlevel' => CONTEXT_MODULE, 506 'reviewerid' => $user->id, 507 ]; 508 509 $rs = $DB->get_recordset_sql($sql, $params); 510 511 foreach ($rs as $record) { 512 \context_helper::preload_from_record($record); 513 $context = \context_module::instance($record->cmid); 514 $writer = \core_privacy\local\request\writer::with_context($context); 515 $subcontext = [get_string('myassessments', 'mod_workshop'), $record->id]; 516 517 $data = (object) [ 518 'weight' => $record->weight, 519 'timecreated' => transform::datetime($record->timecreated), 520 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, 521 'grade' => $record->grade, 522 'gradinggrade' => $record->gradinggrade, 523 'gradinggradeover' => $record->gradinggradeover, 524 'feedbackauthor' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 525 'overallfeedback_content', $record->id, $record->feedbackauthor), 526 'feedbackauthorformat' => $record->feedbackauthorformat, 527 'feedbackreviewer' => $record->feedbackreviewer, 528 'feedbackreviewerformat' => $record->feedbackreviewerformat, 529 ]; 530 531 $submission = (object) [ 532 'myownsubmission' => transform::yesno($record->authorid == $user->id), 533 'example' => transform::yesno($record->example), 534 'timecreated' => transform::datetime($record->timecreated), 535 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, 536 'title' => $record->title, 537 'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 538 'submission_content', $record->submissionid, $record->content), 539 'contentformat' => $record->contentformat, 540 ]; 541 542 $writer->export_data($subcontext, $data); 543 $writer->export_related_data($subcontext, 'submission', $submission); 544 $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_content', $record->id); 545 $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_attachment', $record->id); 546 $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->submissionid); 547 $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->submissionid); 548 549 // Export details of how the assessment forms were filled. 550 static::export_assessment_forms($user, $context, $subcontext, $record->id); 551 } 552 553 $rs->close(); 554 } 555 556 /** 557 * Export the grading strategy data related to the particular assessment. 558 * 559 * @param stdClass $user User we are exporting for 560 * @param context $context Workshop activity content 561 * @param array $subcontext Subcontext path of the assessment 562 * @param int $assessmentid ID of the exported assessment 563 */ 564 protected static function export_assessment_forms(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { 565 566 foreach (\workshop::available_strategies_list() as $strategy => $title) { 567 $providername = '\workshopform_'.$strategy.'\privacy\provider'; 568 569 if (is_subclass_of($providername, '\mod_workshop\privacy\workshopform_provider')) { 570 component_class_callback($providername, 'export_assessment_form', 571 [ 572 $user, 573 $context, 574 array_merge($subcontext, [get_string('assessmentform', 'mod_workshop'), $title]), 575 $assessmentid, 576 ] 577 ); 578 579 } else { 580 debugging('Missing class '.$providername.' implementing workshopform_provider interface', DEBUG_DEVELOPER); 581 } 582 } 583 } 584 585 /** 586 * Delete personal data for all users in the context. 587 * 588 * @param context $context Context to delete personal data from. 589 */ 590 public static function delete_data_for_all_users_in_context(\context $context) { 591 global $CFG, $DB; 592 require_once($CFG->libdir.'/gradelib.php'); 593 594 if ($context->contextlevel != CONTEXT_MODULE) { 595 return; 596 } 597 598 $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING); 599 600 if (!$cm) { 601 // Probably some kind of expired context. 602 return; 603 } 604 605 $workshop = $DB->get_record('workshop', ['id' => $cm->instance], 'id, course', MUST_EXIST); 606 607 $submissions = $DB->get_records('workshop_submissions', ['workshopid' => $workshop->id], '', 'id'); 608 $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($submissions), '', 'id'); 609 610 $DB->delete_records('workshop_aggregations', ['workshopid' => $workshop->id]); 611 $DB->delete_records_list('workshop_grades', 'assessmentid', array_keys($assessments)); 612 $DB->delete_records_list('workshop_assessments', 'id', array_keys($assessments)); 613 $DB->delete_records_list('workshop_submissions', 'id', array_keys($submissions)); 614 615 $fs = get_file_storage(); 616 $fs->delete_area_files($context->id, 'mod_workshop', 'submission_content'); 617 $fs->delete_area_files($context->id, 'mod_workshop', 'submission_attachment'); 618 $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_content'); 619 $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_attachment'); 620 621 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 0, null, ['reset' => true]); 622 grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 1, null, ['reset' => true]); 623 624 \core_plagiarism\privacy\provider::delete_plagiarism_for_context($context); 625 } 626 627 /** 628 * Delete personal data for the user in a list of contexts. 629 * 630 * Removing assessments of submissions from the Workshop is not trivial. Removing one user's data can easily affect 631 * other users' grades and completion criteria. So we replace the non-essential contents with a "deleted" message, 632 * but keep the actual info in place. The argument is that one's right for privacy should not overweight others' 633 * right for accessing their own personal data and be evaluated on their basis. 634 * 635 * @param approved_contextlist $contextlist List of contexts to delete data from. 636 */ 637 public static function delete_data_for_user(approved_contextlist $contextlist) { 638 global $DB; 639 640 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 641 $user = $contextlist->get_user(); 642 $fs = get_file_storage(); 643 644 // Replace sensitive data in all submissions by the user in the given contexts. 645 646 $sql = "SELECT ws.id AS submissionid 647 FROM {course_modules} cm 648 JOIN {modules} m ON cm.module = m.id AND m.name = :module 649 JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id 650 JOIN {workshop} w ON cm.instance = w.id 651 JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid 652 WHERE ctx.id {$contextsql}"; 653 654 $params = $contextparams + [ 655 'module' => 'workshop', 656 'contextlevel' => CONTEXT_MODULE, 657 'authorid' => $user->id, 658 ]; 659 660 $submissionids = $DB->get_fieldset_sql($sql, $params); 661 662 if ($submissionids) { 663 list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED); 664 665 $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title', 666 'mod_workshop'), "id $submissionidsql", $submissionidparams); 667 $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content', 668 'mod_workshop'), "id $submissionidsql", $submissionidparams); 669 $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content', 670 'mod_workshop'), "id $submissionidsql", $submissionidparams); 671 672 foreach ($contextlist->get_contextids() as $contextid) { 673 $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_content', 674 $submissionidsql, $submissionidparams); 675 $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_attachment', 676 $submissionidsql, $submissionidparams); 677 } 678 } 679 680 // Replace personal data in received assessments - feedback is seen as belonging to the recipient. 681 682 $sql = "SELECT wa.id AS assessmentid 683 FROM {course_modules} cm 684 JOIN {modules} m ON cm.module = m.id AND m.name = :module 685 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel 686 JOIN {workshop} w ON cm.instance = w.id 687 JOIN {workshop_submissions} ws ON ws.workshopid = w.id AND ws.authorid = :authorid 688 JOIN {workshop_assessments} wa ON wa.submissionid = ws.id 689 WHERE ctx.id {$contextsql}"; 690 691 $params = $contextparams + [ 692 'module' => 'workshop', 693 'contextlevel' => CONTEXT_MODULE, 694 'authorid' => $user->id, 695 ]; 696 697 $assessmentids = $DB->get_fieldset_sql($sql, $params); 698 699 if ($assessmentids) { 700 list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); 701 702 $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content', 703 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); 704 705 foreach ($contextlist->get_contextids() as $contextid) { 706 $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_content', 707 $assessmentidsql, $assessmentidparams); 708 $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_attachment', 709 $assessmentidsql, $assessmentidparams); 710 } 711 } 712 713 // Replace sensitive data in provided assessments records. 714 715 $sql = "SELECT wa.id AS assessmentid 716 FROM {course_modules} cm 717 JOIN {modules} m ON cm.module = m.id AND m.name = :module 718 JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel 719 JOIN {workshop} w ON cm.instance = w.id 720 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 721 JOIN {workshop_assessments} wa ON wa.submissionid = ws.id AND wa.reviewerid = :reviewerid 722 WHERE ctx.id {$contextsql}"; 723 724 $params = $contextparams + [ 725 'module' => 'workshop', 726 'contextlevel' => CONTEXT_MODULE, 727 'reviewerid' => $user->id, 728 ]; 729 730 $assessmentids = $DB->get_fieldset_sql($sql, $params); 731 732 if ($assessmentids) { 733 list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); 734 735 $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content', 736 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); 737 } 738 739 foreach ($contextlist as $context) { 740 \core_plagiarism\privacy\provider::delete_plagiarism_for_user($user->id, $context); 741 } 742 } 743 744 /** 745 * Delete personal data for multiple users within a single workshop context. 746 * 747 * See documentation for {@link self::delete_data_for_user()} for more details on what we do and don't actually 748 * delete and why. 749 * 750 * @param approved_userlist $userlist The approved context and user information to delete information for. 751 */ 752 public static function delete_data_for_users(approved_userlist $userlist) { 753 global $DB; 754 755 $context = $userlist->get_context(); 756 $fs = get_file_storage(); 757 758 if ($context->contextlevel != CONTEXT_MODULE) { 759 // This should not happen but let's be double sure when it comes to deleting data. 760 return; 761 } 762 763 $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING); 764 765 if (!$cm) { 766 // Probably some kind of expired context. 767 return; 768 } 769 770 $userids = $userlist->get_userids(); 771 772 if (!$userids) { 773 return; 774 } 775 776 list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 777 778 // Erase sensitive data in all submissions by all the users in the given context. 779 780 $sql = "SELECT ws.id AS submissionid 781 FROM {workshop} w 782 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 783 WHERE w.id = :workshopid AND ws.authorid $usersql"; 784 785 $params = $userparams + [ 786 'workshopid' => $cm->instance, 787 ]; 788 789 $submissionids = $DB->get_fieldset_sql($sql, $params); 790 791 if ($submissionids) { 792 list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED); 793 794 $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title', 795 'mod_workshop'), "id $submissionidsql", $submissionidparams); 796 $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content', 797 'mod_workshop'), "id $submissionidsql", $submissionidparams); 798 $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content', 799 'mod_workshop'), "id $submissionidsql", $submissionidparams); 800 801 $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_content', 802 $submissionidsql, $submissionidparams); 803 $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_attachment', 804 $submissionidsql, $submissionidparams); 805 } 806 807 // Erase personal data in received assessments - feedback is seen as belonging to the recipient. 808 809 $sql = "SELECT wa.id AS assessmentid 810 FROM {workshop} w 811 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 812 JOIN {workshop_assessments} wa ON wa.submissionid = ws.id 813 WHERE w.id = :workshopid AND ws.authorid $usersql"; 814 815 $params = $userparams + [ 816 'workshopid' => $cm->instance, 817 ]; 818 819 $assessmentids = $DB->get_fieldset_sql($sql, $params); 820 821 if ($assessmentids) { 822 list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); 823 824 $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content', 825 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); 826 827 $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_content', 828 $assessmentidsql, $assessmentidparams); 829 $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_attachment', 830 $assessmentidsql, $assessmentidparams); 831 } 832 833 // Erase sensitive data in provided assessments records. 834 835 $sql = "SELECT wa.id AS assessmentid 836 FROM {workshop} w 837 JOIN {workshop_submissions} ws ON ws.workshopid = w.id 838 JOIN {workshop_assessments} wa ON wa.submissionid = ws.id 839 WHERE w.id = :workshopid AND wa.reviewerid $usersql"; 840 841 $params = $userparams + [ 842 'workshopid' => $cm->instance, 843 ]; 844 845 $assessmentids = $DB->get_fieldset_sql($sql, $params); 846 847 if ($assessmentids) { 848 list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); 849 850 $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content', 851 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); 852 } 853 854 foreach ($userids as $userid) { 855 \core_plagiarism\privacy\provider::delete_plagiarism_for_user($userid, $context); 856 } 857 } 858 859 /** 860 * Get the user preferences. 861 * 862 * @return array List of user preferences 863 */ 864 protected static function get_user_prefs(): array { 865 return [ 866 'workshop_perpage', 867 'workshop-viewlet-allexamples-collapsed', 868 'workshop-viewlet-allsubmissions-collapsed', 869 'workshop-viewlet-assessmentform-collapsed', 870 'workshop-viewlet-assignedassessments-collapsed', 871 'workshop-viewlet-cleargrades-collapsed', 872 'workshop-viewlet-conclusion-collapsed', 873 'workshop-viewlet-examples-collapsed', 874 'workshop-viewlet-examplesfail-collapsed', 875 'workshop-viewlet-gradereport-collapsed', 876 'workshop-viewlet-instructauthors-collapsed', 877 'workshop-viewlet-instructreviewers-collapsed', 878 'workshop-viewlet-intro-collapsed', 879 'workshop-viewlet-overallfeedback-collapsed', 880 'workshop-viewlet-ownsubmission-collapsed', 881 'workshop-viewlet-publicsubmissions-collapsed', 882 'workshop-viewlet-yourgrades-collapsed' 883 ]; 884 } 885 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body