diff --git a/mod/assign/classes/privacy/assign_plugin_request_data.php b/mod/assign/classes/privacy/assign_plugin_request_data.php new file mode 100644 index 00000000000..c93597234f3 --- /dev/null +++ b/mod/assign/classes/privacy/assign_plugin_request_data.php @@ -0,0 +1,116 @@ +. + +/** + * This file contains the mod_assign assign_plugin_request_data class + * + * For assign plugin privacy data to fulfill requests. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_assign\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An object for fulfilling an assign plugin data request. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class assign_plugin_request_data { + + /** @var context The context that we are dealing with. */ + protected $context; + + /** @var object For submisisons the submission object, for feedback the grade object. */ + protected $pluginobject; + + /** @var array The path or location that we are exporting data to. */ + protected $subcontext; + + /** @var object If set then only export data related directly to this user. */ + protected $user; + + /** @var assign The assign object */ + protected $assign; + + /** + * Object creator for assign plugin request data. + * + * @param \context $context Context object. + * @param \stdClass $pluginobject The grade object. + * @param array $subcontext Directory / file location. + * @param \stdClass $user The user object. + * @param \assign $assign The assign object. + */ + public function __construct(\context $context, \assign $assign, \stdClass $pluginobject = null, array $subcontext = [], + \stdClass $user = null) { + $this->context = $context; + $this->pluginobject = $pluginobject; + $this->subcontext = $subcontext; + $this->user = $user; + $this->assign = $assign; + } + + /** + * Getter for this attribute. + * + * @return context Context + */ + public function get_context() { + return $this->context; + } + + /** + * Getter for this attribute. + * + * @return object The assign plugin object + */ + public function get_pluginobject() { + return $this->pluginobject; + } + + /** + * Getter for this attribute. + * + * @return array The location (path) that this data is being writter to. + */ + public function get_subcontext() { + return $this->subcontext; + } + + /** + * Getter for this attribute. + * + * @return object The user id. If set then only information directly related to this user ID will be returned. + */ + public function get_user() { + return $this->user; + } + + /** + * Getter for this attribute. + * + * @return assign The assign object. + */ + public function get_assign() { + return $this->assign; + } +} diff --git a/mod/assign/classes/privacy/provider.php b/mod/assign/classes/privacy/provider.php new file mode 100644 index 00000000000..ccb895dc47a --- /dev/null +++ b/mod/assign/classes/privacy/provider.php @@ -0,0 +1,496 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_assign\privacy; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\metadata\provider as metadataprovider; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\plugin\provider as pluginprovider; +use \core_privacy\local\request\user_preference_provider as preference_provider; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\helper; +use \core_privacy\manager; + +/** + * Privacy class for requesting user data. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements metadataprovider, pluginprovider, preference_provider { + + /** Interface for all assign submission sub-plugins. */ + const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider'; + + /** Interface for all assign feedback sub-plugins. */ + const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider'; + + /** + * Provides meta data that is stored about a user with mod_assign + * + * @param collection $collection A collection of meta data items to be added to. + * @return collection Returns the collection of metadata. + */ + public static function get_metadata(collection $collection) : collection { + $assigngrades = [ + 'userid' => 'privacy:metadata:userid', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'timemodified', + 'grader' => 'privacy:metadata:grader', + 'grade' => 'privacy:metadata:grade', + 'attemptnumber' => 'attemptnumber' + ]; + $assignoverrides = [ + 'groupid' => 'privacy:metadata:groupid', + 'userid' => 'privacy:metadata:userid', + 'allowsubmissionsfromdate' => 'allowsubmissionsfromdate', + 'duedate' => 'duedate', + 'cutoffdate' => 'cutoffdate' + ]; + $assignsubmission = [ + 'userid' => 'privacy:metadata:userid', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'timemodified', + 'status' => 'gradingstatus', + 'groupid' => 'privacy:metadata:groupid', + 'attemptnumber' => 'attemptnumber', + 'latest' => 'privacy:metadata:latest' + ]; + $assignuserflags = [ + 'userid' => 'privacy:metadata:userid', + 'assignment' => 'privacy:metadata:assignmentid', + 'locked' => 'locksubmissions', + 'mailed' => 'privacy:metadata:mailed', + 'extensionduedate' => 'extensionduedate', + 'workflowstate' => 'markingworkflowstate', + 'allocatedmarker' => 'allocatedmarker' + ]; + $assignusermapping = [ + 'assignment' => 'privacy:metadata:assignmentid', + 'userid' => 'privacy:metadata:userid' + ]; + $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades'); + $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides'); + $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail'); + $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags'); + $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping'); + $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage'); + $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter'); + $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter'); + $collection->add_user_preference('assign_workflowfilter', 'privacy:metadata:assignworkflowfilter'); + $collection->add_user_preference('assign_quickgrading', 'privacy:metadata:assignquickgrading'); + $collection->add_user_preference('assign_downloadasfolders', 'privacy:metadata:assigndownloadasfolders'); + + // Link to subplugins. + $collection->add_plugintype_link('assignsubmission', [],'privacy:metadata:assignsubmissionpluginsummary'); + $collection->add_plugintype_link('assignfeedback', [], 'privacy:metadata:assignfeedbackpluginsummary'); + $collection->add_subsystem_link('core_message', [], 'privacy:metadata:assignmessageexplanation'); + + return $collection; + } + + /** + * Returns all of the contexts that has information relating to the userid. + * + * @param int $userid The user ID. + * @return contextlist an object with the contexts related to a userid. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $params = ['modulename' => 'assign', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + 'graderid' => $userid, + 'aouserid' => $userid, + 'asnuserid' => $userid, + 'aufuserid' => $userid, + 'aumuserid' => $userid]; + + $sql = "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {assign} a ON cm.instance = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + LEFT JOIN {assign_grades} ag ON a.id = ag.assignment + LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid + LEFT JOIN {assign_submission} asn ON a.id = asn.assignment + LEFT JOIN {assign_user_flags} auf ON a.id = auf.assignment + LEFT JOIN {assign_user_mapping} aum ON a.id = aum.assignment + WHERE ag.userid = :userid OR ag.grader = :graderid OR ao.userid = :aouserid + OR asn.userid = :asnuserid OR auf.userid = :aufuserid OR aum.userid = :aumuserid"; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, + 'get_context_for_userid_within_feedback', [$userid, $contextlist]); + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, + 'get_context_for_userid_within_submission', [$userid, $contextlist]); + + return $contextlist; + } + + /** + * Write out the user data filtered by contexts. + * + * @param approved_contextlist $contextlist contexts that we are writing data out from. + */ + public static function export_user_data(approved_contextlist $contextlist) { + foreach ($contextlist->get_contexts() as $context) { + // Check that the context is a module context. + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + $user = $contextlist->get_user(); + $assigndata = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + + writer::with_context($context)->export_data([], $assigndata); + $assign = new \assign($context, null, null); + + // I need to find out if I'm a student or a teacher. + if ($userids = self::get_graded_users($user->id, $assign)) { + // Return teacher info. + $currentpath = [get_string('privacy:studentpath', 'mod_assign')]; + foreach ($userids as $studentuserid) { + $studentpath = array_merge($currentpath, [$studentuserid->id]); + static::export_submission($assign, $studentuserid, $context, $studentpath, true); + } + } + + static::export_overrides($context, $assign, $user); + static::export_submission($assign, $user, $context, []); + // Meta data. + self::store_assign_user_flags($context, $assign, $user->id); + if ($assign->is_blind_marking()) { + $uniqueid = $assign->get_uniqueid_for_user_static($assign->get_instance()->id, $contextlist->get_user()->id); + if ($uniqueid) { + writer::with_context($context) + ->export_metadata([get_string('blindmarking', 'mod_assign')], 'blindmarkingid', $uniqueid, + get_string('privacy:blindmarkingidentifier', 'mod_assign')); + } + } + } + } + + /** + * Delete all use data which matches the specified context. + * + * @param context $context The module context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if ($context->contextlevel == CONTEXT_MODULE) { + // Apparently we can't trust anything that comes via the context. + // Go go mega query to find out it we have an assign context that matches an existing assignment. + $sql = "SELECT a.id + FROM {assign} 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' => 'assign', 'contextmodule' => CONTEXT_MODULE, 'contextid' => $context->id]; + $count = $DB->get_field_sql($sql, $params); + // If we have a count over zero then we can proceed. + if ($count > 0) { + // Get the assignment related to this context. + $assign = new \assign($context, null, null); + // What to do first... Get sub plugins to delete their stuff. + $requestdata = new assign_plugin_request_data($context, $assign); + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, + 'delete_submission_for_context', [$requestdata]); + $requestdata = new assign_plugin_request_data($context, $assign); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, + 'delete_feedback_for_context', [$requestdata]); + $DB->delete_records('assign_grades', ['assignment' => $assign->get_instance()->id]); + + // Time to roll my own method for deleting overrides. + static::delete_user_overrides($assign); + $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]); + $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]); + $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]); + } + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + + foreach ($contextlist as $context) { + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + // Get the assign object. + $assign = new \assign($context, null, null); + $assignid = $assign->get_instance()->id; + + $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]); + foreach ($submissions as $submission) { + $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user); + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, + 'delete_submission_for_userid', [$requestdata]); + } + + $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]); + foreach ($grades as $grade) { + $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, + 'delete_feedback_for_grade', [$requestdata]); + } + + static::delete_user_overrides($assign, $user); + $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]); + $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]); + $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]); + $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]); + } + } + + /** + * Deletes assignment overrides. + * + * @param \assign $assign The assignment object + * @param \stdClass $user The user object if we are deleting only the overrides for one user. + */ + protected static function delete_user_overrides(\assign $assign, \stdClass $user = null) { + global $DB; + + $assignid = $assign->get_instance()->id; + $params = (isset($user)) ? ['assignid' => $assignid, 'userid' => $user->id] : ['assignid' => $assignid]; + + $overrides = $DB->get_records('assign_overrides', $params); + if (!empty($overrides)) { + foreach ($overrides as $override) { + + // First delete calendar events associated with this override. + $conditions = ['modulename' => 'assign', 'instance' => $assignid]; + if (isset($user)) { + $conditions['userid'] = $user->id; + } + $DB->delete_records('event', $conditions); + + // Next delete the overrides. + $DB->delete_records('assign_overrides', ['id' => $override->id]); + } + } + } + + /** + * Find out if this user has graded any users. + * + * @param int $userid The user ID (potential teacher). + * @param assign $assign The assignment object. + * @return array If successful an array of objects with userids that this user graded, otherwise false. + */ + protected static function get_graded_users(int $userid, \assign $assign) { + $params = ['grader' => $userid, 'assignid' => $assign->get_instance()->id]; + + $sql = "SELECT DISTINCT userid AS id + FROM {assign_grades} + WHERE grader = :grader AND assignment = :assignid"; + + $useridlist = new useridlist($userid, $assign->get_instance()->id); + $useridlist->add_from_sql($sql, $params); + + // Call sub-plugins to see if they have information not already collected. + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_student_user_ids', + [$useridlist]); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_student_user_ids', [$useridlist]); + + $userids = $useridlist->get_userids(); + return ($userids) ? $userids : false; + } + + /** + * Writes out various user meta data about the assignment. + * + * @param \context $context The context of this assignment. + * @param \assign $assign The assignment object. + * @param int $userid The user ID + */ + protected static function store_assign_user_flags(\context $context, \assign $assign, int $userid) { + $datatypes = ['locked' => get_string('locksubmissions', 'mod_assign'), + 'mailed' => get_string('privacy:metadata:mailed', 'mod_assign'), + 'extensionduedate' => get_string('extensionduedate', 'mod_assign'), + 'workflowstate' => get_string('markingworkflowstate', 'mod_assign'), + 'allocatedmarker' => get_string('allocatedmarker_help', 'mod_assign')]; + $userflags = (array)$assign->get_user_flags($userid, false); + + foreach ($datatypes as $key => $description) { + if (isset($userflags[$key]) && !empty($userflags[$key])) { + $value = $userflags[$key]; + if ($key == 'locked' || $key == 'mailed') { + $value = transform::yesno($value); + } else if ($key == 'extensionduedate') { + $value = transform::datetime($value); + } + writer::with_context($context)->export_metadata([], $key, $value, $description); + } + } + } + + /** + * Formats and then exports the user's grade data. + * + * @param \stdClass $grade The assign grade object + * @param \context $context The context object + * @param array $currentpath Current directory path that we are exporting to. + */ + protected static function export_grade_data(\stdClass $grade, \context $context, array $currentpath) { + $gradedata = (object)[ + 'timecreated' => transform::datetime($grade->timecreated), + 'timemodified' => transform::datetime($grade->timemodified), + 'grader' => transform::user($grade->grader), + 'grade' => $grade->grade, + 'attemptnumber' => $grade->attemptnumber + ]; + writer::with_context($context) + ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata); + } + + /** + * Formats and then exports the user's submission data. + * + * @param \stdClass $submission The assign submission object + * @param \context $context The context object + * @param array $currentpath Current directory path that we are exporting to. + */ + protected static function export_submission_data(\stdClass $submission, \context $context, array $currentpath) { + $submissiondata = (object)[ + 'timecreated' => transform::datetime($submission->timecreated), + 'timemodified' => transform::datetime($submission->timemodified), + 'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'), + 'groupid' => $submission->groupid, + 'attemptnumber' => $submission->attemptnumber, + 'latest' => transform::yesno($submission->latest) + ]; + writer::with_context($context) + ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_assign')]), $submissiondata); + } + + /** + * 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(); + $assignpreferences = [ + 'assign_perpage' => ['string' => get_string('privacy:metadata:assignperpage', 'mod_assign'), 'bool' => false], + 'assign_filter' => ['string' => get_string('privacy:metadata:assignfilter', 'mod_assign'), 'bool' => false], + 'assign_markerfilter' => ['string' => get_string('privacy:metadata:assignmarkerfilter', 'mod_assign'), 'bool' => true], + 'assign_workflowfilter' => ['string' => get_string('privacy:metadata:assignworkflowfilter', 'mod_assign'), + 'bool' => true], + 'assign_quickgrading' => ['string' => get_string('privacy:metadata:assignquickgrading', 'mod_assign'), 'bool' => true], + 'assign_downloadasfolders' => ['string' => get_string('privacy:metadata:assigndownloadasfolders', 'mod_assign'), + 'bool' => true] + ]; + foreach ($assignpreferences 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_assign', $key, $value, $preference['string']); + } + } + } + + /** + * Export overrides for this assignment. + * + * @param \context $context Context + * @param \assign $assign The assign object. + * @param \stdClass $user The user object. + */ + public static function export_overrides(\context $context, \assign $assign, \stdClass $user) { + + $overrides = $assign->override_exists($user->id); + // Overrides returns an array with data in it, but an override with actual data will have the assign ID set. + if (isset($overrides->assignid)) { + $data = new \stdClass(); + if (!empty($overrides->duedate)) { + $data->duedate = transform::datetime($overrides->duedate); + } + if (!empty($overrides->cutoffdate)) { + $overrides->cutoffdate = transform::datetime($overrides->cutoffdate); + } + if (!empty($overrides->allowsubmissionsfromdate)) { + $overrides->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate); + } + if (!empty($data)) { + writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data); + } + } + } + + /** + * Exports assignment submission data for a user. + * + * @param \assign $assign The assignment object + * @param \stdClass $user The user object + * @param \context_module $context The context + * @param array $path The path for exporting data + * @param bool|boolean $exportforteacher A flag for if this is exporting data as a teacher. + */ + protected static function export_submission(\assign $assign, \stdClass $user, \context_module $context, array $path, + bool $exportforteacher = false) { + $submissions = $assign->get_all_submissions($user->id); + $teacher = ($exportforteacher) ? $user : null; + foreach ($submissions as $submission) { + // Attempt numbers start at zero, which is fine for programming, but doesn't make as much sense + // for users. + $submissionpath = array_merge($path, + [get_string('privacy:attemptpath', 'mod_assign', ($submission->attemptnumber + 1))]); + + $params = new assign_plugin_request_data($context, $assign, $submission, $submissionpath ,$teacher); + manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, + 'export_submission_user_data', [$params]); + if (!isset($teacher)) { + self::export_submission_data($submission, $context, $submissionpath); + } + $grade = $assign->get_user_grade($user->id, false, $submission->attemptnumber); + if ($grade) { + $params = new assign_plugin_request_data($context, $assign, $grade, $submissionpath, $teacher); + manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'export_feedback_user_data', + [$params]); + + self::export_grade_data($grade, $context, $submissionpath); + } + } + } +} diff --git a/mod/assign/classes/privacy/useridlist.php b/mod/assign/classes/privacy/useridlist.php new file mode 100644 index 00000000000..430d6eb4495 --- /dev/null +++ b/mod/assign/classes/privacy/useridlist.php @@ -0,0 +1,99 @@ +. + +/** + * This file contains the mod_assign useridlist + * + * This is for collecting a list of user IDs + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_assign\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An object for collecting user IDs related to a teacher. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class useridlist { + + /** @var int The ID of the teacher. */ + protected $teacherid; + + /** @var int The ID of the assignment object. */ + protected $assignid; + + /** @var array A collection of user IDs (students). */ + protected $userids = []; + + /** + * Create this object. + * + * @param int $teacherid The teacher ID. + * @param int $assignid The assignment ID. + */ + public function __construct($teacherid, $assignid) { + $this->teacherid = $teacherid; + $this->assignid = $assignid; + } + + /** + * Returns the teacher ID. + * + * @return int The teacher ID. + */ + public function get_teacherid() { + return $this->teacherid; + } + + /** + * Returns the assign ID. + * + * @return int The assign ID. + */ + public function get_assignid() { + return $this->assignid; + } + + /** + * Returns the user IDs. + * + * @return array User IDs. + */ + public function get_userids() { + return $this->userids; + } + + /** + * Add sql and params to return user IDs. + * + * @param string $sql The sql string to return user IDs. + * @param array $params Parameters for the sql statement. + */ + public function add_from_sql($sql, $params) { + global $DB; + $userids = $DB->get_records_sql($sql, $params); + if (!empty($userids)) { + $this->userids = array_merge($this->userids, $userids); + } + } +} diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 9ae84d1a731..8e606c769b7 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -79,8 +79,8 @@ $string['assignmentsperpage'] = 'Assignments per page'; $string['assignsubmission'] = 'Submission plugin'; $string['assignsubmissionpluginname'] = 'Submission plugin'; $string['attemptheading'] = 'Attempt {$a->attemptnumber}: {$a->submissionsummary}'; -$string['attemptnumber'] = 'Attempt number'; $string['attempthistory'] = 'Previous attempts'; +$string['attemptnumber'] = 'Attempt number'; $string['attemptsettings'] = 'Attempt settings'; $string['attemptreopenmethod'] = 'Attempts reopened'; $string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are:
  • Never - The student submission cannot be reopened.
  • Manually - The student submission can be reopened by a teacher.
  • Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Gradebook setup section) for this assignment.
