diff --git a/mod/assignment/classes/privacy/provider.php b/mod/assignment/classes/privacy/provider.php new file mode 100644 index 0000000000000..bc4dea16206a9 --- /dev/null +++ b/mod/assignment/classes/privacy/provider.php @@ -0,0 +1,475 @@ +. + +/** + * Privacy Subsystem implementation for mod_assignment. + * + * @package mod_assignment + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_assignment\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use core_privacy\local\request\helper; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assignment/lib.php'); + +/** + * Implementation of the privacy subsystem plugin provider for mod_assignment. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * Return the fields which contain personal data. + * + * @param collection $collection a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table( + 'assignment_submissions', + [ + 'userid' => 'privacy:metadata:assignment_submissions:userid', + 'timecreated' => 'privacy:metadata:assignment_submissions:timecreated', + 'timemodified' => 'privacy:metadata:assignment_submissions:timemodified', + 'numfiles' => 'privacy:metadata:assignment_submissions:numfiles', + 'data1' => 'privacy:metadata:assignment_submissions:data1', + 'data2' => 'privacy:metadata:assignment_submissions:data2', + 'grade' => 'privacy:metadata:assignment_submissions:grade', + 'submissioncomment' => 'privacy:metadata:assignment_submissions:submissioncomment', + 'teacher' => 'privacy:metadata:assignment_submissions:teacher', + 'timemarked' => 'privacy:metadata:assignment_submissions:timemarked', + 'mailed' => 'privacy:metadata:assignment_submissions:mailed' + ], + 'privacy:metadata:assignment_submissions' + ); + + // Legacy mod_assignment preferences from Moodle 2.X. + $collection->add_user_preference('assignment_filter', 'privacy:metadata:assignmentfilter'); + $collection->add_user_preference('assignment_mailinfo', 'privacy:metadata:assignmentmailinfo'); + $collection->add_user_preference('assignment_perpage', 'privacy:metadata:assignmentperpage'); + $collection->add_user_preference('assignment_quickgrade', 'privacy:metadata:assignmentquickgrade'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new contextlist(); + + $sql = "SELECT DISTINCT + ctx.id + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assignment} a ON cm.instance = a.id + JOIN {assignment_submissions} s ON s.assignment = a.id + WHERE s.userid = :userid + OR s.teacher = :teacher"; + + $params = [ + 'contextmodule' => CONTEXT_MODULE, + 'modulename' => 'assignment', + 'userid' => $userid, + 'teacher' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. + * User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + if (empty($contextlist->count())) { + return; + } + + $user = $contextlist->get_user(); + + foreach ($contextlist->get_contexts() as $context) { + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + + // Cannot make use of helper::export_context_files(), need to manually export assignment details. + $assignmentdata = self::get_assignment_by_context($context); + + // Get assignment details object for output. + $assignment = self::get_assignment_output($assignmentdata); + writer::with_context($context)->export_data([], $assignment); + + // Check if the user has marked any assignment's submissions to determine assignment submissions to export. + $teacher = (self::has_marked_assignment_submissions($assignmentdata->id, $user->id) == true) ? true : false; + + // Get the assignment submissions submitted by & marked by the user for an assignment. + $submissionsdata = self::get_assignment_submissions_by_assignment($assignmentdata->id, $user->id, $teacher); + + foreach ($submissionsdata as $submissiondata) { + // Default subcontext path to export assignment submissions submitted by the user. + $subcontexts = [ + get_string('privacy:submissionpath', 'mod_assignment') + ]; + + if ($teacher == true) { + if ($submissiondata->teacher == $user->id) { + // Export assignment submissions that have been marked by the user. + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($submissiondata->userid) + ]; + } + } + + // Get assignment submission details object for output. + $submission = self::get_assignment_submission_output($submissiondata); + $itemid = $submissiondata->id; + + writer::with_context($context) + ->export_data($subcontexts, $submission) + ->export_area_files($subcontexts, 'mod_assignment', 'submission', $itemid); + } + } + } + + /** + * Stores the user preferences related to mod_assign. + * + * @param int $userid The user ID that we want the preferences for. + */ + public static function export_user_preferences(int $userid) { + $context = \context_system::instance(); + $assignmentpreferences = [ + 'assignment_filter' => [ + 'string' => get_string('privacy:metadata:assignmentfilter', 'mod_assignment'), + 'bool' => false + ], + 'assignment_mailinfo' => [ + 'string' => get_string('privacy:metadata:assignmentmailinfo', 'mod_assignment'), + 'bool' => false + ], + 'assignment_perpage' => [ + 'string' => get_string('privacy:metadata:assignmentperpage', 'mod_assignment'), + 'bool' => false + ], + 'assignment_quickgrade' => [ + 'string' => get_string('privacy:metadata:assignmentquickgrade', 'mod_assignment'), + 'bool' => false + ], + ]; + foreach ($assignmentpreferences as $key => $preference) { + $value = get_user_preferences($key, null, $userid); + if ($preference['bool']) { + $value = transform::yesno($value); + } + if (isset($value)) { + writer::with_context($context) + ->export_user_preference('mod_assignment', $key, $value, $preference['string']); + } + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (empty($context)) { + return; + } + + if ($context->contextlevel == CONTEXT_MODULE) { + // Delete all assignment submissions for the assignment associated with the context module. + $assignment = self::get_assignment_by_context($context); + if ($assignment != null) { + $DB->delete_records('assignment_submissions', ['assignment' => $assignment->id]); + + // Delete all file uploads associated with the assignment submission for the specified context. + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'mod_assignment', 'submission'); + } + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + + // Only retrieve assignment submissions submitted by the user for deletion. + $assignmentsubmissionids = array_keys(self::get_assignment_submissions_by_contextlist($contextlist, $userid)); + $DB->delete_records_list('assignment_submissions', 'id', $assignmentsubmissionids); + + // Delete all file uploads associated with the assignment submission for the user's specified list of contexts. + $fs = get_file_storage(); + foreach ($contextlist->get_contextids() as $contextid) { + foreach ($assignmentsubmissionids as $submissionid) { + $fs->delete_area_files($contextid, 'mod_assignment', 'submission', $submissionid); + } + } + } + + // Start of helper functions. + + /** + * Helper function to check if a user has marked assignment submissions for a given assignment. + * + * @param int $assignmentid The assignment ID to check if user has marked associated submissions. + * @param int $userid The user ID to check if user has marked associated submissions. + * @return bool If user has marked associated submissions returns true, otherwise false. + * @throws \dml_exception + */ + protected static function has_marked_assignment_submissions($assignmentid, $userid) { + global $DB; + + $params = [ + 'assignment' => $assignmentid, + 'teacher' => $userid + ]; + + $sql = "SELECT count(s.id) as nomarked + FROM {assignment_submissions} s + WHERE s.assignment = :assignment + AND s.teacher = :teacher"; + + $results = $DB->get_record_sql($sql, $params); + + return ($results->nomarked > 0) ? true : false; + } + + /** + * Helper function to return assignment for a context module. + * + * @param object $context The context module object to return the assignment record by. + * @return mixed The assignment details or null record associated with the context module. + * @throws \dml_exception + */ + protected static function get_assignment_by_context($context) { + global $DB; + + $params = [ + 'modulename' => 'assignment', + 'contextmodule' => CONTEXT_MODULE, + 'contextid' => $context->id + ]; + + $sql = "SELECT a.id, + a.name, + a.intro, + a.assignmenttype, + a.grade, + a.timedue, + a.timeavailable, + a.timemodified + FROM {assignment} a + JOIN {course_modules} cm ON a.id = cm.instance + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid"; + + return $DB->get_record_sql($sql, $params); + } + + /** + * Helper function to return assignment submissions submitted by / marked by a user and their contextlist. + * + * @param object $contextlist Object with the contexts related to a userid to retrieve assignment submissions by. + * @param int $userid The user ID to find assignment submissions that were submitted by. + * @param bool $teacher The teacher status to determine if marked assignment submissions should be returned. + * @return array Array of assignment submission details. + * @throws \coding_exception + * @throws \dml_exception + */ + protected static function get_assignment_submissions_by_contextlist($contextlist, $userid, $teacher = false) { + global $DB; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $params = [ + 'contextmodule' => CONTEXT_MODULE, + 'modulename' => 'assignment', + 'userid' => $userid + ]; + + $sql = "SELECT s.id as id, + s.assignment as assignment, + s.numfiles as numfiles, + s.data1 as data1, + s.data2 as data2, + s.grade as grade, + s.submissioncomment as submissioncomment, + s.teacher as teacher, + s.timemarked as timemarked, + s.timecreated as timecreated, + s.timemodified as timemodified + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assignment} a ON cm.instance = a.id + JOIN {assignment_submissions} s ON s.assignment = a.id + WHERE (s.userid = :userid"; + + if ($teacher == true) { + $sql .= " OR s.teacher = :teacher"; + $params['teacher'] = $userid; + } + + $sql .= ")"; + + $sql .= " AND ctx.id {$contextsql}"; + $params += $contextparams; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Helper function to retrieve assignment submissions submitted by / marked by a user for a specific assignment. + * + * @param int $assignmentid The assignment ID to retrieve assignment submissions by. + * @param int $userid The user ID to retrieve assignment submissions submitted / marked by. + * @param bool $teacher The teacher status to determine if marked assignment submissions should be returned. + * @return array Array of assignment submissions details. + * @throws \dml_exception + */ + protected static function get_assignment_submissions_by_assignment($assignmentid, $userid, $teacher = false) { + global $DB; + + $params = [ + 'assignment' => $assignmentid, + 'userid' => $userid + ]; + + $sql = "SELECT s.id as id, + s.assignment as assignment, + s.numfiles as numfiles, + s.data1 as data1, + s.data2 as data2, + s.grade as grade, + s.submissioncomment as submissioncomment, + s.teacher as teacher, + s.timemarked as timemarked, + s.timecreated as timecreated, + s.timemodified as timemodified, + s.userid as userid + FROM {assignment_submissions} s + WHERE s.assignment = :assignment + AND (s.userid = :userid"; + + if ($teacher == true) { + $sql .= " OR s.teacher = :teacher"; + $params['teacher'] = $userid; + } + + $sql .= ")"; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Helper function generate assignment output object for exporting. + * + * @param object $assignmentdata Object containing assignment data. + * @return object Formatted assignment output object for exporting. + */ + protected static function get_assignment_output($assignmentdata) { + $assignment = (object) [ + 'name' => $assignmentdata->name, + 'intro' => $assignmentdata->intro, + 'assignmenttype' => $assignmentdata->assignmenttype, + 'grade' => $assignmentdata->grade, + 'timemodified' => transform::datetime($assignmentdata->timemodified) + ]; + + if ($assignmentdata->timeavailable != 0) { + $assignment->timeavailable = transform::datetime($assignmentdata->timeavailable); + } + + if ($assignmentdata->timedue != 0) { + $assignment->timedue = transform::datetime($assignmentdata->timedue); + } + + return $assignment; + } + + /** + * Helper function generate assignment submission output object for exporting. + * + * @param object $submissiondata Object containing assignment submission data. + * @return object Formatted assignment submission output for exporting. + */ + protected static function get_assignment_submission_output($submissiondata) { + $submission = (object) [ + 'assignment' => $submissiondata->assignment, + 'numfiles' => $submissiondata->numfiles, + 'data1' => $submissiondata->data1, + 'data2' => $submissiondata->data2, + 'grade' => $submissiondata->grade, + 'submissioncomment' => $submissiondata->submissioncomment, + 'teacher' => transform::user($submissiondata->teacher) + ]; + + if ($submissiondata->timecreated != 0) { + $submission->timecreated = transform::datetime($submissiondata->timecreated); + } + + if ($submissiondata->timemarked != 0) { + $submission->timemarked = transform::datetime($submissiondata->timemarked); + } + + if ($submissiondata->timemodified != 0) { + $submission->timemodified = transform::datetime($submissiondata->timemodified); + } + + return $submission; + } +} diff --git a/mod/assignment/lang/en/assignment.php b/mod/assignment/lang/en/assignment.php index 969f22d1cedf1..b13ba8e613b04 100644 --- a/mod/assignment/lang/en/assignment.php +++ b/mod/assignment/lang/en/assignment.php @@ -42,3 +42,21 @@ $string['upgradenotification'] = 'This activity is based on an older assignment module.'; $string['viewassignmentupgradetool'] = 'View the assignment upgrade tool'; $string['pluginadministration'] = 'Assignment 2.2 (Disabled) administration'; +$string['privacy:markedsubmissionspath'] = 'markedsubmissions'; +$string['privacy:submissionpath'] = 'submission'; +$string['privacy:metadata:assignmentfilter'] = 'Filter preference of assignment submissions.'; +$string['privacy:metadata:assignmentperpage'] = 'Number of assignment submissions shown per page preference.'; +$string['privacy:metadata:assignmentmailinfo'] = 'Mail info preference for assignment submissions.'; +$string['privacy:metadata:assignmentquickgrade'] = 'Quick grading preference for assignment submissions.'; +$string['privacy:metadata:assignment_submissions'] = 'Assignment submissions associated with an assignment.'; +$string['privacy:metadata:assignment_submissions:userid'] = 'The user ID submitting the assignment submission.'; +$string['privacy:metadata:assignment_submissions:timecreated'] = 'The creation date/time of the assignment submission.'; +$string['privacy:metadata:assignment_submissions:timemodified'] = 'The modification date/time of the assignment submission.'; +$string['privacy:metadata:assignment_submissions:numfiles'] = 'The maximum number of files allowed for the assignment submission.'; +$string['privacy:metadata:assignment_submissions:data1'] = 'The onlinetext submitted for the assignment submission.'; +$string['privacy:metadata:assignment_submissions:data2'] = ''; +$string['privacy:metadata:assignment_submissions:grade'] = 'The grade value awarded for the assignment submission.'; +$string['privacy:metadata:assignment_submissions:submissioncomment'] = 'The submission comment accompanying the assignment submission.'; +$string['privacy:metadata:assignment_submissions:teacher'] = 'The teacher user ID grading the assignment submission.'; +$string['privacy:metadata:assignment_submissions:timemarked'] = 'The marking date/time of the assignment submission.'; +$string['privacy:metadata:assignment_submissions:mailed'] = 'The mailed notification status of the assignment submission.'; diff --git a/mod/assignment/tests/privacy_test.php b/mod/assignment/tests/privacy_test.php new file mode 100644 index 0000000000000..5c53a64e3db6b --- /dev/null +++ b/mod/assignment/tests/privacy_test.php @@ -0,0 +1,606 @@ +. + +/** + * Privacy test for the event monitor + * + * @package mod_assignment + * @category test + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../lib.php'); + +use \mod_assignment\privacy\provider; +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\writer; +use \core_privacy\tests\provider_testcase; + +/** + * Privacy test for the event monitor + * + * @package mod_assignment + * @category test + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_assignment_privacy_testcase extends advanced_testcase { + + /** + * @var int array Array of test student ids associated for Course 1. + */ + private $course1students = []; + + /** + * @var int array Array of test student ids associated for Course 2. + */ + private $course2students = []; + + /** + * Test for provider::get_contexts_for_userid(). + * + * @throws coding_exception + */ + public function test_get_contexts_for_userid() { + global $DB; + + $this->resetAfterTest(true); + $this->create_courses_and_assignments(); + + // Get Teacher 1 to test get_contexts_for_userid(). + $teacher1 = $DB->get_record('user', ['username' => 'teacher1']); + $contextids = provider::get_contexts_for_userid($teacher1->id); + // Verify there should be 4 contexts, as Teacher 1 has submitted tests and marked Assignments in Course 1 and 2. + $this->assertEquals(4, count($contextids->get_contextids())); + + // Get Teacher 2 to test get_contexts_for_userid(). + $teacher2 = $DB->get_record('user', ['username' => 'teacher2']); + $contextids = provider::get_contexts_for_userid($teacher2->id); + // Verify there should be 0 contexts, as teacher 2 has not marked any Assignments. + $this->assertEquals(0, count($contextids->get_contextids())); + + // Get Student 1 to test get_contexts_for_userid(). + $student1 = $DB->get_record('user', ['username' => 'student1']); + $contextids = provider::get_contexts_for_userid($student1->id); + // Verify there should be 2 contexts, as student 1 added submissions for both Assignments in Course 1. + $this->assertEquals(2, count($contextids->get_contextids())); + + // Get Student 2 to test get_contexts_for_userid(). + $student2 = $DB->get_record('user', ['username' => 'student2']); + $contextids = provider::get_contexts_for_userid($student2->id); + // Verify there should be 2 context, as student 2 added submissions for both Assignments in Course 2. + $this->assertEquals(2, count($contextids->get_contextids())); + } + + /** + * Test for provider::export_user_data(). + * + * @throws coding_exception + */ + public function test_export_user_data_teacher() { + global $DB; + + $this->resetAfterTest(true); + $this->create_courses_and_assignments(); + + // Test Teacher 1 export_data_for_user() - marking assignment submissions for both Course 1 and 2. + $teacher1 = $DB->get_record('user', ['username' => 'teacher1']); + + $contextlist = provider::get_contexts_for_userid($teacher1->id); + $approvedcontextlist = new approved_contextlist($teacher1, 'mod_assignment', $contextlist->get_contextids()); + + // Verify Teacher 1 has four contexts. + $this->assertCount(4, $contextlist->get_contextids()); + + // Retrieve Assignment Submissions data for Teacher 1. + provider::export_user_data($approvedcontextlist); + + $contexts = $contextlist->get_contexts(); + + // Context 1 - Course 1's Assignment 1 -- (onlinetext). + $writer = writer::with_context($contexts[0]); + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($teacher1->id) + ]; + // Verify the test assignment submission from Teacher 1 exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals('

Course 1 - Ass 1: Teacher Test Submission

', $submission->data1); + + foreach ($this->course1students as $student) { + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($student->id) + ]; + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 1 - Ass 1: " . $student->id . "

", $submission->data1); + } + + // Context 2 - Course 1's Assignment 2 -- (single file upload). + $writer = writer::with_context($contexts[1]); + foreach ($this->course1students as $student) { + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($student->id) + ]; + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 1 - Ass 2: " . $student->id . "

", $submission->data1); + + // Verify the student assignment submission file upload exists. + $submissionfiles = $writer->get_files($subcontexts); + $this->assertTrue(array_key_exists('Student' . $student->id . '-Course1-Ass2-(File 1 of 1)', $submissionfiles)); + } + + // Context 3 - Course 2's Assignment 1 -- (offline). + $writer = writer::with_context($contexts[2]); + foreach ($this->course2students as $student) { + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($student->id) + ]; + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 2 - Ass 1: " . $student->id . "

", $submission->data1); + } + + // Context 4 - Course 2's Assignment 2 -- (multiple file upload). + $writer = writer::with_context($contexts[3]); + foreach ($this->course2students as $student) { + $subcontexts = [ + get_string('privacy:markedsubmissionspath', 'mod_assignment'), + transform::user($student->id) + ]; + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 2 - Ass 2: " . $student->id . "

", $submission->data1); + + // Verify the student assignment submission file upload exists. + $submissionfiles = $writer->get_files($subcontexts); + $this->assertTrue(array_key_exists('Student' . $student->id . '-Course2-Ass2-(File 1 of 2)', $submissionfiles)); + $this->assertTrue(array_key_exists('Student' . $student->id . '-Course2-Ass2-(File 2 of 2)', $submissionfiles)); + } + } + + /** + * Test for provider::export_user_data(). + * + * @throws dml_exception + */ + public function test_export_user_data_student() { + global $DB; + + $this->resetAfterTest(true); + $this->create_courses_and_assignments(); + + // Test Student 1 export_data_for_user() - added assignment submissions for both assignments in Course 1. + $student1 = $DB->get_record('user', ['username' => 'student1']); + + $contextlist = provider::get_contexts_for_userid($student1->id); + $approvedcontextlist = new approved_contextlist($student1, 'mod_assignment', $contextlist->get_contextids()); + + // Retrieve Assignment Submissions data for Student 1. + provider::export_user_data($approvedcontextlist); + $contexts = $contextlist->get_contexts(); + + // Context 1 - Course 1's Assignment 1 -- (onlinetext). + $writer = writer::with_context($contexts[0]); + $subcontexts = [ + get_string('privacy:submissionpath', 'mod_assignment') + ]; + + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 1 - Ass 1: " . $student1->id . "

", $submission->data1); + + // Context 2 - Course 1's Assignment 2 -- (single file upload). + $writer = writer::with_context($contexts[1]); + $subcontexts = [ + get_string('privacy:submissionpath', 'mod_assignment') + ]; + + // Verify the student assignment submission exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 1 - Ass 2: " . $student1->id . "

", $submission->data1); + + // Verify the student assignment submission file upload exists. + $submissionfiles = $writer->get_files($subcontexts); + $this->assertTrue(array_key_exists('Student' . $student1->id . '-Course1-Ass2-(File 1 of 1)', $submissionfiles)); + + // Test Student 2 export_data_for_user() - added assignment submissions for both assignments in Course 2. + $student2 = $DB->get_record('user', ['username' => 'student2']); + + $contextlist = provider::get_contexts_for_userid($student2->id); + $approvedcontextlist = new approved_contextlist($student2, 'mod_assignment', $contextlist->get_contextids()); + + // Retrieve Assignment Submissions data for Student 2. + provider::export_user_data($approvedcontextlist); + $contexts = $contextlist->get_contexts(); + + // Context 1 - Course 2's Assignment 1 -- (offline). + $writer = writer::with_context($contexts[0]); + $subcontexts = [ + get_string('privacy:submissionpath', 'mod_assignment') + ]; + + // Verify the student assignment submissions exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 2 - Ass 1: " . $student2->id . "

", $submission->data1); + + // Context 2 - Course 2's Assignment 2 -- (multiple file upload). + $writer = writer::with_context($contexts[1]); + $subcontexts = [ + get_string('privacy:submissionpath', 'mod_assignment') + ]; + + // Verify the student assignment submission exists. + $submission = $writer->get_data($subcontexts); + $this->assertEquals("

Course 2 - Ass 2: " . $student2->id . "

", $submission->data1); + + // Verify the student assignment submission file upload exists. + $submissionfiles = $writer->get_files($subcontexts); + $this->assertTrue(array_key_exists('Student' . $student2->id . '-Course2-Ass2-(File 1 of 2)', $submissionfiles)); + $this->assertTrue(array_key_exists('Student' . $student2->id . '-Course2-Ass2-(File 2 of 2)', $submissionfiles)); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + * + * @throws dml_exception + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(true); + $this->create_courses_and_assignments(); + + // Test teacher1 delete_data_for_all_users_in_context(). + $teacher1 = $DB->get_record('user', ['username' => 'teacher1']); + $contextlist = provider::get_contexts_for_userid($teacher1->id); + + foreach ($contextlist as $context) { + provider::delete_data_for_all_users_in_context($context); + + // Verify assignment submission(s) were deleted for the context. + $deleted = $this->get_assignment_submissions($context->id); + $this->assertCount(0, $deleted); + + // Verify all the file submissions associated with the context for all users were deleted. + $files = $DB->get_records('files', ['component' => 'mod_assignment', 'filearea' => 'submission', 'contextid' => $context->id]); + $this->assertEquals(0, count($files)); + } + } + + /** + * Test for provider::delete_data_for_user(). + * + * @throws dml_exception + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(true); + $this->create_courses_and_assignments(); + + // Test Teacher 1 delete_data_for_user(), should only remove the 1 test submission added by Teacher 1. + // Should not remove any assignment submission records marked by the teacher. + $teacher1 = $DB->get_record('user', ['username' => 'teacher1']); + $contextlist = provider::get_contexts_for_userid($teacher1->id); + $approvedcontextlist = new approved_contextlist($teacher1, 'mod_assignment', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + // Verify the submissions submitted by students still exists. + $markedsubmissions = $DB->get_records('assignment_submissions', ['teacher' => $teacher1->id]); + $this->assertCount(4, $markedsubmissions); + + // Test student 1 delete_data_for_user(). + $student1 = $DB->get_record('user', ['username' => 'student1']); + $contextlist = provider::get_contexts_for_userid($student1->id); + $approvedcontextlist = new approved_contextlist($student1, 'mod_assignment', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + // Verify student 1's assignment submissions were deleted. + $assignmentsubmissions = $DB->get_records('assignment_submissions', ['userid' => $student1->id]); + $this->assertEquals(0, count($assignmentsubmissions)); + + // Verify student 1's file submissions were deleted. + foreach ($contextlist->get_contextids() as $contextid) { + $files = $DB->get_records('files', ['component' => 'mod_assignment', 'filearea' => 'submission', 'contextid' => $contextid]); + $this->assertEquals(0, count($files)); + } + } + + // Start of helper functions. + + /** + * Helper function to setup Course, users, and assignments for testing. + */ + protected function create_courses_and_assignments() { + // Create Courses, Users, and Assignments. + $course1 = $this->getDataGenerator()->create_course(['shortname' => 'course1']); + $course2 = $this->getDataGenerator()->create_course(['shortname' => 'course2']); + + $teacher1 = $this->getDataGenerator()->create_user(['username' => 'teacher1']); + $teacher2 = $this->getDataGenerator()->create_user(['username' => 'teacher2']); + + $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']); + $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']); + + $this->course1students = [ + $student1 + ]; + + $this->course2students = [ + $student2 + ]; + + $course1assignment1 = $this->getDataGenerator()->create_module('assignment', + [ + 'course' => $course1->id, + 'name' => 'Course 1 - Assignment 1 (onlinetext)', + 'assignmenttype' => 'onlinetext', + ] + ); + $course1assignment2 = $this->getDataGenerator()->create_module('assignment', + [ + 'course' => $course1->id, + 'name' => 'Course 1 - Assignment 2 (single file upload)', + 'assignmenttype' => 'uploadsingle', + ] + ); + $course2assignment1 = $this->getDataGenerator()->create_module('assignment', + [ + 'course' => $course2->id, + 'name' => 'Course 2 - Assignment 1 (offline)', + 'assignmenttype' => 'offline', + ] + ); + $course2assignment2 = $this->getDataGenerator()->create_module('assignment', + [ + 'course' => $course2->id, + 'name' => 'Course 2 - Assignment 2 (multiple file upload)', + 'assignmenttype' => 'upload', + ] + ); + + // Teacher 1 add test assignment submission for Course 1 - Assignment 1. + $this->add_assignment_submission( + $course1assignment1, + $teacher1, + "Course 1 - Ass 1: Teacher Test Submission" + ); + + // Student 1 add assignment submissions for Course 1 - Assignment 1 and 2. + $this->add_assignment_submission( + $course1assignment1, + $student1, + "Course 1 - Ass 1: " . $student1->id + ); + $this->add_file_assignment_submission( + $course1assignment2, + $student1, + "Course 1 - Ass 2: " . $student1->id, + 'Student' . $student1->id . '-Course1-Ass2' + ); + + // Student 2 add assignment submissions for Course 2 - Assignment 1 and 2. + $this->add_assignment_submission( + $course2assignment1, + $student2, + "Course 2 - Ass 1: " . $student2->id + ); + $this->add_file_assignment_submission( + $course2assignment2, + $student2, + "Course 2 - Ass 2: " . $student2->id, + 'Student' . $student2->id . '-Course2-Ass2', + 2 + ); + + // Teacher 1 to mark assignment submissions for Course 1's Assignment 1 and 2. + $course1submissions = $this->get_course_assignment_submissions($course1->id); + foreach ($course1submissions as $submission) { + $this->mark_assignment_submission($submission->assignment, $submission->id, $teacher1, 49); + } + + // Teacher 1 to mark assignment submissions for Course 2's Assignment 1 and 2. + $course2submissions = $this->get_course_assignment_submissions($course2->id); + foreach ($course2submissions as $submission) { + $this->mark_assignment_submission($submission->assignment, $submission->id, $teacher1, 50); + } + } + + /** + * Helper function to add an assignment submission for testing. + * + * @param object $assignment Object containing assignment submission details to create for testing. + * @param object $user Object of the user making the assignment submission. + * @param string $submissiondata The onlintext string value of the assignment submission. + * @throws dml_exception + */ + protected function add_assignment_submission($assignment, $user, $submissiondata) { + global $DB; + + $submission = (object) [ + 'assignment' => $assignment->id, + 'userid' => $user->id, + 'timecreated' => date('U'), + 'data1' => '

' . $submissiondata . '

', + 'submissioncomment' => 'My submission by ' . $user->username + ]; + + return $DB->insert_record('assignment_submissions', $submission); + } + + /** + * Helper function to add an assignment submission with file submissions for testing. + * + * @param object $assignment Object containing assignment submission details to create for testing. + * @param object $user Object of the user making the assignment submission. + * @param string $submissiondata The onlintext string value of the assignment submission. + * @param string $filename The filename of the file submission included with the assignment submission. + * @param int $numfiles The number of files included with the assignment submission. + * @throws dml_exception + * @throws file_exception + * @throws stored_file_creation_exception + */ + protected function add_file_assignment_submission($assignment, $user, $submissiondata, $filename, $numfiles = 1) { + global $CFG, $DB; + + $submission = (object) [ + 'assignment' => $assignment->id, + 'userid' => $user->id, + 'timecreated' => date('U'), + 'data1' => '

' . $submissiondata . '

', + 'numfiles' => $numfiles, + 'submissioncomment' => 'My submission by ' . $user->username + ]; + + $submissionid = $DB->insert_record('assignment_submissions', $submission); + + // Create a file submission with the test pdf. + $this->setUser($user->id); + $context = context_module::instance($assignment->cmid); + + $fs = get_file_storage(); + $sourcefile = $CFG->dirroot . '/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf'; + + for ($f = 1; $f <= $numfiles; $f++) { + $pdfsubmission = (object)array( + 'contextid' => $context->id, + 'component' => 'mod_assignment', + 'filearea' => 'submission', + 'itemid' => $submissionid, + 'filepath' => '/', + 'filename' => $filename . "-(File $f of $numfiles)" + ); + $fs->create_file_from_pathname($pdfsubmission, $sourcefile); + } + } + + /** + * Helper function to retrieve the assignment submission records for a given course. + * + * @param int $courseid The course ID to get assignment submissions by. + * @return array Array of assignment submission details. + * @throws dml_exception + */ + protected function get_course_assignment_submissions($courseid) { + global $DB; + + $sql = "SELECT s.id, + s.assignment, + s.userid, + s.timecreated, + s.timemodified, + s.numfiles, + s.data1, + s.data2, + s.grade, + s.submissioncomment, + s.format, + s.teacher, + s.timemarked, + s.mailed + FROM {assignment} a + JOIN {assignment_submissions} s ON s.assignment = a.id + WHERE a.course = :courseid"; + $params = [ + 'courseid' => $courseid + ]; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Helper function to update an assignment submission with grading details for a teacher. + * + * @param int $assignmentid The assignment ID to update assignment submissions with marking/graded details. + * @param int $submissionid The assignment submission ID to update with marking/grading details. + * @param int $teacher The teacher user ID to making the marking/grading details. + * @param int $gradedata The grade value set for the marking/grading details. + */ + protected function mark_assignment_submission($assignmentid, $submissionid, $teacher, $gradedata) { + global $DB; + + $submission = (object) [ + 'id' => $submissionid, + 'assignment' => $assignmentid, + 'grade' => $gradedata, + 'teacher' => $teacher->id, + 'timemarked' => date('U') + ]; + + return $DB->update_record('assignment_submissions', $submission); + } + + /** + * Helper function to retrieve the assignment records for a given context. + * + * @param int $contextid The context module ID value to retrieve assignment IDs by. + * @return array Array of assignment IDs. + * @throws dml_exception + */ + protected function get_assignments($contextid) { + global $DB; + + $sql = "SELECT a.id + FROM {assignment} a + JOIN {course_modules} cm ON a.id = cm.instance + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid"; + $params = [ + 'modulename' => 'assignment', + 'contextmodule' => CONTEXT_MODULE, + 'contextid' => $contextid + ]; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Helper function to retrieve the assignment submission records for a given context. + * + * @param int $contextid The context module ID value to retrieve assignment submission IDs by. + * @return array Array of assignment submission IDs. + * @throws dml_exception + */ + protected function get_assignment_submissions($contextid) { + global $DB; + + $sql = "SELECT s.id + FROM {assignment_submissions} s + JOIN {assignment} a ON a.id = s.assignment + JOIN {course_modules} cm ON a.id = cm.instance + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid"; + $params = [ + 'modulename' => 'assignment', + 'contextmodule' => CONTEXT_MODULE, + 'contextid' => $contextid + ]; + + return $DB->get_records_sql($sql, $params); + } +}