From fa83660e35c0b210c0f5b9bc6f7a9a2c26e5e250 Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Wed, 31 Jan 2018 14:20:17 +0800 Subject: [PATCH] MDL-61308 assign_feedback: Privacy code for user rights. --- .../privacy/assignfeedback_provider.php | 87 +++++++ .../privacy/feedback_legacy_polyfill.php | 100 +++++++ .../comments/classes/privacy/provider.php | 125 +++++++++ .../lang/en/assignfeedback_comments.php | 7 +- .../feedback/comments/tests/privacy_test.php | 203 +++++++++++++++ .../editpdf/classes/privacy/provider.php | 147 +++++++++++ .../lang/en/assignfeedback_editpdf.php | 7 + .../feedback/editpdf/tests/privacy_test.php | 244 ++++++++++++++++++ .../file/classes/privacy/provider.php | 140 ++++++++++ .../file/lang/en/assignfeedback_file.php | 2 + .../feedback/file/tests/privacy_test.php | 209 +++++++++++++++ .../offline/classes/privacy/provider.php | 47 ++++ .../lang/en/assignfeedback_offline.php | 1 + .../privacy_feedback_legacy_polyfill_test.php | 225 ++++++++++++++++ 14 files changed, 1542 insertions(+), 2 deletions(-) create mode 100644 mod/assign/classes/privacy/assignfeedback_provider.php create mode 100644 mod/assign/classes/privacy/feedback_legacy_polyfill.php create mode 100644 mod/assign/feedback/comments/classes/privacy/provider.php create mode 100644 mod/assign/feedback/comments/tests/privacy_test.php create mode 100644 mod/assign/feedback/editpdf/classes/privacy/provider.php create mode 100644 mod/assign/feedback/editpdf/tests/privacy_test.php create mode 100644 mod/assign/feedback/file/classes/privacy/provider.php create mode 100644 mod/assign/feedback/file/tests/privacy_test.php create mode 100644 mod/assign/feedback/offline/classes/privacy/provider.php create mode 100644 mod/assign/tests/privacy_feedback_legacy_polyfill_test.php diff --git a/mod/assign/classes/privacy/assignfeedback_provider.php b/mod/assign/classes/privacy/assignfeedback_provider.php new file mode 100644 index 0000000000000..c93852a28272c --- /dev/null +++ b/mod/assign/classes/privacy/assignfeedback_provider.php @@ -0,0 +1,87 @@ +. + +/** + * This file contains the assignfeedback_provider interface. + * + * Assignment Sub plugins should implement this if they store personal information. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_assign\privacy; + +use core_privacy\local\request\contextlist; + +defined('MOODLE_INTERNAL') || die(); + +interface assignfeedback_provider extends \core_privacy\local\request\plugin\subplugin_provider { + + /** + * Retrieves the contextids associated with the provided userid for this subplugin. + * NOTE if your subplugin must have an entry in the assign_grade table to work, then this + * method can be empty. + * + * @param int $userid The user ID to get context IDs for. + * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. + */ + public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist); + + /** + * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for + * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback() + * then you probably have to fill this in as well. + * + * @param useridlist $useridlist A list of user IDs of students graded by this user. + */ + public static function get_student_user_ids(useridlist $useridlist); + + /** + * Export feedback data with the available grade and userid information provided. + * assign_plugin_request_data contains: + * - context + * - grade object + * - current path (subcontext) + * - user object + * + * @param assign_plugin_request_data $exportdata Contains data to help export the user information. + */ + public static function export_feedback_user_data(assign_plugin_request_data $exportdata); + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * assign_plugin_request_data contains: + * - context + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function delete_feedback_for_context(assign_plugin_request_data $requestdata); + + /** + * Calling this function should delete all user data associated with this grade. + * assign_plugin_request_data contains: + * - context + * - grade object + * - user object + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata); +} \ No newline at end of file diff --git a/mod/assign/classes/privacy/feedback_legacy_polyfill.php b/mod/assign/classes/privacy/feedback_legacy_polyfill.php new file mode 100644 index 0000000000000..2fd6bdd69fa7d --- /dev/null +++ b/mod/assign/classes/privacy/feedback_legacy_polyfill.php @@ -0,0 +1,100 @@ +. + +/** + * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up. + * + * @package mod_assign + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_assign\privacy; + +use core_privacy\local\request\contextlist; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The trait used to provide backwards compatability for third-party plugins. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait feedback_legacy_polyfill { + + /** + * Retrieves the contextids associated with the provided userid for this subplugin. + * NOTE if your subplugin must have an entry in the assign_grade table to work, then this + * method can be empty. + * + * @param int $userid The user ID to get context IDs for. + * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. + */ + public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { + return static::_get_context_for_userid_within_feedback($userid, $contextlist); + } + + /** + * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for + * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback() + * then you probably have to fill this in as well. + * + * @param useridlist $useridlist A list of user IDs of students graded by this user. + */ + public static function get_student_user_ids(useridlist $useridlist) { + return static::_get_student_user_ids($useridlist); + } + + /** + * Export feedback data with the available grade and userid information provided. + * assign_plugin_request_data contains: + * - context + * - grade object + * - current path (subcontext) + * - user object + * + * @param assign_plugin_request_data $exportdata Contains data to help export the user information. + */ + public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { + return static::_export_feedback_user_data($exportdata); + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * assign_plugin_request_data contains: + * - context + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { + return static::_delete_feedback_for_context($requestdata); + } + + /** + * Calling this function should delete all user data associated with this grade. + * assign_plugin_request_data contains: + * - context + * - grade object + * - user object + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { + return static::_delete_feedback_for_grade($requestdata); + } +} diff --git a/mod/assign/feedback/comments/classes/privacy/provider.php b/mod/assign/feedback/comments/classes/privacy/provider.php new file mode 100644 index 0000000000000..ac9aa7fa58db9 --- /dev/null +++ b/mod/assign/feedback/comments/classes/privacy/provider.php @@ -0,0 +1,125 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_comments + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignfeedback_comments\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 \mod_assign\privacy\assignfeedback_provider; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\contextlist; +use \mod_assign\privacy\assign_plugin_request_data; +use \mod_assign\privacy\useridlist; + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_comments + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements metadataprovider, assignfeedback_provider { + + /** + * Return meta data about this plugin. + * + * @param collection $collection A list of information to add to. + * @return collection Return the collection after adding to it. + */ + public static function get_metadata(collection $collection) : collection { + $data = [ + 'assignment' => 'privacy:metadata:assignmentid', + 'grade' => 'privacy:metadata:gradepurpose', + 'commenttext' => 'privacy:metadata:commentpurpose' + ]; + $collection->add_database_table('assignfeedback_comments', $data, 'privacy:metadata:tablesummary'); + return $collection; + } + + /** + * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign + * provider. + * + * @param int $userid The user ID. + * @param contextlist $contextlist The context list. + */ + public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { + // This uses the assign_grades table. + } + + /** + * This also does not need to be filled in as this is already collected in the mod assign provider. + * + * @param useridlist $useridlist A list of user IDs + */ + public static function get_student_user_ids(useridlist $useridlist) { + // Not required. + } + + /** + * Export all user data for this plugin. + * + * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful + * information to help with exporting. + */ + public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { + // Get that comment information and jam it into that exporter. + $assign = $exportdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'comments'); + $comments = $plugin->get_feedback_comments($exportdata->get_pluginobject()->id); + if ($comments && !empty($comments->commenttext)) { + $data = (object)['commenttext' => format_text($comments->commenttext, $comments->commentformat, + ['context' => $exportdata->get_context()])]; + writer::with_context($exportdata->get_context()) + ->export_data(array_merge($exportdata->get_subcontext(), + [get_string('privacy:commentpath', 'assignfeedback_comments')]), $data); + } + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { + $assign = $requestdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'comments'); + $plugin->delete_instance(); + } + + /** + * Calling this function should delete all user data associated with this grade entry. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { + global $DB; + $DB->delete_records('assignfeedback_comments', ['assignment' => $requestdata->get_assign()->get_instance()->id, + 'grade' => $requestdata->get_pluginobject()->id]); + } +} diff --git a/mod/assign/feedback/comments/lang/en/assignfeedback_comments.php b/mod/assign/feedback/comments/lang/en/assignfeedback_comments.php index 525eee68609ae..52d868120d0ab 100644 --- a/mod/assign/feedback/comments/lang/en/assignfeedback_comments.php +++ b/mod/assign/feedback/comments/lang/en/assignfeedback_comments.php @@ -22,14 +22,17 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.'; $string['enabled'] = 'Feedback comments'; $string['enabled_help'] = 'If enabled, the marker can leave feedback comments for each submission. '; $string['pluginname'] = 'Feedback comments'; +$string['privacy:commentpath'] = 'Feedback comments'; +$string['privacy:metadata:assignmentid'] = 'Assignment identifier'; +$string['privacy:metadata:commentpurpose'] = 'The comment text.'; +$string['privacy:metadata:gradepurpose'] = 'The grade ID associated with the comment.'; +$string['privacy:metadata:tablesummary'] = 'This stores comments made by the graders as feedback for the student on their submission.'; $string['commentinline'] = 'Comment inline'; $string['commentinline_help'] = 'If enabled, the submission text will be copied into the feedback comment field during grading, making it easier to comment inline (using a different colour, perhaps) or to edit the original text.'; $string['commentinlinedefault'] = 'Comment inline by default'; $string['commentinlinedefault_help'] = 'If set, this comment inline functionality will be enabled by default for all new assignments.'; - diff --git a/mod/assign/feedback/comments/tests/privacy_test.php b/mod/assign/feedback/comments/tests/privacy_test.php new file mode 100644 index 0000000000000..36494523ac465 --- /dev/null +++ b/mod/assign/feedback/comments/tests/privacy_test.php @@ -0,0 +1,203 @@ +. + +/** + * Unit tests for assignfeedback_comments. + * + * @package assignfeedback_comments + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assign/locallib.php'); +require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php'); + +/** + * Unit tests for mod/assign/feedback/comments/classes/privacy/ + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class assignfeedback_comments_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase { + + /** + * Convenience function for creating feedback data. + * + * @param object $assign assign object + * @param stdClass $student user object + * @param stdClass $teacher user object + * @param string $submissiontext Submission text + * @param string $feedbacktext Feedback text + * @return array Feedback plugin object and the grade object. + */ + protected function create_feedback($assign, $student, $teacher, $submissiontext, $feedbacktext) { + $submission = new \stdClass(); + $submission->assignment = $assign->get_instance()->id; + $submission->userid = $student->id; + $submission->timecreated = time(); + $submission->onlinetext_editor = ['text' => $submissiontext, + 'format' => FORMAT_MOODLE]; + + $this->setUser($student); + $notices = []; + $assign->save_submission($submission, $notices); + + $grade = $assign->get_user_grade($student->id, true); + + $this->setUser($teacher); + + $plugin = $assign->get_feedback_plugin_by_type('comments'); + $feedbackdata = new \stdClass(); + $feedbackdata->assignfeedbackcomments_editor = [ + 'text' => $feedbacktext, + 'format' => 1 + ]; + + $plugin->save($grade, $feedbackdata); + return [$plugin, $grade]; + } + + /** + * Quick test to make sure that get_metadata returns something. + */ + public function test_get_metadata() { + $collection = new \core_privacy\local\metadata\collection('assignfeedback_comments'); + $collection = \assignfeedback_comments\privacy\provider::get_metadata($collection); + $this->assertNotEmpty($collection); + } + + /** + * Test that feedback comments are exported for a user. + */ + public function test_export_feedback_user_data() { + $this->resetAfterTest(); + + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Student. + $user1 = $this->getDataGenerator()->create_user(); + // Teacher. + $user2 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin, $grade) = $this->create_feedback($assign, $user1, $user2, 'Submission text', $feedbacktext); + + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + + // The student should be able to see the teachers feedback. + $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1); + \assignfeedback_comments\privacy\provider::export_feedback_user_data($exportdata); + $this->assertEquals($feedbacktext, $writer->get_data(['Feedback comments'])->commenttext); + + // The teacher should also be able to see the feedback that they provided. + $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user2); + \assignfeedback_comments\privacy\provider::export_feedback_user_data($exportdata); + $this->assertEquals($feedbacktext, $writer->get_data(['Feedback comments'])->commenttext); + } + + /** + * Test that all feedback is deleted for a context. + */ + public function test_delete_feedback_for_context() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Student. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext); + $feedbacktext = '

Comment for second student.

'; + list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext); + + // Check that we have data. + $feedbackcomments = $plugin1->get_feedback_comments($grade1->id); + $this->assertNotEmpty($feedbackcomments); + $feedbackcomments = $plugin1->get_feedback_comments($grade2->id); + $this->assertNotEmpty($feedbackcomments); + + // Delete all comments for this context. + $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign); + assignfeedback_comments\privacy\provider::delete_feedback_for_context($requestdata); + + // Check that the data is now gone. + $feedbackcomments = $plugin1->get_feedback_comments($grade1->id); + $this->assertEmpty($feedbackcomments); + $feedbackcomments = $plugin1->get_feedback_comments($grade2->id); + $this->assertEmpty($feedbackcomments); + } + + /** + * Test that a grade item is deleted for a user. + */ + public function test_delete_feedback_for_grade() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Student. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext); + $feedbacktext = '

Comment for second student.

'; + list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext); + + // Check that we have data. + $feedbackcomments = $plugin1->get_feedback_comments($grade1->id); + $this->assertNotEmpty($feedbackcomments); + $feedbackcomments = $plugin1->get_feedback_comments($grade2->id); + $this->assertNotEmpty($feedbackcomments); + + // Delete all comments for this grade object. + $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade1, [], $user1); + assignfeedback_comments\privacy\provider::delete_feedback_for_grade($requestdata); + + // These comments should be empty. + $feedbackcomments = $plugin1->get_feedback_comments($grade1->id); + $this->assertEmpty($feedbackcomments); + + // These comments should not. + $feedbackcomments = $plugin1->get_feedback_comments($grade2->id); + $this->assertNotEmpty($feedbackcomments); + } +} diff --git a/mod/assign/feedback/editpdf/classes/privacy/provider.php b/mod/assign/feedback/editpdf/classes/privacy/provider.php new file mode 100644 index 0000000000000..e61e781210123 --- /dev/null +++ b/mod/assign/feedback/editpdf/classes/privacy/provider.php @@ -0,0 +1,147 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_editpdf + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignfeedback_editpdf\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 \mod_assign\privacy\assignfeedback_provider; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\contextlist; +use \mod_assign\privacy\assign_plugin_request_data; +use \mod_assign\privacy\useridlist; + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_editpdf + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements metadataprovider, assignfeedback_provider { + + /** + * Return meta data about this plugin. + * + * @param collection $collection A list of information to add to. + * @return collection Return the collection after adding to it. + */ + public static function get_metadata(collection $collection) : collection { + $quickdata = [ + 'userid' => 'privacy:metadata:userid', + 'rawtext' => 'privacy:metadata:rawtextpurpose', + 'colour' => 'privacy:metadata:colourpurpose' + ]; + $collection->add_database_table('assignfeedback_editpdf_quick', $quickdata, 'privacy:metadata:tablepurpose'); + $collection->add_subsystem_link('core_files', [], 'privacy:metadata:filepurpose'); + $collection->add_subsystem_link('core_fileconverted', [], 'privacy:metadata:conversionpurpose'); + return $collection; + } + + /** + * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign + * provider. + * + * @param int $userid The user ID. + * @param contextlist $contextlist The context list. + */ + public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { + // This uses the assign_grade table. + } + + /** + * This also does not need to be filled in as this is already collected in the mod assign provider. + * + * @param useridlist $useridlist A list of user IDs + */ + public static function get_student_user_ids(useridlist $useridlist) { + // Not required. + } + + /** + * Export all user data for this plugin. + * + * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful + * information to help with exporting. + */ + public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { + $currentpath = $exportdata->get_subcontext(); + $currentpath[] = get_string('privacy:path', 'assignfeedback_editpdf'); + $assign = $exportdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); + $fileareas = $plugin->get_file_areas(); + $grade = $exportdata->get_pluginobject(); + foreach ($fileareas as $filearea => $notused) { + writer::with_context($exportdata->get_context()) + ->export_area_files($currentpath, 'assignfeedback_editpdf', $filearea, $grade->id); + } + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { + + $assign = $requestdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); + $fileareas = $plugin->get_file_areas(); + $fs = get_file_storage(); + foreach ($fileareas as $filearea => $notused) { + // Delete pdf files. + $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf', $filearea); + } + // Delete entries from the tables. + $plugin->delete_instance(); + } + + /** + * Calling this function should delete all user data associated with this grade. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { + global $DB; + + $assign = $requestdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); + $fileareas = $plugin->get_file_areas(); + $fs = get_file_storage(); + foreach ($fileareas as $filearea => $notused) { + // Delete pdf files. + $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf', + $filearea, $requestdata->get_pluginobject()->id); + } + + // Remove table entries. + $DB->delete_records('assignfeedback_editpdf_annot', ['gradeid' => $requestdata->get_pluginobject()->id]); + $DB->delete_records('assignfeedback_editpdf_cmnt', ['gradeid' => $requestdata->get_pluginobject()->id]); + // Submission records in assignfeedback_editpdf_queue will be cleaned up in a scheduled task + } +} diff --git a/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php b/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php index 6aec217dad218..115f490da515e 100644 --- a/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php +++ b/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php @@ -71,6 +71,13 @@ $string['pagexofy'] = 'Page {$a->page} of {$a->total}'; $string['pen'] = 'Pen'; $string['pluginname'] = 'Annotate PDF'; +$string['privacy:metadata:colourpurpose'] = 'Colour of the comment or annotation'; +$string['privacy:metadata:conversionpurpose'] = 'Files are converted to PDFs to allow for annotations.'; +$string['privacy:metadata:filepurpose'] = 'Stores an annotated PDF with feedback for the user.'; +$string['privacy:metadata:rawtextpurpose'] = 'Stores raw text for the quick data.'; +$string['privacy:metadata:tablepurpose'] = 'Stores teacher specified quicklist comments'; +$string['privacy:metadata:userid'] = 'An identifier for the user.'; +$string['privacy:path'] = 'PDF Feedback'; $string['generatingpdf'] = 'Generating the PDF...'; $string['rectangle'] = 'Rectangle'; $string['red'] = 'Red'; diff --git a/mod/assign/feedback/editpdf/tests/privacy_test.php b/mod/assign/feedback/editpdf/tests/privacy_test.php new file mode 100644 index 0000000000000..2291d6cd09fcc --- /dev/null +++ b/mod/assign/feedback/editpdf/tests/privacy_test.php @@ -0,0 +1,244 @@ +. + +/** + * Unit tests for assignfeedback_editpdf. + * + * @package assignfeedback_editpdf + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assign/locallib.php'); +require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php'); + +use \assignfeedback_editpdf\page_editor; +use \mod_assign\privacy\assign_plugin_request_data; + +/** + * Unit tests for mod/assign/feedback/editpdf/classes/privacy/ + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class assignfeedback_editpdf_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase { + + public function setUp() { + // Skip this test if ghostscript is not supported. + $result = \assignfeedback_editpdf\pdf::test_gs_path(false); + if ($result->status !== \assignfeedback_editpdf\pdf::GSPATH_OK) { + $this->markTestSkipped('Ghostscript not setup'); + return; + } + parent::setUp(); + } + + /** + * Convenience function for creating feedback data. + * + * @param object $assign assign object + * @param stdClass $student user object + * @param stdClass $teacher user object + * @return array Feedback plugin object and the grade object. + */ + protected function create_feedback($assign, $student, $teacher) { + global $CFG; + + // Create a file submission with the test pdf. + $submission = $assign->get_user_submission($student->id, true); + + $this->setUser($student->id); + + $fs = get_file_storage(); + $pdfsubmission = (object) array( + 'contextid' => $assign->get_context()->id, + 'component' => 'assignsubmission_file', + 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, + 'itemid' => $submission->id, + 'filepath' => '/', + 'filename' => 'submission.pdf' + ); + $sourcefile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf'; + $fi = $fs->create_file_from_pathname($pdfsubmission, $sourcefile); + + $data = new \stdClass(); + $plugin = $assign->get_submission_plugin_by_type('file'); + $plugin->save($submission, $data); + + $this->setUser($teacher->id); + + $plugin = $assign->get_feedback_plugin_by_type('editpdf'); + + $grade = $assign->get_user_grade($student->id, true); + + $comment = new \assignfeedback_editpdf\comment(); + + $comment->rawtext = 'Comment text'; + $comment->width = 100; + $comment->x = 100; + $comment->y = 100; + $comment->colour = 'red'; + page_editor::set_comments($grade->id, 0, [$comment]); + + $annotation = new \assignfeedback_editpdf\annotation(); + + $annotation->path = ''; + $annotation->x = 100; + $annotation->y = 100; + $annotation->endx = 200; + $annotation->endy = 200; + $annotation->type = 'line'; + $annotation->colour = 'red'; + + page_editor::set_annotations($grade->id, 0, [$annotation]); + + $comments = page_editor::get_comments($grade->id, 0, true); + $annotations = page_editor::get_annotations($grade->id, 0, false); + page_editor::release_drafts($grade->id); + $storedfile = \assignfeedback_editpdf\document_services::generate_feedback_document($assign->get_instance()->id, $student->id, + $grade->attemptnumber); + + return [$plugin, $grade, $storedfile]; + } + + /** + * Quick test to make sure that get_metadata returns something. + */ + public function test_get_metadata() { + $collection = new \core_privacy\local\metadata\collection('assignfeedback_editpdf'); + $collection = \assignfeedback_editpdf\privacy\provider::get_metadata($collection); + $this->assertNotEmpty($collection); + } + + /** + * Test that feedback comments are exported for a user. + */ + public function test_export_feedback_user_data() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Student. + $user1 = $this->getDataGenerator()->create_user(); + // Teacher. + $user2 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course, + 'assignsubmission_file_enabled' => 1, + 'assignsubmission_file_maxfiles' => 1, + 'assignfeedback_editpdf_enabled' => 1, + 'assignsubmission_file_maxsizebytes' => 1000000]); + + $context = $assign->get_context(); + + list($plugin, $grade, $storedfile) = $this->create_feedback($assign, $user1, $user2); + + // Check that we have data. + $this->assertFalse($plugin->is_empty($grade)); + + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + + // The student should be able to see the teachers feedback. + $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1); + \assignfeedback_editpdf\privacy\provider::export_feedback_user_data($exportdata); + // print_object($writer->get_files([get_string('privacy:path', 'assignfeedback_editpdf')])); + // print_object($writer->get_files(['PDF feedback', $storedfile->get_filename()])); + $pdffile = $writer->get_files([get_string('privacy:path', 'assignfeedback_editpdf')])[$storedfile->get_filename()]; + // The writer should have returned a stored file. + $this->assertInstanceOf('stored_file', $pdffile); + } + + /** + * Test that all feedback is deleted for a context. + */ + public function test_delete_feedback_for_context() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Students. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course, + 'assignsubmission_file_enabled' => 1, + 'assignsubmission_file_maxfiles' => 1, + 'assignfeedback_editpdf_enabled' => 1, + 'assignsubmission_file_maxsizebytes' => 1000000]); + + $context = $assign->get_context(); + + list($plugin1, $grade1, $storedfile1) = $this->create_feedback($assign, $user1, $user3); + list($plugin2, $grade2, $storedfile2) = $this->create_feedback($assign, $user2, $user3); + + // Check that we have data. + $this->assertFalse($plugin1->is_empty($grade1)); + $this->assertFalse($plugin2->is_empty($grade2)); + + $requestdata = new assign_plugin_request_data($context, $assign); + \assignfeedback_editpdf\privacy\provider::delete_feedback_for_context($requestdata); + + // Check that we now have no data. + $this->assertTrue($plugin1->is_empty($grade1)); + $this->assertTrue($plugin2->is_empty($grade2)); + } + + /** + * Test that a grade item is deleted for a user. + */ + public function test_delete_feedback_for_grade() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Students. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course, + 'assignsubmission_file_enabled' => 1, + 'assignsubmission_file_maxfiles' => 1, + 'assignfeedback_editpdf_enabled' => 1, + 'assignsubmission_file_maxsizebytes' => 1000000]); + + $context = $assign->get_context(); + + list($plugin1, $grade1, $storedfile1) = $this->create_feedback($assign, $user1, $user3); + list($plugin2, $grade2, $storedfile2) = $this->create_feedback($assign, $user2, $user3); + + // Check that we have data. + $this->assertFalse($plugin1->is_empty($grade1)); + $this->assertFalse($plugin2->is_empty($grade2)); + + $requestdata = new assign_plugin_request_data($context, $assign, $grade1, [], $user1); + \assignfeedback_editpdf\privacy\provider::delete_feedback_for_grade($requestdata); + + // Check that we now have no data for user 1. + $this->assertTrue($plugin1->is_empty($grade1)); + // Check that user 2 data is still there. + $this->assertFalse($plugin2->is_empty($grade2)); + } +} diff --git a/mod/assign/feedback/file/classes/privacy/provider.php b/mod/assign/feedback/file/classes/privacy/provider.php new file mode 100644 index 0000000000000..f2f609fd2ff7f --- /dev/null +++ b/mod/assign/feedback/file/classes/privacy/provider.php @@ -0,0 +1,140 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_file + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignfeedback_file\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 \mod_assign\privacy\assignfeedback_provider; +use \mod_assign\privacy\assign_plugin_request_data; +use mod_assign\privacy\useridlist; + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_file + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements metadataprovider, assignfeedback_provider { + + /** + * Return meta data about this plugin. + * + * @param collection $collection A list of information to add to. + * @return collection Return the collection after adding to it. + */ + public static function get_metadata(collection $collection) : collection { + $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); + return $collection; + } + + /** + * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign + * provider. + * + * @param int $userid The user ID. + * @param contextlist $contextlist The context list. + */ + public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { + // This uses the assign_grade table. + } + + /** + * This also does not need to be filled in as this is already collected in the mod assign provider. + * + * @param useridlist $useridlist A list of user IDs + */ + public static function get_student_user_ids(useridlist $useridlist) { + // Not required. + } + + /** + * Export all user data for this plugin. + * + * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful + * information to help with exporting. + */ + public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { + $currentpath = $exportdata->get_subcontext(); + $currentpath[] = get_string('privacy:path', 'assignfeedback_file'); + $assign = $exportdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'file'); + $gradeid = $exportdata->get_pluginobject()->id; + $filefeedback = $plugin->get_file_feedback($gradeid); + if ($filefeedback) { + $fileareas = $plugin->get_file_areas(); + foreach ($fileareas as $filearea => $notused) { + \core_privacy\local\request\writer::with_context($exportdata->get_context()) + ->export_area_files($currentpath, 'assignfeedback_file', $filearea, $gradeid); + } + } + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { + + $assign = $requestdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'file'); + $fileareas = $plugin->get_file_areas(); + $fs = get_file_storage(); + foreach ($fileareas as $filearea => $notused) { + // Delete feedback files. + $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_file', $filearea); + } + $plugin->delete_instance(); + } + + /** + * Calling this function should delete all user data associated with this grade. + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { + global $DB; + + $assign = $requestdata->get_assign(); + $plugin = $assign->get_plugin_by_type('assignfeedback', 'file'); + $fileareas = $plugin->get_file_areas(); + $fs = get_file_storage(); + foreach ($fileareas as $filearea => $notused) { + // Delete feedback files. + $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_file', $filearea, + $requestdata->get_pluginobject()->id); + } + + // Delete table entries. + $DB->delete_records('assignfeedback_file', ['assignment' => $requestdata->get_assign()->get_instance()->id, + 'grade' => $requestdata->get_pluginobject()->id]); + } +} diff --git a/mod/assign/feedback/file/lang/en/assignfeedback_file.php b/mod/assign/feedback/file/lang/en/assignfeedback_file.php index 20d4069c27764..3765b000f2d51 100644 --- a/mod/assign/feedback/file/lang/en/assignfeedback_file.php +++ b/mod/assign/feedback/file/lang/en/assignfeedback_file.php @@ -37,6 +37,8 @@ $string['feedbackfileupdated'] = 'Modified feedback file "{$a->filename}" for student "{$a->student}"'; $string['feedbackzip_help'] = 'A zip file containing a list of feedback files for one or more students. Feedback files will be assigned to students based on the participant id which should be the second part of each filename immediately after the users full name. This naming convention is used when downloading submissions so you can download all submissions, add comments to a few files and then rezip and upload all of the files. Files with no changes will be ignored.'; $string['file'] = 'Feedback files'; +$string['privacy:metadata:filepurpose'] = 'Feedback files from the teacher for the student.'; +$string['privacy:path'] = 'Feedback files'; $string['filesupdated'] = 'Feedback files updated: {$a}'; $string['filesadded'] = 'Feedback files added: {$a}'; $string['importfeedbackfiles'] = 'Import feedback file(s)'; diff --git a/mod/assign/feedback/file/tests/privacy_test.php b/mod/assign/feedback/file/tests/privacy_test.php new file mode 100644 index 0000000000000..5537755d86c6b --- /dev/null +++ b/mod/assign/feedback/file/tests/privacy_test.php @@ -0,0 +1,209 @@ +. + +/** + * Unit tests for assignfeedback_file. + * + * @package assignfeedback_file + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assign/locallib.php'); +require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php'); + +use mod_assign\privacy\assign_plugin_request_data; + +/** + * Unit tests for mod/assign/feedback/file/classes/privacy/ + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class assignfeedback_file_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase { + + /** + * Convenience function for creating feedback data. + * + * @param object $assign assign object + * @param stdClass $student user object + * @param stdClass $teacher user object + * @param string $submissiontext Submission text + * @param string $feedbacktext Feedback text + * @return array Feedback plugin object and the grade object. + */ + protected function create_feedback($assign, $student, $teacher, $submissiontext, $feedbacktext) { + + $submission = new \stdClass(); + $submission->assignment = $assign->get_instance()->id; + $submission->userid = $student->id; + $submission->timecreated = time(); + $submission->onlinetext_editor = ['text' => $submissiontext, + 'format' => FORMAT_MOODLE]; + + $this->setUser($student); + $notices = []; + $assign->save_submission($submission, $notices); + + $grade = $assign->get_user_grade($student->id, true); + + $this->setUser($teacher); + + $context = context_user::instance($teacher->id); + + $draftitemid = file_get_unused_draft_itemid(); + file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1); + + $dummy = array( + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftitemid, + 'filepath' => '/', + 'filename' => 'feedback1.txt' + ); + + $fs = get_file_storage(); + $file = $fs->create_file_from_string($dummy, $feedbacktext); + + // Create formdata. + $data = new stdClass(); + $data->{'files_' . $teacher->id . '_filemanager'} = $draftitemid; + + $plugin = $assign->get_feedback_plugin_by_type('file'); + // Save the feedback. + $plugin->save($grade, $data); + + return [$plugin, $grade]; + } + + /** + * Quick test to make sure that get_metadata returns something. + */ + public function test_get_metadata() { + $collection = new \core_privacy\local\metadata\collection('assignfeedback_file'); + $collection = \assignfeedback_file\privacy\provider::get_metadata($collection); + $this->assertNotEmpty($collection); + } + + /** + * Test that feedback comments are exported for a user. + */ + public function test_export_feedback_user_data() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Student. + $user1 = $this->getDataGenerator()->create_user(); + // Teacher. + $user2 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin, $grade) = $this->create_feedback($assign, $user1, $user2, 'Submission text', $feedbacktext); + + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + + // The student should be able to see the teachers feedback. + $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1); + \assignfeedback_file\privacy\provider::export_feedback_user_data($exportdata); + $feedbackfile = $writer->get_files([get_string('privacy:path', 'assignfeedback_file')])['feedback1.txt']; + // Check that we got a stored file. + $this->assertInstanceOf('stored_file', $feedbackfile); + $this->assertEquals('feedback1.txt', $feedbackfile->get_filename()); + } + + /** + * Test that all feedback is deleted for a context. + */ + public function test_delete_feedback_for_context() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Students. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext); + $feedbacktext = '