'; @@ -245,8 +245,8 @@ $string['filternone'] = 'No filter'; $string['filternotsubmitted'] = 'Not submitted'; $string['filterrequiregrading'] = 'Requires grading'; $string['filtersubmitted'] = 'Submitted'; -$string['gradedby'] = 'Graded by'; $string['graded'] = 'Graded'; +$string['gradedby'] = 'Graded by'; $string['gradedon'] = 'Graded on'; $string['gradebelowzero'] = 'Grade must be greater than or equal to zero.'; $string['gradeabovemaximum'] = 'Grade must be less than or equal to {$a}.'; @@ -387,6 +387,32 @@ $string['preventsubmissionnotingroup_help'] = 'If enabled, users who are not mem $string['preventsubmissions'] = 'Prevent the user from making any more submissions to this assignment.'; $string['preventsubmissionsshort'] = 'Prevent submission changes'; $string['previous'] = 'Previous'; +$string['privacy:attemptpath'] = 'attempt {$a}'; +$string['privacy:blindmarkingidentifier'] = 'The identifier used for blind marking.'; +$string['privacy:gradepath'] = 'grade'; +$string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders'; +$string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.'; +$string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\''; +$string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment'; +$string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.'; +$string['privacy:metadata:assignmentid'] = 'Assignment identifier.'; +$string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.'; +$string['privacy:metadata:assignoverrides'] = 'Stores override information for the assignment'; +$string['privacy:metadata:assignperpage'] = 'Number of assignments shown per page.'; +$string['privacy:metadata:assignquickgrading'] = 'A preference as to whether quick grading is used or not.'; +$string['privacy:metadata:assignsubmissiondetail'] = 'Stores user submission information'; +$string['privacy:metadata:assignsubmissionpluginsummary'] = 'Submission data for the assignment.'; +$string['privacy:metadata:assignuserflags'] = 'Stores user meta data such as extension dates'; +$string['privacy:metadata:assignusermapping'] = 'The mapping for blind marking'; +$string['privacy:metadata:assignworkflowfilter'] = 'Filter by the different workflow stages.'; +$string['privacy:metadata:grade'] = 'The numerical grade for this assignment submission. Can be determined by scales/advancedgradingforms etc but will always be converted back to a floating point number.'; +$string['privacy:metadata:grader'] = 'The user ID of the person grading.'; +$string['privacy:metadata:groupid'] = 'Group ID that the user is a member of.'; +$string['privacy:metadata:latest'] = 'Greatly simplifies queries wanting to know information about only the latest attempt.'; +$string['privacy:metadata:mailed'] = 'Has this user been mailed yet?'; +$string['privacy:metadata:timecreated'] = 'Time created'; +$string['privacy:metadata:userid'] = 'Identifier for the user.'; +$string['privacy:studentpath'] = 'studentsubmissions'; $string['quickgrading'] = 'Quick grading'; $string['quickgradingresult'] = 'Quick grading'; $string['quickgradingchangessaved'] = 'The grade changes were saved'; @@ -454,6 +480,7 @@ $string['submissionlog'] = 'Student: {$a->fullname}, Status: {$a->status}'; $string['submissionnotcopiedinvalidstatus'] = 'The submission was not copied because it has been edited since it was reopened.'; $string['submissionnoteditable'] = 'Student cannot edit this submission'; $string['submissionnotready'] = 'This assignment is not ready to submit:'; +$string['privacy:submissionpath'] = 'submission'; $string['submissionplugins'] = 'Submission plugins'; $string['submissionreceipts'] = 'Send submission receipts'; $string['submissionreceiptothertext'] = 'Your assignment submission for diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 830531ea5c6..eb6523324ae 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -5287,7 +5287,7 @@ class assign { * @param int $userid If not set, $USER->id will be used. * @return array $submissions All submission records for this user (or group). */ - protected function get_all_submissions($userid) { + public function get_all_submissions($userid) { global $DB, $USER; // If the userid is not null then use userid. diff --git a/mod/assign/tests/privacy_test.php b/mod/assign/tests/privacy_test.php new file mode 100644 index 00000000000..a9800bc0ba1 --- /dev/null +++ b/mod/assign/tests/privacy_test.php @@ -0,0 +1,562 @@ +. + +/** + * Base class for unit tests for mod_assign. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_assign\tests; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +use \core_privacy\tests\provider_testcase; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \mod_assign\privacy\provider; + +/** + * Unit tests for mod/assign/classes/privacy/ + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_assign_privacy_testcase extends provider_testcase { + + /** + * Convenience method for creating a submission. + * + * @param assign $assign The assign object + * @param stdClass $user The user object + * @param string $submissiontext Submission text + * @param integer $attemptnumber The attempt number + * @return object A submission object. + */ + protected function create_submission($assign, $user, $submissiontext, $attemptnumber = 0) { + $submission = $assign->get_user_submission($user->id, true, $attemptnumber); + $submission->onlinetext_editor = ['text' => $submissiontext, + 'format' => FORMAT_MOODLE]; + + $this->setUser($user); + $notices = []; + $assign->save_submission($submission, $notices); + return $submission; + } + + /** + * Convenience function to create an instance of an assignment. + * + * @param array $params Array of parameters to pass to the generator + * @return assign The assign class. + */ + protected function create_instance($params = array()) { + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = \context_module::instance($cm->id); + return new \assign($context, $cm, $params['course']); + } + + /** + * Test that getting the contexts for a user works. + */ + public function test_get_contexts_for_userid() { + global $DB; + $this->resetAfterTest(); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + + $user1 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); + $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 'student'); + // Need a second user to create content in other assignments. + $user2 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student'); + + // Create multiple assignments. + // Assignment with a text submission. + $assign1 = $this->create_instance(['course' => $course1]); + // Assignment two in a different course that the user is not enrolled in. + $assign2 = $this->create_instance(['course' => $course2]); + // Assignment three has an entry in the override table. + $assign3 = $this->create_instance(['course' => $course3, 'cutoffdate' => time()]); + // Assignment four - blind marking. + $assign4 = $this->create_instance(['course' => $course1, 'blindmarking' => 1]); + // Assignment five - user flags. + $assign5 = $this->create_instance(['course' => $course3]); + + // Override has to be manually inserted into the DB. + $overridedata = new \stdClass(); + $overridedata->assignid = $assign3->get_instance()->id; + $overridedata->userid = $user1->id; + $overridedata->duedate = time(); + $DB->insert_record('assign_overrides', $overridedata); + // Assign unique id for blind marking in assignment four for user 1. + \assign::get_uniqueid_for_user_static($assign4->get_instance()->id, $user1->id); + // Create an entry in the user flags table. + $assign5->get_user_flags($user1->id, true); + + // The user will be in these contexts. + $usercontextids = [ + $assign1->get_context()->id, + $assign3->get_context()->id, + $assign4->get_context()->id, + $assign5->get_context()->id, + ]; + + $submission = new \stdClass(); + $submission->assignment = $assign1->get_instance()->id; + $submission->userid = $user1->id; + $submission->timecreated = time(); + $submission->onlinetext_editor = ['text' => 'Submission text', + 'format' => FORMAT_MOODLE]; + + $this->setUser($user1); + $notices = []; + $assign1->save_submission($submission, $notices); + + // Create a submission for the second assignment. + $submission->assignment = $assign2->get_instance()->id; + $submission->userid = $user2->id; + $this->setUser($user2); + $assign2->save_submission($submission, $notices); + + $contextlist = provider::get_contexts_for_userid($user1->id); + $this->assertEquals(count($usercontextids), count($contextlist->get_contextids())); + // There should be no difference between the contexts. + $this->assertEmpty(array_diff($usercontextids, $contextlist->get_contextids())); + } + + /** + * Test that a student with multiple submissions and grades is returned with the correct data. + */ + public function test_export_user_data_student() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + $assign = $this->create_instance([ + 'course' => $course, + 'name' => 'Assign 1', + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'maxattempts' => 3, + 'assignsubmission_onlinetext_enabled' => true, + 'assignfeedback_comments_enabled' => true + ]); + + $context = $assign->get_context(); + // Create some submissions (multiple attempts) for a student. + $submissiontext = 'My first submission'; + $submission = $this->create_submission($assign, $user, $submissiontext); + + $this->setUser($teacher); + + $grade1 = '67.00'; + $teachercommenttext = 'Please try again.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade1; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user->id, $data); + + $submissiontext2 = 'My second submission'; + $submission = $this->create_submission($assign, $user, $submissiontext2, 1); + + $this->setUser($teacher); + + $grade2 = '72.00'; + $teachercommenttext2 = 'This is better. Thanks.'; + $data = new \stdClass(); + $data->attemptnumber = 1; + $data->grade = $grade2; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext2, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user->id, $data); + + $writer = writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + + // The student should have some text submitted. + // Add the course context as well to make sure there is no error. + $approvedlist = new approved_contextlist($user, 'mod_assign', [$context->id, $coursecontext->id]); + provider::export_user_data($approvedlist); + + // Check that we have general details about the assignment. + $this->assertEquals('Assign 1', $writer->get_data()->name); + // Check Submissions. + $this->assertEquals($submissiontext, $writer->get_data(['attempt 1', 'Submission Text'])->text); + $this->assertEquals($submissiontext2, $writer->get_data(['attempt 2', 'Submission Text'])->text); + $this->assertEquals(0, $writer->get_data(['attempt 1', 'submission'])->attemptnumber); + $this->assertEquals(1, $writer->get_data(['attempt 2', 'submission'])->attemptnumber); + // Check grades. + $this->assertEquals($grade1, $writer->get_data(['attempt 1', 'grade'])->grade); + $this->assertEquals($grade2, $writer->get_data(['attempt 2', 'grade'])->grade); + // Check feedback. + $this->assertContains($teachercommenttext, $writer->get_data(['attempt 1', 'Feedback comments'])->commenttext); + $this->assertContains($teachercommenttext2, $writer->get_data(['attempt 2', 'Feedback comments'])->commenttext); + } + + /** + * Tests the data returned for a teacher. + */ + public function test_export_user_data_teacher() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + $assign = $this->create_instance([ + 'course' => $course, + 'name' => 'Assign 1', + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'maxattempts' => 3, + 'assignsubmission_onlinetext_enabled' => true, + 'assignfeedback_comments_enabled' => true + ]); + + $context = $assign->get_context(); + + // Create and grade some submissions from the students. + $submissiontext = 'My first submission'; + $submission = $this->create_submission($assign, $user1, $submissiontext); + + $this->setUser($teacher); + + $grade1 = '54.00'; + $teachercommenttext = 'Comment on user 1 attempt 1.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade1; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user1->id, $data); + + // Create and grade some submissions from the students. + $submissiontext2 = 'My first submission for user 2'; + $submission = $this->create_submission($assign, $user2, $submissiontext2); + + $this->setUser($teacher); + + $grade2 = '56.00'; + $teachercommenttext2 = 'Comment on user 2 first attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade2; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext2, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Create and grade some submissions from the students. + $submissiontext3 = 'My second submission for user 2'; + $submission = $this->create_submission($assign, $user2, $submissiontext3, 1); + + $this->setUser($teacher); + + $grade3 = '83.00'; + $teachercommenttext3 = 'Comment on user 2 another attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 1; + $data->grade = $grade3; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext3, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Set up some flags. + $duedate = time(); + $flagdata = $assign->get_user_flags($teacher->id, true); + $flagdata->mailed = 1; + $flagdata->extensionduedate = $duedate; + $assign->update_user_flags($flagdata); + + $writer = writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + + // The student should have some text submitted. + $approvedlist = new approved_contextlist($teacher, 'mod_assign', [$context->id, $coursecontext->id]); + provider::export_user_data($approvedlist); + + // Check flag metadata. + $metadata = $writer->get_all_metadata(); + $this->assertEquals(\core_privacy\local\request\transform::yesno(1), $metadata['mailed']->value); + $this->assertEquals(\core_privacy\local\request\transform::datetime($duedate), $metadata['extensionduedate']->value); + + // Check for student grades given. + $student1grade = $writer->get_data(['studentsubmissions', $user1->id, 'attempt 1', 'grade']); + $this->assertEquals($grade1, $student1grade->grade); + $student2grade1 = $writer->get_data(['studentsubmissions', $user2->id, 'attempt 1', 'grade']); + $this->assertEquals($grade2, $student2grade1->grade); + $student2grade2 = $writer->get_data(['studentsubmissions', $user2->id, 'attempt 2', 'grade']); + $this->assertEquals($grade3, $student2grade2->grade); + // Check for feedback given to students. + $this->assertContains($teachercommenttext, $writer->get_data(['studentsubmissions', $user1->id, 'attempt 1', + 'Feedback comments'])->commenttext); + $this->assertContains($teachercommenttext2, $writer->get_data(['studentsubmissions', $user2->id, 'attempt 1', + 'Feedback comments'])->commenttext); + $this->assertContains($teachercommenttext3, $writer->get_data(['studentsubmissions', $user2->id, 'attempt 2', + 'Feedback comments'])->commenttext); + } + + /** + * A test for deleting all user data for a given context. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + $assign = $this->create_instance([ + 'course' => $course, + 'name' => 'Assign 1', + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'maxattempts' => 3, + 'assignsubmission_onlinetext_enabled' => true, + 'assignfeedback_comments_enabled' => true + ]); + + $context = $assign->get_context(); + + // Create and grade some submissions from the students. + $submissiontext = 'My first submission'; + $submission = $this->create_submission($assign, $user1, $submissiontext); + + $this->setUser($teacher); + + // Overrides for both students. + $overridedata = new \stdClass(); + $overridedata->assignid = $assign->get_instance()->id; + $overridedata->userid = $user1->id; + $overridedata->duedate = time(); + $DB->insert_record('assign_overrides', $overridedata); + $overridedata->userid = $user2->id; + $DB->insert_record('assign_overrides', $overridedata); + assign_update_events($assign); + + $grade1 = '54.00'; + $teachercommenttext = 'Comment on user 1 attempt 1.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade1; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user1->id, $data); + + // Create and grade some submissions from the students. + $submissiontext2 = 'My first submission for user 2'; + $submission = $this->create_submission($assign, $user2, $submissiontext2); + + $this->setUser($teacher); + + $grade2 = '56.00'; + $teachercommenttext2 = 'Comment on user 2 first attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade2; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext2, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Create and grade some submissions from the students. + $submissiontext3 = 'My second submission for user 2'; + $submission = $this->create_submission($assign, $user2, $submissiontext3, 1); + + $this->setUser($teacher); + + $grade3 = '83.00'; + $teachercommenttext3 = 'Comment on user 2 another attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 1; + $data->grade = $grade3; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext3, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Delete all user data for this assignment. + provider::delete_data_for_all_users_in_context($context); + + // Check all relevant tables. + $records = $DB->get_records('assign_submission'); + $this->assertEmpty($records); + $records = $DB->get_records('assign_grades'); + $this->assertEmpty($records); + $records = $DB->get_records('assignsubmission_onlinetext'); + $this->assertEmpty($records); + $records = $DB->get_records('assignfeedback_comments'); + $this->assertEmpty($records); + + // Check that overrides and the calendar events are deleted. + $records = $DB->get_records('event'); + $this->assertEmpty($records); + $records = $DB->get_records('assign_overrides'); + $this->assertEmpty($records); + } + + /** + * A test for deleting all user data for one user. + */ + public function test_delete_data_for_user() { + global $DB; + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + + $coursecontext = \context_course::instance($course->id); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + $assign = $this->create_instance([ + 'course' => $course, + 'name' => 'Assign 1', + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'maxattempts' => 3, + 'assignsubmission_onlinetext_enabled' => true, + 'assignfeedback_comments_enabled' => true + ]); + + $context = $assign->get_context(); + + // Create and grade some submissions from the students. + $submissiontext = 'My first submission'; + $submission1 = $this->create_submission($assign, $user1, $submissiontext); + + $this->setUser($teacher); + + // Overrides for both students. + $overridedata = new \stdClass(); + $overridedata->assignid = $assign->get_instance()->id; + $overridedata->userid = $user1->id; + $overridedata->duedate = time(); + $DB->insert_record('assign_overrides', $overridedata); + $overridedata->userid = $user2->id; + $DB->insert_record('assign_overrides', $overridedata); + assign_update_events($assign); + + $grade1 = '54.00'; + $teachercommenttext = 'Comment on user 1 attempt 1.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade1; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user1->id, $data); + + // Create and grade some submissions from the students. + $submissiontext2 = 'My first submission for user 2'; + $submission2 = $this->create_submission($assign, $user2, $submissiontext2); + + $this->setUser($teacher); + + $grade2 = '56.00'; + $teachercommenttext2 = 'Comment on user 2 first attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 0; + $data->grade = $grade2; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext2, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Create and grade some submissions from the students. + $submissiontext3 = 'My second submission for user 2'; + $submission3 = $this->create_submission($assign, $user2, $submissiontext3, 1); + + $this->setUser($teacher); + + $grade3 = '83.00'; + $teachercommenttext3 = 'Comment on user 2 another attempt.'; + $data = new \stdClass(); + $data->attemptnumber = 1; + $data->grade = $grade3; + $data->assignfeedbackcomments_editor = ['text' => $teachercommenttext3, 'format' => FORMAT_MOODLE]; + + // Give the submission a grade. + $assign->save_grade($user2->id, $data); + + // Delete user 2's data. + $approvedlist = new approved_contextlist($user2, 'mod_assign', [$context->id, $coursecontext->id]); + provider::delete_data_for_user($approvedlist); + + // Check all relevant tables. + $records = $DB->get_records('assign_submission'); + foreach ($records as $record) { + $this->assertEquals($user1->id, $record->userid); + $this->assertNotEquals($user2->id, $record->userid); + } + $records = $DB->get_records('assign_grades'); + foreach ($records as $record) { + $this->assertEquals($user1->id, $record->userid); + $this->assertNotEquals($user2->id, $record->userid); + } + $records = $DB->get_records('assignsubmission_onlinetext'); + $this->assertCount(1, $records); + $record = array_shift($records); + // The only submission is for user 1. + $this->assertEquals($submission1->id, $record->submission); + $records = $DB->get_records('assignfeedback_comments'); + $this->assertCount(1, $records); + $record = array_shift($records); + // The only record is the feedback comment for user 1. + $this->assertEquals($teachercommenttext, $record->commenttext); + + // Check calendar events as well as assign overrides. + $records = $DB->get_records('event'); + $this->assertCount(1, $records); + $record = array_shift($records); + // The remaining event should be for user 1. + $this->assertEquals($user1->id, $record->userid); + // Now for assign_overrides + $records = $DB->get_records('assign_overrides'); + $this->assertCount(1, $records); + $record = array_shift($records); + // The remaining event should be for user 1. + $this->assertEquals($user1->id, $record->userid); + } +}