diff --git a/mod/workshop/classes/privacy/provider.php b/mod/workshop/classes/privacy/provider.php index ed4eb6dd921..1a93a3c8f30 100644 --- a/mod/workshop/classes/privacy/provider.php +++ b/mod/workshop/classes/privacy/provider.php @@ -27,10 +27,12 @@ namespace mod_workshop\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\deletion_criteria; use core_privacy\local\request\helper; use core_privacy\local\request\transform; +use core_privacy\local\request\userlist; use core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); @@ -45,6 +47,7 @@ require_once($CFG->dirroot.'/mod/workshop/locallib.php'); */ class provider implements \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\user_preference_provider, \core_privacy\local\request\plugin\provider { @@ -161,6 +164,63 @@ class provider implements return $contextlist; } + /** + * Get the list of users within a specific context. + * + * @param userlist $userlist To be filled list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'module' => 'workshop', + ]; + + // One query to fetch them all, one query to find them, one query to bring them all and into the userlist add them. + $sql = "SELECT ws.authorid, ws.gradeoverby, wa.reviewerid, wa.gradinggradeoverby, wr.userid + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id + WHERE cm.id = :instanceid"; + + $userids = []; + $rs = $DB->get_recordset_sql($sql, $params); + + foreach ($rs as $r) { + if ($r->authorid) { + $userids[$r->authorid] = true; + } + if ($r->gradeoverby) { + $userids[$r->gradeoverby] = true; + } + if ($r->reviewerid) { + $userids[$r->reviewerid] = true; + } + if ($r->gradinggradeoverby) { + $userids[$r->gradinggradeoverby] = true; + } + if ($r->userid) { + $userids[$r->userid] = true; + } + } + + $rs->close(); + + if ($userids) { + $userlist->add_users(array_keys($userids)); + } + } + /** * Export personal data stored in the given contexts. * @@ -662,4 +722,119 @@ class provider implements \core_plagiarism\privacy\provider::delete_plagiarism_for_user($user->id, $context); } } + + /** + * Delete personal data for multiple users within a single workshop context. + * + * See documentation for {@link self::delete_data_for_user()} for more details on what we do and don't actually + * delete and why. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $fs = get_file_storage(); + + if ($context->contextlevel != CONTEXT_MODULE) { + // This should not happen but let's be double sure when it comes to deleting data. + return; + } + + $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING); + + if (!$cm) { + // Probably some kind of expired context. + return; + } + + $userids = $userlist->get_userids(); + + if (!$userids) { + return; + } + + list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + + // Erase sensitive data in all submissions by all the users in the given context. + + $sql = "SELECT ws.id AS submissionid + FROM {workshop} w + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + WHERE w.id = :workshopid AND ws.authorid $usersql"; + + $params = $userparams + [ + 'workshopid' => $cm->instance, + ]; + + $submissionids = $DB->get_fieldset_sql($sql, $params); + + if ($submissionids) { + list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + + $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_content', + $submissionidsql, $submissionidparams); + $fs->delete_area_files_select($context->id, 'mod_workshop', 'submission_attachment', + $submissionidsql, $submissionidparams); + } + + // Erase personal data in received assessments - feedback is seen as belonging to the recipient. + + $sql = "SELECT wa.id AS assessmentid + FROM {workshop} w + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + WHERE w.id = :workshopid AND ws.authorid $usersql"; + + $params = $userparams + [ + 'workshopid' => $cm->instance, + ]; + + $assessmentids = $DB->get_fieldset_sql($sql, $params); + + if ($assessmentids) { + list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); + + $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_content', + $assessmentidsql, $assessmentidparams); + $fs->delete_area_files_select($context->id, 'mod_workshop', 'overallfeedback_attachment', + $assessmentidsql, $assessmentidparams); + } + + // Erase sensitive data in provided assessments records. + + $sql = "SELECT wa.id AS assessmentid + FROM {workshop} w + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + WHERE w.id = :workshopid AND wa.reviewerid $usersql"; + + $params = $userparams + [ + 'workshopid' => $cm->instance, + ]; + + $assessmentids = $DB->get_fieldset_sql($sql, $params); + + if ($assessmentids) { + list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); + } + + foreach ($userids as $userid) { + \core_plagiarism\privacy\provider::delete_plagiarism_for_user($userid, $context); + } + } } diff --git a/mod/workshop/lang/en/workshop.php b/mod/workshop/lang/en/workshop.php index fc931307902..403c377c7be 100644 --- a/mod/workshop/lang/en/workshop.php +++ b/mod/workshop/lang/en/workshop.php @@ -275,7 +275,7 @@ $string['privacy:metadata:workshopgrades'] = 'Holds information about how the as $string['privacy:metadata:workshopid'] = 'ID of the workshop activity'; $string['privacy:metadata:workshopsubmissions'] = 'Holds information about workshop module submissions'; $string['privacy:request:delete:title'] = '[Deleted]'; -$string['privacy:request:delete:content'] = 'The content has been deleted at the request of the user.'; +$string['privacy:request:delete:content'] = 'The content has been deleted (requested by the user or expired)'; $string['publishedsubmissions'] = 'Published submissions'; $string['publishsubmission'] = 'Publish submission'; $string['publishsubmission_help'] = 'Published submissions are available to the others when the workshop is closed.'; diff --git a/mod/workshop/tests/privacy_provider_test.php b/mod/workshop/tests/privacy_provider_test.php index 5c6951617e0..e0c6d81f516 100644 --- a/mod/workshop/tests/privacy_provider_test.php +++ b/mod/workshop/tests/privacy_provider_test.php @@ -204,6 +204,53 @@ class mod_workshop_privacy_provider_testcase extends advanced_testcase { $this->assertEquals([$context21->id, $context12->id], $contextlist->get_contextids(), null, 0.0, 10, true); } + /** + * Test {@link \mod_workshop\privacy\provider::get_users_in_context()} implementation. + */ + public function test_get_users_in_context() { + + $cm11 = get_coursemodule_from_instance('workshop', $this->workshop11->id); + $cm12 = get_coursemodule_from_instance('workshop', $this->workshop12->id); + $cm21 = get_coursemodule_from_instance('workshop', $this->workshop21->id); + + $context11 = context_module::instance($cm11->id); + $context12 = context_module::instance($cm12->id); + $context21 = context_module::instance($cm21->id); + + // Users in the workshop11. + $userlist11 = new \core_privacy\local\request\userlist($context11, 'mod_workshop'); + \mod_workshop\privacy\provider::get_users_in_context($userlist11); + $expected11 = [ + $this->student1->id, // Student1 has data in workshop11 (author + self reviewer). + $this->student2->id, // Student2 has data in workshop11 (reviewer). + $this->student3->id, // Student3 has data in workshop11 (reviewer). + ]; + $actual11 = $userlist11->get_userids(); + $this->assertEquals($expected11, $actual11, '', 0, 10, true); + + // Users in the workshop12. + $userlist12 = new \core_privacy\local\request\userlist($context12, 'mod_workshop'); + \mod_workshop\privacy\provider::get_users_in_context($userlist12); + $expected12 = [ + $this->student1->id, // Student1 has data in workshop12 (author). + $this->student2->id, // Student2 has data in workshop12 (reviewer). + $this->teacher4->id, // Teacher4 has data in workshop12 (gradeoverby). + ]; + $actual12 = $userlist12->get_userids(); + $this->assertEquals($expected12, $actual12, '', 0, 10, true); + + // Users in the workshop21. + $userlist21 = new \core_privacy\local\request\userlist($context21, 'mod_workshop'); + \mod_workshop\privacy\provider::get_users_in_context($userlist21); + $expected21 = [ + $this->student1->id, // Student1 has data in workshop21 (reviewer). + $this->student2->id, // Student2 has data in workshop21 (author). + $this->teacher4->id, // Teacher4 has data in workshop21 (gradinggradeoverby). + ]; + $actual21 = $userlist21->get_userids(); + $this->assertEquals($expected21, $actual21, '', 0, 10, true); + } + /** * Test {@link \mod_workshop\privacy\provider::export_user_data()} implementation. */ @@ -376,4 +423,116 @@ class mod_workshop_privacy_provider_testcase extends advanced_testcase { } } } + + /** + * Test {@link \mod_workshop\privacy\provider::delete_data_for_users()} implementation. + */ + public function test_delete_data_for_users() { + global $DB; + + // Student1 has submissions in two workshops. + $this->assertFalse($this->is_submission_erased($this->submission111)); + $this->assertFalse($this->is_submission_erased($this->submission121)); + + // Student1 has self-assessed one their submission. + $this->assertFalse($this->is_given_assessment_erased($this->assessment1111)); + $this->assertFalse($this->is_received_assessment_erased($this->assessment1111)); + + // Student2 and student3 peer-assessed student1's submission. + $this->assertFalse($this->is_given_assessment_erased($this->assessment1112)); + $this->assertFalse($this->is_given_assessment_erased($this->assessment1113)); + + // Delete data owned by student1 and student3 in the workshop11. + + $context11 = \context_module::instance($this->workshop11->cmid); + + $approveduserlist = new \core_privacy\local\request\approved_userlist($context11, 'mod_workshop', [ + $this->student1->id, + $this->student3->id, + ]); + \mod_workshop\privacy\provider::delete_data_for_users($approveduserlist); + + // Student1's submission is erased in workshop11 but not in the other workshop12. + $this->assertTrue($this->is_submission_erased($this->submission111)); + $this->assertFalse($this->is_submission_erased($this->submission121)); + + // Student1's self-assessment is erased. + $this->assertTrue($this->is_given_assessment_erased($this->assessment1111)); + $this->assertTrue($this->is_received_assessment_erased($this->assessment1111)); + + // Student1's received peer-assessments are also erased because they are "owned" by the recipient of the assessment. + $this->assertTrue($this->is_received_assessment_erased($this->assessment1112)); + $this->assertTrue($this->is_received_assessment_erased($this->assessment1113)); + + // Student2's owned data in the given assessment are not erased. + $this->assertFalse($this->is_given_assessment_erased($this->assessment1112)); + + // Student3's owned data in the given assessment were erased because she/he was in the userlist. + $this->assertTrue($this->is_given_assessment_erased($this->assessment1113)); + + // Personal data in other contexts are not affected. + $this->assertFalse($this->is_submission_erased($this->submission121)); + $this->assertFalse($this->is_given_assessment_erased($this->assessment2121)); + $this->assertFalse($this->is_received_assessment_erased($this->assessment2121)); + } + + /** + * Check if the given submission has the author's personal data erased. + * + * @param int $submissionid Identifier of the submission. + * @return boolean + */ + protected function is_submission_erased(int $submissionid) { + global $DB; + + $submission = $DB->get_record('workshop_submissions', ['id' => $submissionid], 'id, title, content', MUST_EXIST); + + $titledeleted = $submission->title === get_string('privacy:request:delete:title', 'mod_workshop'); + $contentdeleted = $submission->content === get_string('privacy:request:delete:content', 'mod_workshop'); + + if ($titledeleted && $contentdeleted) { + return true; + + } else { + return false; + } + } + + /** + * Check is the received assessment has recipient's (author's) personal data erased. + * + * @param int $assessmentid Identifier of the assessment. + * @return boolean + */ + protected function is_received_assessment_erased(int $assessmentid) { + global $DB; + + $assessment = $DB->get_record('workshop_assessments', ['id' => $assessmentid], 'id, feedbackauthor', MUST_EXIST); + + if ($assessment->feedbackauthor === get_string('privacy:request:delete:content', 'mod_workshop')) { + return true; + + } else { + return false; + } + } + + /** + * Check is the given assessment has reviewer's personal data erased. + * + * @param int $assessmentid Identifier of the assessment. + * @return boolean + */ + protected function is_given_assessment_erased(int $assessmentid) { + global $DB; + + $assessment = $DB->get_record('workshop_assessments', ['id' => $assessmentid], 'id, feedbackreviewer', MUST_EXIST); + + if ($assessment->feedbackreviewer === get_string('privacy:request:delete:content', 'mod_workshop')) { + return true; + + } else { + return false; + } + } }