Comment for second submission.

'; + list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext); + + // Check that we have data. + $this->assertFalse($plugin1->is_empty($grade1)); + $this->assertFalse($plugin2->is_empty($grade2)); + + $requestdata = new assign_plugin_request_data($context, $assign); + \assignfeedback_file\privacy\provider::delete_feedback_for_context($requestdata); + + // Check that we now have no data. + $this->assertTrue($plugin1->is_empty($grade1)); + $this->assertTrue($plugin2->is_empty($grade2)); + } + + /** + * Test that a grade item is deleted for a user. + */ + public function test_delete_feedback_for_grade() { + $this->resetAfterTest(); + // Create course, assignment, submission, and then a feedback comment. + $course = $this->getDataGenerator()->create_course(); + // Students. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + // Teacher. + $user3 = $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($user3->id, $course->id, 'editingteacher'); + $assign = $this->create_instance(['course' => $course]); + + $context = $assign->get_context(); + + $feedbacktext = '

first comment for this test

'; + list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext); + $feedbacktext = '

Comment for second submission.

'; + list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext); + + // Check that we have data. + $this->assertFalse($plugin1->is_empty($grade1)); + $this->assertFalse($plugin2->is_empty($grade2)); + + $requestdata = new assign_plugin_request_data($context, $assign, $grade1, [], $user1); + \assignfeedback_file\privacy\provider::delete_feedback_for_grade($requestdata); + + // Check that we now have no data. + $this->assertTrue($plugin1->is_empty($grade1)); + // User 2's data should still be intact. + $this->assertFalse($plugin2->is_empty($grade2)); + } +} diff --git a/mod/assign/feedback/offline/classes/privacy/provider.php b/mod/assign/feedback/offline/classes/privacy/provider.php new file mode 100644 index 0000000000000..f3793adf2af91 --- /dev/null +++ b/mod/assign/feedback/offline/classes/privacy/provider.php @@ -0,0 +1,47 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_offline + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace assignfeedback_offline\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy class for requesting user data. + * + * @package assignfeedback_offline + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:nullproviderreason'; + } +} diff --git a/mod/assign/feedback/offline/lang/en/assignfeedback_offline.php b/mod/assign/feedback/offline/lang/en/assignfeedback_offline.php index aefde9525024e..d5722fffbcb1f 100644 --- a/mod/assign/feedback/offline/lang/en/assignfeedback_offline.php +++ b/mod/assign/feedback/offline/lang/en/assignfeedback_offline.php @@ -38,6 +38,7 @@ $string['invalidgradeimport'] = 'Moodle could not read the uploaded worksheet. Make sure it is saved in comma separated value format (.csv) and try again.'; $string['gradesfile'] = 'Grading worksheet (csv format)'; $string['gradesfile_help'] = 'Grading worksheet with modified grades. This file must be a csv file that has been downloaded from this assignment and must contain columns for the student grade, and identifier. The encoding for the file must be "UTF-8"'; +$string['privacy:nullproviderreason'] = 'This plugin has no database to store user information. It only uses APIs in mod_assign to help with displaying the grading interface.'; $string['nochanges'] = 'No modified grades found in uploaded worksheet'; $string['offlinegradingworksheet'] = 'Grades'; $string['pluginname'] = 'Offline grading worksheet'; diff --git a/mod/assign/tests/privacy_feedback_legacy_polyfill_test.php b/mod/assign/tests/privacy_feedback_legacy_polyfill_test.php new file mode 100644 index 0000000000000..94f3ebf0023cf --- /dev/null +++ b/mod/assign/tests/privacy_feedback_legacy_polyfill_test.php @@ -0,0 +1,225 @@ +. +/** + * Unit tests for the privacy legacy polyfill for mod_assign. + * + * @package mod_assign + * @category test + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php'); +require_once($CFG->dirroot . '/mod/assign/feedback/comments/locallib.php'); + +/** + * Unit tests for the assignment feedback subplugins API's privacy legacy_polyfill. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_assignfeedback_privacy_legacy_polyfill_test extends advanced_testcase { + + /** + * 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 the get_context_for_userid_within_feedback shim. + */ + public function test_get_context_for_userid_within_feedback() { + $userid = 21; + $contextlist = new \core_privacy\local\request\contextlist(); + $mock = $this->createMock(test_assignfeedback_legacy_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_get_context_for_userid_within_feedback', [$userid, $contextlist]); + test_legacy_polyfill_feedback_provider::$mock = $mock; + test_legacy_polyfill_feedback_provider::get_context_for_userid_within_feedback($userid, $contextlist); + } + + /** + * Test the get_student_user_ids shim. + */ + public function test_get_student_user_ids() { + $teacherid = 107; + $assignid = 15; + $useridlist = new \mod_assign\privacy\useridlist($teacherid, $assignid); + $mock = $this->createMock(test_assignfeedback_legacy_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_get_student_user_ids', [$useridlist]); + test_legacy_polyfill_feedback_provider::$mock = $mock; + test_legacy_polyfill_feedback_provider::get_student_user_ids($useridlist); + } + + /** + * Test the export_feedback_user_data shim. + */ + public function test_export_feedback_user_data() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $assign = $this->create_instance(['course' => $course]); + $context = context_system::instance(); + $subplugin = new assign_feedback_comments($assign, 'comments'); + $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context,$assign); + $mock = $this->createMock(test_assignfeedback_legacy_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_export_feedback_user_data', [$requestdata]); + test_legacy_polyfill_feedback_provider::$mock = $mock; + test_legacy_polyfill_feedback_provider::export_feedback_user_data($requestdata); + } + + /** + * Test the delete_feedback_for_context shim. + */ + public function test_delete_feedback_for_context() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $assign = $this->create_instance(['course' => $course]); + $context = context_system::instance(); + $subplugin = new assign_feedback_comments($assign, 'comments'); + $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context,$assign); + $mock = $this->createMock(test_assignfeedback_legacy_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_delete_feedback_for_context', [$requestdata]); + test_legacy_polyfill_feedback_provider::$mock = $mock; + test_legacy_polyfill_feedback_provider::delete_feedback_for_context($requestdata); + } + + /** + * Test the delete feedback for grade shim. + */ + public function test_delete_feedback_for_grade() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $assign = $this->create_instance(['course' => $course]); + $context = context_system::instance(); + $subplugin = new assign_feedback_comments($assign, 'comments'); + $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context,$assign); + $mock = $this->createMock(test_assignfeedback_legacy_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_delete_feedback_for_grade', [$requestdata]); + test_legacy_polyfill_feedback_provider::$mock = $mock; + test_legacy_polyfill_feedback_provider::delete_feedback_for_grade($requestdata); + } +} +/** + * Legacy polyfill test class for the assignfeedback_provider. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_legacy_polyfill_feedback_provider implements \mod_assign\privacy\assignfeedback_provider { + use \mod_assign\privacy\feedback_legacy_polyfill; + /** + * @var test_legacy_polyfill_feedback_provider $mock. + */ + public static $mock = null; + + /** + * Retrieves the contextids associated with the provided userid for this subplugin. + * NOTE if your subplugin must have an entry in the assign_grade table to work, then this + * method can be empty. + * + * @param int $userid The user ID to get context IDs for. + * @param contextlist $contextlist Use add_from_sql with this object to add your context IDs. + */ + public static function _get_context_for_userid_within_feedback(int $userid, + \core_privacy\local\request\contextlist $contextlist) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for + * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback() + * then you probably have to fill this in as well. + * + * @param useridlist $useridlist A list of user IDs of students graded by this user. + */ + public static function _get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Export feedback data with the available grade and userid information provided. + * assign_plugin_request_data contains: + * - context + * - grade object + * - current path (subcontext) + * - user object + * + * @param assign_plugin_request_data $exportdata Contains data to help export the user information. + */ + public static function _export_feedback_user_data(\mod_assign\privacy\assign_plugin_request_data $exportdata) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * assign_plugin_request_data contains: + * - context + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. + */ + public static function _delete_feedback_for_context(\mod_assign\privacy\assign_plugin_request_data $requestdata) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Calling this function should delete all user data associated with this grade. + * assign_plugin_request_data contains: + * - context + * - grade object + * - user object + * - assign object + * + * @param assign_plugin_request_data $requestdata Data useful for deleting user data. + */ + public static function _delete_feedback_for_grade(\mod_assign\privacy\assign_plugin_request_data $requestdata) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } +} +/** + * Called inside the polyfill methods in the test polyfill provider, allowing us to ensure these are called with correct params. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_assignfeedback_legacy_polyfill_mock_wrapper { + /** + * Get the return value for the specified item. + */ + public function get_return_value() { + } +}