Merge branch 'MDL-61959-master' of git://github.com/sarjona/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 30 Apr 2018 02:16:23 +0000 (10:16 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 30 Apr 2018 02:16:23 +0000 (10:16 +0800)
131 files changed:
admin/tool/cohortroles/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/cohortroles/tests/privacy_test.php [new file with mode: 0644]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/tests/privacy_provider_test.php [new file with mode: 0644]
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/policy/lang/en/tool_policy.php
auth/email/classes/external.php
blocks/activity_modules/lang/en/block_activity_modules.php
blocks/activity_results/lang/en/block_activity_results.php
blocks/badges/lang/en/block_badges.php
blocks/calendar_upcoming/lang/en/block_calendar_upcoming.php
blocks/course_summary/lang/en/block_course_summary.php
blocks/login/lang/en/block_login.php
blocks/myprofile/lang/en/block_myprofile.php
blocks/private_files/lang/en/block_private_files.php
blocks/tag_youtube/lang/en/block_tag_youtube.php
calendar/type/gregorian/lang/en/calendartype_gregorian.php
cohort/classes/privacy/provider.php [new file with mode: 0644]
cohort/tests/privacy_test.php [new file with mode: 0644]
course/lib.php
dataformat/csv/lang/en/dataformat_csv.php
dataformat/excel/lang/en/dataformat_excel.php
dataformat/html/lang/en/dataformat_html.php
dataformat/json/lang/en/dataformat_json.php
dataformat/ods/lang/en/dataformat_ods.php
enrol/manual/classes/enrol_users_form.php
enrol/manual/lang/en/enrol_manual.php
enrol/paypal/lang/en/enrol_paypal.php
enrol/self/lang/en/enrol_self.php
lang/en/admin.php
lang/en/analytics.php
lang/en/badges.php
lang/en/blog.php
lang/en/cohort.php
lang/en/editor.php
lang/en/moodle.php
lang/en/my.php
lang/en/notes.php
lang/en/rss.php
lang/en/tag.php
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/textarea/lang/en/editor_textarea.php
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/editor/tinymce/plugins/pdw/lang/en/tinymce_pdw.php
lib/form/templates/filetypes-descriptions.mustache
lib/tests/component_test.php
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/src/notification/js/confirm.js
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/jabber/lang/en/message_jabber.php
mod/assign/classes/privacy/assign_plugin_request_data.php [new file with mode: 0644]
mod/assign/classes/privacy/assignfeedback_provider.php [new file with mode: 0644]
mod/assign/classes/privacy/assignsubmission_provider.php [new file with mode: 0644]
mod/assign/classes/privacy/feedback_legacy_polyfill.php [new file with mode: 0644]
mod/assign/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/classes/privacy/submission_legacy_polyfill.php [new file with mode: 0644]
mod/assign/classes/privacy/useridlist.php [new file with mode: 0644]
mod/assign/feedback/comments/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/comments/lang/en/assignfeedback_comments.php
mod/assign/feedback/comments/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/file/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/file/lang/en/assignfeedback_file.php
mod/assign/feedback/file/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/offline/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/offline/lang/en/assignfeedback_offline.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/submission/comments/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/comments/lang/en/assignsubmission_comments.php
mod/assign/submission/comments/tests/privacy_test.php [new file with mode: 0644]
mod/assign/submission/file/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/tests/privacy_test.php [new file with mode: 0644]
mod/assign/submission/onlinetext/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/onlinetext/lang/en/assignsubmission_onlinetext.php
mod/assign/submission/onlinetext/tests/privacy_test.php [new file with mode: 0644]
mod/assign/tests/privacy_feedback_legacy_polyfill_test.php [new file with mode: 0644]
mod/assign/tests/privacy_submission_legacy_polyfill_test.php [new file with mode: 0644]
mod/assign/tests/privacy_test.php [new file with mode: 0644]
mod/choice/classes/external.php
mod/choice/lang/en/choice.php
mod/choice/tests/externallib_test.php
mod/feedback/lang/en/feedback.php
mod/folder/lang/en/folder.php
mod/forum/tests/behat/edit_post_student.feature
mod/label/lang/en/label.php
mod/lesson/classes/privacy/provider.php [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/tests/privacy_test.php [new file with mode: 0644]
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php
mod/page/lang/en/page.php
mod/resource/lang/en/resource.php
mod/survey/lang/en/survey.php
mod/url/lang/en/url.php
my/classes/privacy/provider.php [new file with mode: 0644]
my/tests/privacy_test.php [new file with mode: 0644]
notes/classes/privacy/provider.php [new file with mode: 0644]
notes/tests/privacy_test.php [new file with mode: 0644]
question/classes/output/qbank_chooser.php
repository/flickr_public/lang/en/repository_flickr_public.php
repository/youtube/lang/en/repository_youtube.php
rss/classes/privacy/provider.php
theme/boost/scss/moodle/bootstrap-rtl.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/templates/core/action_menu.mustache
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
user/profile/field/checkbox/classes/privacy/provider.php [new file with mode: 0644]
user/profile/field/checkbox/lang/en/profilefield_checkbox.php
user/profile/field/checkbox/tests/privacy_test.php [new file with mode: 0644]
version.php

diff --git a/admin/tool/cohortroles/classes/privacy/provider.php b/admin/tool/cohortroles/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..bc38e7c
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for tool_cohortroles.
+ *
+ * @package    tool_cohortroles
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\context;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tool_cohortroles implementing metadata and plugin providers.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param   collection $collection The initialised collection to add items to.
+     * @return  collection     A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        // The tool_cohortroles plugin utilises the mdl_tool_cohortroles table.
+        $collection->add_database_table(
+            'tool_cohortroles',
+            [
+                'id' => 'privacy:metadata:tool_cohortroles:id',
+                'cohortid' => 'privacy:metadata:tool_cohortroles:cohortid',
+                'roleid' => 'privacy:metadata:tool_cohortroles:roleid',
+                'userid' => 'privacy:metadata:tool_cohortroles:userid',
+                'timecreated' => 'privacy:metadata:tool_cohortroles:timecreated',
+                'timemodified' => 'privacy:metadata:tool_cohortroles:timemodified',
+                'usermodified' => 'privacy:metadata:tool_cohortroles:usermodified'
+            ],
+            'privacy:metadata:tool_cohortroles'
+        );
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param   int $userid The user to search.
+     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $contextlist = new contextlist();
+
+        // Retrieve the User context associated with tool_cohortroles records.
+        $sql = "SELECT DISTINCT c.id
+                  FROM {context} c
+                  JOIN {tool_cohortroles} cr ON cr.userid = c.instanceid AND c.contextlevel = :contextuser
+                 WHERE cr.userid = :userid";
+
+        $params = [
+            'contextuser' => CONTEXT_USER,
+            'userid'       => $userid
+        ];
+
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param   approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // If the user has tool_cohortroles data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Retrieve the tool_cohortroles records created for the user.
+        $sql = 'SELECT cr.id as cohortroleid,
+                       c.name as cohortname,
+                       c.idnumber as cohortidnumber,
+                       c.description as cohortdescription,
+                       r.shortname as roleshortname,
+                       cr.userid as userid,
+                       cr.timecreated as timecreated,
+                       cr.timemodified as timemodified
+                  FROM {tool_cohortroles} cr
+                  JOIN {cohort} c ON c.id = cr.cohortid
+                  JOIN {role} r ON r.id = cr.roleid
+                 WHERE cr.userid = :userid';
+
+        $params = [
+            'userid' => $userid
+        ];
+
+        $cohortroles = $DB->get_records_sql($sql, $params);
+        foreach ($cohortroles as $cohortrole) {
+            // The tool_cohortroles data export is organised in:
+            // {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json.
+            $subcontext = [
+                get_string('pluginname', 'tool_cohortroles'),
+                $cohortrole->cohortname,
+                $cohortrole->roleshortname
+            ];
+
+            $data = (object) [
+                'cohortname' => $cohortrole->cohortname,
+                'cohortidnumber' => $cohortrole->cohortidnumber,
+                'cohortdescription' => $cohortrole->cohortdescription,
+                'roleshortname' => $cohortrole->roleshortname,
+                'userid' => transform::user($cohortrole->userid),
+                'timecreated' => transform::datetime($cohortrole->timecreated),
+                'timemodified' => transform::datetime($cohortrole->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $data);
+        }
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param   context $context The specific context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Delete the tool_cohortroles records created for the userid.
+        $DB->delete_records('tool_cohortroles', ['userid' => $userid]);
+    }
+
+    /**
+     * 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;
+
+        // If the user has tool_cohortroles data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Delete the tool_cohortroles records created for the userid.
+        $DB->delete_records('tool_cohortroles', ['userid' => $userid]);
+    }
+
+}
index 08f00c9..a8afe83 100644 (file)
@@ -42,3 +42,11 @@ $string['selectrole'] = 'Select role';
 $string['selectusers'] = 'Select users to assign role';
 $string['taskname'] = 'Sync cohort role assignments';
 $string['thisuserroles'] = 'Roles assigned relative to this user';
+$string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
+$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
+$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
+$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
+$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
+$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort  role mapping was created.';
+$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
+$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
diff --git a/admin/tool/cohortroles/tests/privacy_test.php b/admin/tool/cohortroles/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..f118b74
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the tool_cohortroles implementation of the privacy API.
+ *
+ * @package    tool_cohortroles
+ * @category   test
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\approved_contextlist;
+use \tool_cohortroles\api;
+use \tool_cohortroles\privacy\provider;
+
+/**
+ * Unit tests for the tool_cohortroles implementation of the privacy API.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Overriding setUp() function to always reset after tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+        global $DB;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 3.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_user_data() {
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Retrieve the User's tool_cohortroles data.
+        $approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
+        provider::export_user_data($approvedcontextlist);
+
+        // Test the tool_cohortroles data is exported at the User context level.
+        $writer = writer::with_context($context);
+        $this->assertTrue($writer->has_any_data());
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 4;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 4.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Delete all the User's records in mdl_tool_cohortroles table by the specified User context.
+        provider::delete_data_for_all_users_in_context($context);
+
+        // Test the cohort roles records in mdl_tool_cohortroles table is equals zero.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount(0, $cohortroles);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 4;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 4.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Delete all the User's records in mdl_tool_cohortroles table by the specified User approved context list.
+        $approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // Test the records in mdl_tool_cohortroles table is equals zero.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount(0, $cohortroles);
+    }
+
+    /**
+     * Helper function to setup tool_cohortroles records for testing a specific user.
+     *
+     * @param int $userid           The ID of the user used for testing.
+     * @param int $nocohortroles    The number of tool_cohortroles to create for the user.
+     * @throws \core_competency\invalid_persistent_exception
+     * @throws coding_exception
+     */
+    protected function setup_test_scenario_data($userid, $nocohortroles) {
+        $roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
+
+        for ($c = 0; $c < $nocohortroles; $c++) {
+            $cohort = $this->getDataGenerator()->create_cohort();
+
+            $params = (object)array(
+                'userid' => $userid,
+                'roleid' => $roleid,
+                'cohortid' => $cohort->id
+            );
+
+            api::create_cohort_role_assignment($params);
+        }
+    }
+
+}
index 3132033..999ba5e 100644 (file)
@@ -100,7 +100,7 @@ $string['expiry'] = 'Expiry';
 $string['expandplugin'] = 'Expand and collapse plugin.';
 $string['expandplugintype'] = 'Expand and collapse plugin type.';
 $string['explanationtitle'] = 'Icons used on this page and what they mean.';
-$string['external'] = 'External';
+$string['external'] = 'Additional';
 $string['externalexplanation'] = 'An additional plugin installed on this site.';
 $string['frontpagecourse'] = 'Front page course';
 $string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
@@ -168,7 +168,7 @@ $string['privacy:metadata:request'] = 'Information from personal data requests (
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s respective privacy officer regarding the request.';
+$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
index c8271f3..b074060 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, you can use the [server]:[port], for example mail.example.com:587 format. If you leave this field blank, Moodle will use the default port for the type of mail server you specify.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
@@ -45,7 +45,7 @@ $string['invalid_recipient_handler'] = 'If a valid message is received but the s
 This handler processes those replies.
 
 It is not possible to disable sender verification of this handler because the user may reply from an incorrect email address if their email client configuration is incorrect.';
-$string['invalid_recipient_handler_name'] = 'Invalid recipient handler';
+$string['invalid_recipient_handler_name'] = 'Invalid sender handler';
 $string['invalidrecipientdescription'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
 $string['invalidrecipientdescriptionhtml'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
 $string['invalidrecipientfinal'] = 'The message "{$a->subject}" could not be authenticated. Please check that you are sending your message from the same email address as in your profile.';
index 9ef98e1..43c8464 100644 (file)
@@ -277,6 +277,7 @@ class api {
             $settings->tool_mobile_customlangstrings = get_config('tool_mobile', 'customlangstrings');
             $settings->tool_mobile_disabledfeatures = get_config('tool_mobile', 'disabledfeatures');
             $settings->tool_mobile_custommenuitems = get_config('tool_mobile', 'custommenuitems');
+            $settings->tool_mobile_apppolicy = get_config('tool_mobile', 'apppolicy');
         }
 
         return $settings;
diff --git a/admin/tool/mobile/classes/privacy/provider.php b/admin/tool/mobile/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..c29c0a1
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for tool_mobile.
+ *
+ * @package    tool_mobile
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_mobile\privacy;
+defined('MOODLE_INTERNAL') || die();
+use \core_privacy\local\request\writer;
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+
+/**
+ * Privacy provider for tool_mobile.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\user_preference_provider,
+    \core_privacy\local\request\plugin\provider {
+    /**
+     * Returns meta data about this system.
+     *
+     * @param   collection     $collection The initialised item collection to add items to.
+     * @return  collection     A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        // There is a one user preference.
+        $collection->add_user_preference('tool_mobile_autologin_request_last',
+            'privacy:metadata:preference:tool_mobile_autologin_request_last');
+        $collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:core_userkey');
+
+        return $collection;
+    }
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $sql = "SELECT ctx.id
+                  FROM {user_private_key} k
+                  JOIN {user} u ON k.userid = u.id
+                  JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
+                 WHERE k.userid = :userid AND k.script = 'tool_mobile'";
+        $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        // If the user has data, then only the CONTEXT_USER should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        // Export associated userkeys.
+        \core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile');
+    }
+    /**
+     * Export all user preferences for the plugin.
+     *
+     * @param   int         $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $autologinrequestlast = get_user_preferences('tool_mobile_autologin_request_last', null, $userid);
+        if ($autologinrequestlast !== null) {
+            $time = transform::datetime($autologinrequestlast);
+            writer::export_user_preference('tool_mobile',
+                'tool_mobile_autologin_request_last',
+                $time,
+                get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile')
+            );
+        }
+    }
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+        // Delete all the userkeys.
+        \core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
+    }
+    /**
+     * 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) {
+        // If the user has data, then only the user context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+        // Delete all the userkeys.
+        \core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
+    }
+}
\ No newline at end of file
index 1693ba5..195af12 100644 (file)
@@ -25,6 +25,8 @@
 $string['adodbdebugwarning'] = 'ADOdb debugging is enabled. It should be disabled in the external database authentication or external database enrolment plugin settings.';
 $string['androidappid'] = 'Android app\'s unique identifier';
 $string['androidappid_desc'] = 'This setting may be left as default unless you have a custom Android app.';
+$string['apppolicy'] = 'App policy URL';
+$string['apppolicy_help'] = 'The URL of a policy for app users which is listed on the About page in the app. If the field is left empty, the site policy URL will be used instead.';
 $string['autologinkeygenerationlockout'] = 'Auto-login key generation is blocked. You need to wait 6 minutes between requests.';
 $string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed for site admins.';
 $string['cachedef_plugininfo'] = 'This stores the list of plugins with mobile addons';
@@ -65,7 +67,7 @@ $string['invalidcertificatechainwarning'] = 'It seems that the certificate chain
 $string['invalidcertificateexpiredatewarning'] = 'It seems that the HTTPS certificate for the site has expired.';
 $string['invalidcertificatestartdatewarning'] = 'It seems that the HTTPS certificate for the site is not yet valid (with a start date in the future).';
 $string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
-$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site policies.';
+$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site security settings.';
 $string['iosappid'] = 'iOS app\'s unique identifier';
 $string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
 $string['loginintheapp'] = 'Via the app';
@@ -92,3 +94,5 @@ $string['smartappbanners'] = 'App Banners';
 $string['typeoflogin'] = 'Type of login';
 $string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins.';
 $string['getmoodleonyourmobile'] = 'Get the mobile app';
+$string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
+$string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
index 8af64c6..125a72a 100644 (file)
@@ -43,6 +43,9 @@ if ($hassiteconfig) {
                 new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
     }
 
+    $temp->add(new admin_setting_configtext('tool_mobile/apppolicy', new lang_string('apppolicy', 'tool_mobile'),
+        new lang_string('apppolicy_help', 'tool_mobile'), '', PARAM_URL));
+
     $ADMIN->add('mobileapp', $temp);
 
     // Show only mobile settings if the mobile service is enabled.
index 0b8873b..d5a8a34 100644 (file)
@@ -173,6 +173,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
             array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
             array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
+            array('name' => 'tool_mobile_apppolicy', 'value' => ''),
         );
         $this->assertCount(0, $result['warnings']);
         $this->assertEquals($expected, $result['settings']);
diff --git a/admin/tool/mobile/tests/privacy_provider_test.php b/admin/tool/mobile/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..d553a10
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Base class for unit tests for tool_mobile.
+ *
+ * @package    tool_mobile
+ * @category   test
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\approved_contextlist;
+use \tool_mobile\privacy\provider;
+
+/**
+ * Unit tests for the tool_mobile implementation of the privacy API.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_mobile_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test to check export_user_preferences.
+     * returns user preferences data.
+     */
+    public function test_export_user_preferences() {
+        $user = $this->getDataGenerator()->create_user();
+        $expectedtime = time();
+        set_user_preference('tool_mobile_autologin_request_last', time(), $user);
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $prefs = $writer->get_user_preferences('tool_mobile');
+        $time = transform::datetime($expectedtime);
+        $this->assertEquals($time, $prefs->tool_mobile_autologin_request_last->value);
+        $this->assertEquals(get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile'),
+            $prefs->tool_mobile_autologin_request_last->description);
+    }
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $key = get_user_key('tool_mobile', $user->id);
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $this->assertEquals($context->id, $contextlist->current()->id);
+    }
+    /**
+     * Test that data is exported correctly for this plugin.
+     */
+    public function test_export_user_data() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Validate exported data.
+        $this->setUser($user);
+        $writer = writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $context, 'tool_mobile');
+        $userkeydata = $writer->get_related_data([], 'userkeys');
+        $this->assertCount(1, $userkeydata->keys);
+        $this->assertEquals($key->script, reset($userkeydata->keys)->script);
+    }
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Before deletion, we should have 1 user_private_key.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(1, $count);
+        // Delete data.
+        provider::delete_data_for_all_users_in_context($context);
+        // After deletion, the user_private_key entries should have been deleted.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(0, $count);
+    }
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Before deletion, we should have 1 user_private_key.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(1, $count);
+        // Delete data.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $approvedcontextlist = new approved_contextlist($user, 'tool_mobile', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+        // After deletion, the user_private_key entries should have been deleted.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(0, $count);
+    }
+}
\ No newline at end of file
index 81d20bb..01b18fd 100644 (file)
@@ -97,9 +97,9 @@ $string['privacy:metadata:timecreatedsub'] = 'When this subscription was created
 $string['privacy:metadata:timemodifiedrule'] = 'When this rule was last modified';
 $string['privacy:metadata:timesent'] = 'When the message was sent';
 $string['privacy:metadata:timewindow'] = 'Time window in seconds';
-$string['privacy:metadata:userid'] = 'Id of user who created the rule';
-$string['privacy:metadata:useridhistory'] = 'User to whom this notification was sent';
-$string['privacy:metadata:useridsub'] = 'User id of the subscriber';
+$string['privacy:metadata:userid'] = 'The ID of user who created the rule.';
+$string['privacy:metadata:useridhistory'] = 'The ID of the user to whom this notification was sent';
+$string['privacy:metadata:useridsub'] = 'The ID of the subscriber.';
 $string['privacy:subscriptions'] = 'My event monitor subscriptions';
 $string['processevents'] = 'Process events';
 $string['rulename'] = 'Rule name';
index d218563..5febbbe 100644 (file)
@@ -118,7 +118,7 @@ $string['policyversionacceptedinotherlang'] = 'This policy version has been agre
 $string['previousversions'] = '{$a} previous versions';
 $string['privacy:metadata:acceptances'] = 'Information from policies agreements made by the users of this site.';
 $string['privacy:metadata:acceptances:policyversionid'] = 'The ID of the accepted version policy.';
-$string['privacy:metadata:acceptances:userid'] = 'The ID of the user who has agreed to the policy.';
+$string['privacy:metadata:acceptances:userid'] = 'The ID of the user who agreed to the policy.';
 $string['privacy:metadata:acceptances:status'] = 'The status of the agreement: 0 if not accepted; 1 otherwise.';
 $string['privacy:metadata:acceptances:lang'] = 'The current language displayed when the policy is accepted.';
 $string['privacy:metadata:acceptances:usermodified'] = 'The ID of the user accepting the policy, if made on behalf of another user.';
index a51aa69..a7b9bf2 100644 (file)
@@ -293,7 +293,7 @@ class auth_email_external extends external_api {
         $data['email2'] = $data['email'];
         // Force policy agreed if a site policy is set. The client is responsible of implementing the interface check.
         $manager = new \core_privacy\local\sitepolicy\manager();
-        if (!$manager->is_defined()) {
+        if ($manager->is_defined()) {
             $data['policyagreed'] = 1;
         }
         unset($data['recaptcharesponse']);
index 38f388a..1bb9de5 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['activity_modules:addinstance'] = 'Add a new activities block';
 $string['pluginname'] = 'Activities';
-$string['privacy:metadata'] = 'The Activites block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Activities block only shows data stored in other locations.';
index 23a2b10..97dec82 100644 (file)
@@ -65,4 +65,4 @@ $string['worstgrade'] = 'The lowest grade:';
 $string['worstgrades'] = 'The {$a} lowest grades:';
 $string['worstgroupgrade'] = 'The group with the lowest average:';
 $string['worstgroupgrades'] = 'The {$a} groups with the lowest average:';
-$string['privacy:metadata'] = 'The Activites results block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Activity results block only shows data stored in other locations.';
index df44819..d1cb144 100644 (file)
@@ -28,4 +28,4 @@ $string['numbadgestodisplay'] = 'Number of latest badges to display';
 $string['nothingtodisplay'] = 'You have no badges to display';
 $string['badges:addinstance'] = 'Add a new My latest badges block';
 $string['badges:myaddinstance'] = 'Add a new My latest badges block to Dashboard';
-$string['privacy:metadata'] = 'The Badges block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Latest badges block only shows data stored in other locations.';
index f565520..0b951f3 100644 (file)
@@ -26,4 +26,4 @@ $string['calendar_upcoming:addinstance'] = 'Add a new upcoming events block';
 $string['calendar_upcoming:myaddinstance'] = 'Add a new upcoming events block to Dashboard';
 $string['gotocalendar'] = 'Go to calendar...';
 $string['pluginname'] = 'Upcoming events';
-$string['privacy:metadata'] = 'The Upcoming calendar events block only displays existing calendar data.';
+$string['privacy:metadata'] = 'The Upcoming events block only displays existing calendar data.';
index 4534d40..902dd5f 100644 (file)
@@ -26,4 +26,4 @@
 $string['coursesummary'] = 'Course summary';
 $string['course_summary:addinstance'] = 'Add a new course/site summary block';
 $string['pluginname'] = 'Course/site summary';
-$string['privacy:metadata'] = 'The Course and site summaryblock only shows information about courses and does not store data itself.';
+$string['privacy:metadata'] = 'The Course/site summary block only shows information about courses and does not store data itself.';
index bf0f6d5..29b53ad 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['login:addinstance'] = 'Add a new login block';
 $string['pluginname'] = 'Login';
-$string['privacy:metadata'] = 'The Login block only provides a way to login and does not store any data itself.';
+$string['privacy:metadata'] = 'The Login block only provides a way to log in and does not store any data itself.';
index 24a7767..b7de027 100644 (file)
@@ -45,7 +45,7 @@ $string['myprofile:addinstance'] = 'Add a new logged in user block';
 $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboard';
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
-$string['privacy:metadata'] = 'The Logged in users block only shows information about logged in users and does not store data itself.';
+$string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
 
 // Deprecated since Moodle 3.2.
 $string['display_un'] = 'Display name';
index 31a0066..f0791fc 100644 (file)
@@ -26,4 +26,4 @@ $string['pluginname'] = 'Private files';
 $string['privatefiles'] = 'Private files';
 $string['private_files:addinstance'] = 'Add a new private files block';
 $string['private_files:myaddinstance'] = 'Add a new private files block to Dashboard';
-$string['privacy:metadata'] = 'The Private files block only provides a view of, and link to, the user private files.';
+$string['privacy:metadata'] = 'The Private files block only provides a view of, and a link to, the user\'s private files.';
index 3b169ae..05ecc00 100644 (file)
@@ -47,4 +47,4 @@ $string['scienceandtech'] = 'Science &amp; Tech';
 $string['sports'] = 'Sports';
 $string['tag_youtube:addinstance'] = 'Add a new YouTube block';
 $string['travel'] = 'Travel &amp; Places';
-$string['privacy:metadata'] = 'The Youtube block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The YouTube block only shows data stored in other locations.';
index 8471cf8..683d570 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['name'] = 'Gregorian';
 $string['pluginname'] = 'Gregorian calendar type';
-$string['privacy:metadata'] = 'The Gregorian Calendar format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Gregorian calendar format plugin does not store any personal data.';
diff --git a/cohort/classes/privacy/provider.php b/cohort/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..4e625e2
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    core_cohort
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cohort\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Return the fields which contain personal data.
+     *
+     * @param   collection $collection The initialised collection to add items to.
+     * @return  collection A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table('cohort_members', [
+                'cohortid' => 'privacy:metadata:cohort_members:cohortid',
+                'userid' => 'privacy:metadata:cohort_members:userid',
+                'timeadded' => 'privacy:metadata:cohort_members:timeadded'
+            ], 'privacy:metadata:cohort_members');
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $sql = "SELECT ctx.id
+                  FROM {context} ctx
+            INNER JOIN {cohort} c ON c.contextid = ctx.id
+            INNER JOIN {cohort_members} cm ON cm.cohortid = c.id
+                 WHERE cm.userid = :userid AND (ctx.contextlevel = :contextlevel1 OR ctx.contextlevel = :contextlevel2)";
+        $params = [
+            'userid'        => $userid,
+            'contextlevel1' => CONTEXT_SYSTEM,
+            'contextlevel2' => CONTEXT_COURSECAT,
+        ];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Remove contexts different from SYSTEM or COURSECAT.
+        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
+                $carry[] = $context->id;
+            }
+            return $carry;
+        }, []);
+
+        if (empty($contexts)) {
+            return;
+        }
+
+        // Get cohort data.
+        $userid = $contextlist->get_user()->id;
+        list($contextsql, $contextparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
+        $sql = "SELECT c.name,
+                       c.idnumber,
+                       c.description,
+                       c.visible,
+                       cm.timeadded,
+                       ctx.id as contextid
+                  FROM {context} ctx
+            INNER JOIN {cohort} c ON c.contextid = ctx.id
+            INNER JOIN {cohort_members} cm ON cm.cohortid = c.id
+                 WHERE ctx.id {$contextsql}
+                       AND cm.userid = :userid";
+        $params = [
+                'userid'        => $userid
+            ] + $contextparams;
+
+        $cohorts = $DB->get_recordset_sql($sql, $params);
+        foreach ($cohorts as $cohort) {
+            $alldata[$cohort->contextid][] = (object)[
+                    'name' => $cohort->name,
+                    'idnumber' => $cohort->idnumber,
+                    'visible' => transform::yesno($cohort->visible),
+                    'timeadded' => transform::datetime($cohort->timeadded),
+                ];
+        }
+        $cohorts->close();
+
+        // Export cohort data.
+        array_walk($alldata, function($data, $contextid) {
+            $context = \context::instance_by_id($contextid);
+            writer::with_context($context)->export_related_data([], 'cohort', $data);
+        });
+
+    }
+
+    /**
+     * Delete all use data which matches the specified context.
+     *
+     * @param context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        if (empty($context)) {
+            return;
+        }
+
+        if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
+            return;
+        }
+
+        static::delete_data($context);
+    }
+
+    /**
+     * 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) {
+        if (empty($contextlist->count())) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
+                return;
+            }
+            static::delete_data($context, $userid);
+        }
+    }
+
+    /**
+     * Delete data related to a context and user (if defined).
+     *
+     * @param context $context A context.
+     * @param int $userid The user ID.
+     */
+    protected static function delete_data(\context $context, int $userid = null) {
+        global $DB;
+
+        $cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid', ['contextid' => $context->id]);
+        foreach ($cohortids as $cohortid) {
+            $params = ['cohortid' => $cohortid];
+            if (!empty($userid)) {
+                $params['userid'] = $userid;
+            }
+            $DB->delete_records('cohort_members', $params);
+        }
+    }
+}
diff --git a/cohort/tests/privacy_test.php b/cohort/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..eba3d65
--- /dev/null
@@ -0,0 +1,218 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for unit tests for core_cohort.
+ *
+ * @package    core_cohort
+ * @category   test
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_cohort\privacy\provider;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for cohort\classes\privacy\provider.php
+ *
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_cohort_testcase extends provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort->id, $user->id);
+
+        // User is member of 2 cohorts.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $this->assertCount(2, (array) $contextlist->get_contextids());
+        $this->assertContains($coursecategoryctx->id, $contextlist->get_contextids());
+        $this->assertContains($systemctx->id, $contextlist->get_contextids());
+    }
+
+    /**
+     * Test that data is exported correctly for this plugin.
+     */
+    public function test_export_user_data() {
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+            ]);
+        $systemcohort1 = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+        $systemcohort2 = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 2'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort1->id, $user->id);
+        cohort_add_member($systemcohort2->id, $user->id);
+
+        // Validate system cohort exported data.
+        $writer = writer::with_context($systemctx);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $systemctx, 'core_cohort');
+        $data = $writer->get_related_data([], 'cohort');
+        $this->assertCount(2, $data);
+
+        // Validate category cohort exported data.
+        $writer = writer::with_context($coursecategoryctx);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $coursecategoryctx, 'core_cohort');
+        $data = $writer->get_related_data([], 'cohort');
+        $this->assertCount(1, $data);
+        $this->assertEquals($categorycohort->name, reset($data)->name);
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+                'idnumber' => '',
+                'description' => ''
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort->id, $user->id);
+
+        // Before deletion, we should have 2 entries in the cohort_members table.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(2, $count);
+
+        // Delete data based on system context.
+        provider::delete_data_for_all_users_in_context($systemctx);
+
+        // After deletion, the cohort_members entries should have been deleted.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(1, $count);
+
+        // Delete data based on category context.
+        provider::delete_data_for_all_users_in_context($coursecategoryctx);
+
+        // After deletion, the cohort_members entries should have been deleted.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(0, $count);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+                'idnumber' => '',
+                'description' => ''
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user1 = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user1->id);
+        cohort_add_member($systemcohort->id, $user1->id);
+
+        // Create another user and add to the system and category cohorts.
+        $user2 = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user2->id);
+        cohort_add_member($systemcohort->id, $user2->id);
+
+        // Create another user and add to the system cohort.
+        $user3 = $this->getDataGenerator()->create_user();
+        cohort_add_member($systemcohort->id, $user3->id);
+
+        // Before deletion, we should have 5 entries in the cohort_members table.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(5, $count);
+
+        $contextlist = provider::get_contexts_for_userid($user1->id);
+        $approvedcontextlist = new approved_contextlist($user1, 'cohort', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // After deletion, the cohort_members entries for the first student should have been deleted.
+        $count = $DB->count_records('cohort_members', ['userid' => $user1->id]);
+        $this->assertEquals(0, $count);
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(3, $count);
+
+        // Confirm that the cohorts hasn't been removed.
+        $cohortscount = $DB->get_records('cohort');
+        $this->assertCount(2, (array) $cohortscount);
+    }
+}
index b2d0b1d..2062044 100644 (file)
@@ -2988,6 +2988,8 @@ class course_request {
     public function approve() {
         global $CFG, $DB, $USER;
 
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
         $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
 
         $courseconfig = get_config('moodlecourse');
@@ -3022,6 +3024,8 @@ class course_request {
             $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
         }
 
+        list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
+
         $course = create_course($data);
         $context = context_course::instance($course->id, MUST_EXIST);
 
index cf1176b..c7517f9 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Comma separated values (.csv)';
-$string['privacy:metadata'] = 'The CSV Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The CSV data format plugin does not store any personal data.';
 $string['shortname'] = 'CSV';
 
index cd90da3..b09f1f8 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Microsoft Excel (.xlsx)';
-$string['privacy:metadata'] = 'The Excel Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Excel data format plugin does not store any personal data.';
 $string['shortname'] = 'Excel';
 
index e5efa77..0d62cfb 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'HTML table';
-$string['privacy:metadata'] = 'The HTML Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The HTML table data format plugin does not store any personal data.';
 $string['shortname'] = 'HTML';
 
index 43540ab..a8028a4 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Javascript Object Notation (.json)';
-$string['privacy:metadata'] = 'The JSON Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The JavaScript Object Notation data format plugin does not store any personal data.';
 $string['shortname'] = 'JSON';
 
index edf5a27..1d2773c 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'OpenDocument (.ods)';
-$string['privacy:metadata'] = 'The OpenDoc Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The OpenDocument data format plugin does not store any personal data.';
 $string['shortname'] = 'OpenDoc';
 
index dcb5f29..da4cd68 100644 (file)
@@ -129,6 +129,7 @@ class enrol_manual_enrol_users_form extends moodleform {
 
         $mform->addElement('checkbox', 'recovergrades', get_string('recovergrades', 'enrol'));
         $mform->setAdvanced('recovergrades');
+        $mform->setDefault('recovergrades', $CFG->recovergradesdefault);
         $mform->addElement('select', 'duration', get_string('defaultperiod', 'enrol_manual'), $periodmenu);
         $mform->setDefault('duration', $defaultperiod);
         $mform->setAdvanced('duration');
index e77ddb5..a28d07d 100644 (file)
@@ -30,7 +30,7 @@ $string['assignrole'] = 'Assign role';
 $string['assignroles'] = 'Assign roles';
 $string['browseusers'] = 'Browse users';
 $string['browsecohorts'] = 'Browse cohorts';
-$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
+$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these user enrolments?';
 $string['defaultstart'] = 'Default enrolment start';
 $string['defaultperiod'] = 'Default enrolment duration';
 $string['defaultperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.';
index 500e693..6aa0de2 100644 (file)
@@ -39,7 +39,7 @@ $string['enrolperiod_desc'] = 'Default length of time that the enrolment is vali
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can be enrolled from this date onward only.';
-$string['errdisabled'] = 'PayPal plugin is disabled and does not handle payment notifications.';
+$string['errdisabled'] = 'The PayPal enrolment plugin is disabled and does not handle payment notifications.';
 $string['erripninvalid'] = 'Instant payment notification has not been verified by PayPal.';
 $string['errpaypalconnect'] = 'Could not connect to {$a->url} to verify the instant payment notification: {$a->result}';
 $string['expiredaction'] = 'Enrolment expiry action';
@@ -56,7 +56,7 @@ $string['paypal:unenrolself'] = 'Unenrol self from the course';
 $string['paypalaccepted'] = 'PayPal payments accepted';
 $string['pluginname'] = 'PayPal';
 $string['pluginname_desc'] = 'The PayPal module allows you to set up paid courses.  If the cost for any course is zero, then students are not asked to pay for entry.  There is a site-wide cost that you set here as a default for the whole site and then a course setting that you can set for each course individually. The course cost overrides the site cost.';
-$string['processexpirationstask'] = 'Process expirations task';
+$string['processexpirationstask'] = 'PayPal enrolment send expiry notifications task';
 $string['sendpaymentbutton'] = 'Send payment via PayPal';
 $string['status'] = 'Allow PayPal enrolments';
 $string['status_desc'] = 'Allow users to use PayPal to enrol into a course by default.';
index 75fc027..7dc5684 100644 (file)
@@ -28,7 +28,7 @@ $string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on
 $string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.';
 $string['cohortonly'] = 'Only cohort members';
 $string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.';
-$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
+$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these user enrolments?';
 $string['customwelcomemessage'] = 'Custom welcome message';
 $string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags.
 
index 207dbc2..45e9725 100644 (file)
@@ -432,7 +432,7 @@ $string['debugnormal'] = 'NORMAL: Show errors, warnings and notices';
 $string['debugpageinfo'] = 'Show page information';
 $string['debugsmtp'] = 'Debug email sending';
 $string['debugstringids'] = 'Show origin of languages strings';
-$string['debugstringids_desc'] = 'This option is designed to help translators. When this option is enabled, if you add the parameter strings=1 to a request URL, it will show the language file and string id beside each string that is output.';
+$string['debugstringids_desc'] = 'If enabled, language string components and identifiers are displayed when ?strings=1 or &strings=1 is appended to the page URL.';
 $string['debugvalidators'] = 'Show validator links';
 $string['defaultcity'] = 'Default city';
 $string['defaultcity_help'] = 'A city entered here will be the default city when creating new user accounts.';
@@ -834,7 +834,7 @@ $string['passwordresettime'] = 'Maximum time to validate password reset request'
 $string['passwordreuselimit'] = 'Password rotation limit';
 $string['passwordreuselimit_desc'] = 'Number of times a user must change their password before they are allowed to reuse a password. Hashes of previously used passwords are stored in local database table. This feature might not be compatible with some external authentication plugins.';
 $string['pathtodot'] = 'Path to dot';
-$string['pathtodot_help'] = 'Path to dot. Probably something like /usr/bin/dot. To be able to generate graphics from DOT files, you must have installed the dot executable and point to it here. Note that, for now, this only used by the profiling features (Development->Profiling) built into Moodle.';
+$string['pathtodot_help'] = 'Path to dot. On Linux it is something like /usr/bin/dot. On Windows it is something like C:\Program Files (x86)\Graphviz2.38\bin\dot.exe. On Mac it is something like /opt/local/bin/dot. To be able to generate graphics from DOT files, you must have installed the dot executable and point to it here.';
 $string['pathtodu'] = 'Path to du';
 $string['pathtogs'] = 'Path to ghostscript';
 $string['pathtogs_help'] = 'On most Linux installs, this can be left as \'/usr/bin/gs\'. On Windows it will be something like \'c:\\gs\\bin\\gswin32c.exe\' (make sure there are no spaces in the path - if necessary copy the files \'gswin32c.exe\' and \'gsdll32.dll\' to a new folder without a space in the path)';
@@ -1057,15 +1057,15 @@ $string['sitemaintenanceon'] = 'Your site is currently in maintenance mode (only
 $string['sitemaintenanceoncli'] = 'Your site is currently in CLI maintenance mode, no web access is allowed.';
 $string['sitemaintenancewarning'] = 'Your site is currently in maintenance mode (only admins can log in).  To return this site to normal operation, <a href="maintenance.php">disable maintenance mode</a>.';
 $string['sitemaintenancewarning2'] = 'Your site is currently in maintenance mode (only admins can log in).  To return this site to normal operation, <a href="{$a}">disable maintenance mode</a>.';
-$string['sitepolicies'] = 'Site policies';
+$string['sitepolicies'] = 'Site security settings';
 $string['sitepolicy'] = 'Site policy URL';
-$string['sitepolicy_help'] = 'If you have a site policy that all registered users must see and agree to before using this site, then specify the URL to it here, otherwise leave this field blank. This setting can contain any public URL.';
+$string['sitepolicy_help'] = 'The URL of the site policy that all registered users must see and agree to before accessing the site. Note that this setting will only have an effect if the site policy handler is set to default (core).';
 $string['sitepolicyhandler'] = 'Site policy handler';
-$string['sitepolicyhandler_desc'] = 'Select the component to handle user agreements to site policies. The default core handler provides a simple functionality controlled by the two other settings `sitepolicy` and `sitepolicyguest`. Alternative handlers may be provided by additional plugins and offer more advanced control of site policies.';
+$string['sitepolicyhandler_desc'] = 'This determines how policies and user consents are managed. The default (core) handler enables a site policy URL and a site policy URL for guests to be specified. The policies handler enables site, privacy and other policies to be set. It also enables user consents to be viewed and, if necessary, consent on behalf of minors to be given.';
 $string['sitepolicyhandlercore'] = 'Default (core)';
 $string['sitepolicyhandlerplugin'] = '{$a->name} ({$a->component})';
 $string['sitepolicyguest'] = 'Site policy URL for guests';
-$string['sitepolicyguest_help'] = 'If you have a site policy that all guests must see and agree to before using this site, then specify the URL to it here, otherwise leave this field blank. This setting can contain any public URL. Note: access of not-logged-in users may be prevented with forcelogin setting.';
+$string['sitepolicyguest_help'] = 'The URL of the site policy that all guests must see and agree to before accessing the site. Note that this setting will only have an effect if the site policy handler is set to default (core).';
 $string['sitesectionhelp'] = 'If selected, a topic section will be displayed on the site\'s front page.';
 $string['slasharguments'] = 'Use slash arguments';
 $string['slashargumentswarning'] = 'It is recommended that the use of slash arguments is enabled. In future it will be required. For more details, see the documentation <a href="https://docs.moodle.org/en/admin/environment/slasharguments">Using slash arguments</a>.';
index 643fab4..a96fb4f 100644 (file)
@@ -86,5 +86,5 @@ $string['predictionsprocessor_help'] = 'A predictions processor is the machine-l
 $string['processingsitecontents'] = 'Processing site contents';
 $string['successfullyanalysed'] = 'Successfully analysed';
 $string['timesplittingmethod'] = 'Time-splitting method';
-$string['timesplittingmethod_help'] = 'The time-splitting method divides the course duration into parts; the predictions engine will run at the end of these parts. It is recommended that you only enable the time-splitting methods you may be interested in using. The evaluation process will iterate through all enabled time-spitting methods, so the more time-splitting methods enabled, the slower the evaluation process will be.';
+$string['timesplittingmethod_help'] = 'The time-splitting method divides the course duration into parts; the predictions engine will run at the end of these parts. It is recommended that you only enable the time-splitting methods you may be interested in using. The evaluation process will iterate through all enabled time-splitting methods, so the more time-splitting methods enabled, the slower the evaluation process will be.';
 $string['viewprediction'] = 'View prediction details';
index 28ac515..cb7597e 100644 (file)
@@ -224,7 +224,7 @@ $string['criteria_6_help'] = 'Allows a badge to be awarded to users for completi
 $string['criteria_7'] = 'Awarded badges';
 $string['criteria_7_help'] = 'Allows a badge to be awarded to users based on the other badges thay have earned.';
 $string['criteria_8'] = 'Cohort membership';
-$string['criteria_8_help'] = 'Allows a badge to be awarded to users for becoming a member of certian cohorts.';
+$string['criteria_8_help'] = 'Allows a badge to be awarded to users based on cohort membership.';
 $string['criterror'] = 'Current parameters issues';
 $string['criterror_help'] = 'This fieldset shows all parameters that were initially added to this badge requirement but are no longer available. It is recommended that you un-check such parameters to make sure that users can earn this badge in the future.';
 $string['currentimage'] = 'Current image';
index 3b0053f..0ca7302 100644 (file)
@@ -132,7 +132,7 @@ $string['privacy:metadata:core_comments'] = 'Comments associated with blog entri
 $string['privacy:metadata:core_files'] = 'Files attached to blog entries';
 $string['privacy:metadata:core_tag'] = 'Tags associated with blog entries';
 $string['privacy:metadata:external'] = 'A link to an external RSS feed';
-$string['privacy:metadata:external:userid'] = 'The user ID';
+$string['privacy:metadata:external:userid'] = 'The ID of the user who added the external blog entry.';
 $string['privacy:metadata:external:name'] = 'The name of the feed';
 $string['privacy:metadata:external:description'] = 'The description of the feed';
 $string['privacy:metadata:external:url'] = 'The URL of the feed';
@@ -140,14 +140,14 @@ $string['privacy:metadata:external:filtertags'] = 'The list of tags to filter th
 $string['privacy:metadata:external:timemodified'] = 'Date at which the association was last modified';
 $string['privacy:metadata:external:timefetched'] = 'Date at which the feed was last fetched';
 $string['privacy:metadata:post'] = 'The information related to blog entries';
-$string['privacy:metadata:post:userid'] = 'The user ID';
-$string['privacy:metadata:post:subject'] = 'The subject of the entry';
-$string['privacy:metadata:post:summary'] = 'The summary of the entry';
-$string['privacy:metadata:post:content'] = 'The content of an external entry';
+$string['privacy:metadata:post:userid'] = 'The ID of the user who added the blog entry.';
+$string['privacy:metadata:post:subject'] = 'Blog entry title.';
+$string['privacy:metadata:post:summary'] = 'Blog entry.';
+$string['privacy:metadata:post:content'] = 'The content of an external blog entry.';
 $string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL.';
 $string['privacy:metadata:post:publishstate'] = 'Whether the entry is visible to others or not';
-$string['privacy:metadata:post:created'] = 'Date at which the entry was created';
-$string['privacy:metadata:post:lastmodified'] = 'Date at which the entry was last modified';
+$string['privacy:metadata:post:created'] = 'Date when the entry was created.';
+$string['privacy:metadata:post:lastmodified'] = 'Date when the entry was last modified.';
 $string['privacy:metadata:post:usermodified'] = 'The user who last modified the entry';
 $string['privacy:path:blogassociations'] = 'Associated blog posts';
 $string['privacy:unknown'] = 'Unknown';
index ee7503b..cfda2a1 100644 (file)
@@ -70,6 +70,10 @@ $string['nocomponent'] = 'Created manually';
 $string['potusers'] = 'Potential users';
 $string['potusersmatching'] = 'Potential matching users';
 $string['preview'] = 'Preview';
+$string['privacy:metadata:cohort_members'] = 'Information about the user\'s cohort.';
+$string['privacy:metadata:cohort_members:cohortid'] = 'The ID of the cohort';
+$string['privacy:metadata:cohort_members:timeadded'] = 'The timestamp indicating when the user was added to the cohort';
+$string['privacy:metadata:cohort_members:userid'] = 'The ID of the user which is associated to the cohort';
 $string['removeuserwarning'] = 'Removing users from a cohort may result in unenrolling of users from multiple courses which includes deleting of user settings, grades, group membership and other user information from affected courses.';
 $string['selectfromcohort'] = 'Select members from cohort';
 $string['systemcohorts'] = 'System cohorts';
index 54c35b5..013b923 100644 (file)
@@ -117,7 +117,7 @@ $string['pleaseenteralt'] = 'Please enter the alternate text';
 $string['popupeditor'] = 'Enlarge Editor';
 $string['preformatted'] = 'Preformatted';
 $string['preview'] = 'Preview';
-$string['privacy:metadata:preference:htmleditor'] = 'The preferred editor to use when using an HTML Text Area';
+$string['privacy:metadata:preference:htmleditor'] = 'Text editor preference.';
 $string['privacy:preference:htmleditor'] = 'Your preferred editor to use for writing HTML text is {$a}';
 $string['properties'] = 'Properties';
 $string['redo'] = 'Redo your last action';
index 3d9055f..4032ffb 100644 (file)
@@ -1434,7 +1434,7 @@ $string['notifyloginfailuresmessageend'] = 'You can view these logs at {$a}';
 $string['notifyloginfailuresmessagestart'] = 'Here is a list of failed login attempts at {$a} since you were last notified';
 $string['notifyloginfailuressubject'] = '{$a} :: Failed logins notification';
 $string['notincluded'] = 'Not included';
-$string['notingroup'] = 'Sorry, but you need to be part of a group to see this activity.';
+$string['notingroup'] = 'Sorry, but you need to be part of a group to see this page.';
 $string['notpublic'] = 'Not public!';
 $string['nousersfound'] = 'No users found';
 $string['nousersmatching'] = 'No users matching \'{$a}\' were found';
index ea1e734..216b1ab 100644 (file)
@@ -38,3 +38,4 @@ $string['reseteveryonesdashboard'] = 'Reset Dashboard for all users';
 $string['reseteveryonesprofile'] = 'Reset profile for all users';
 $string['resetpage'] = 'Reset page to default';
 $string['reseterror'] = 'There was an error resetting your page';
+$string['privacy:metadata:core_my:preference:user_home_page_preference'] = 'The user home page preference configured for the Dashboard page.';
index c2b388d..6fb2f45 100644 (file)
@@ -58,6 +58,13 @@ $string['page-notes-x'] = 'Any note pages';
 $string['page-notes-index'] = 'Notes main page';
 $string['personal'] = 'personal';
 $string['personalnotes'] = 'Personal notes';
+$string['privacy:metadata:core_notes'] = 'The Notes component stores user notes within the core subsystem.';
+$string['privacy:metadata:core_notes:content'] = 'The content of the note.';
+$string['privacy:metadata:core_notes:courseid'] = 'The Id of the course associated with the note.';
+$string['privacy:metadata:core_notes:created'] = 'The creation date/time for the note.';
+$string['privacy:metadata:core_notes:lastmodified'] = 'The last modified date/time for the note.';
+$string['privacy:metadata:core_notes:publishstate'] = 'The publish state of the note.';
+$string['privacy:metadata:core_notes:userid'] = 'The Id of the user associated with the note.';
 $string['publishstate'] = 'Context';
 $string['publishstate_help'] = 'A note\'s context determines who can see the note.
 
index 809e938..7447f82 100644 (file)
@@ -24,6 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-
 $string['privacy:metadata:core_userkey'] = 'User\'s keys used to access RSS from a URL';
 $string['rss'] = 'RSS';
index 563e2e1..4507a97 100644 (file)
@@ -97,8 +97,8 @@ $string['privacy:metadata:tag:name'] = 'The name of the tag - this is the normal
 $string['privacy:metadata:tag:rawname'] = 'The name of the tag - this is the display name.';
 $string['privacy:metadata:tag:description'] = 'The description of the tag.';
 $string['privacy:metadata:tag:flag'] = 'Whether a tag has been flagged as inappropriate.';
-$string['privacy:metadata:tag:timemodified'] = 'The last time that the tag was last modified.';
-$string['privacy:metadata:tag:userid'] = 'The user who first created the tag.';
+$string['privacy:metadata:tag:timemodified'] = 'The time that the tag was last modified.';
+$string['privacy:metadata:tag:userid'] = 'The ID of the user who created the tag.';
 $string['privacy:metadata:taginstance'] = 'The link between each tag and where it is used.';
 $string['privacy:metadata:taginstance:tagid'] = 'The link to the tag.';
 $string['privacy:metadata:taginstance:ordering'] = 'The relative order of this tag.';
index 18a5b8f..0e90141 100644 (file)
@@ -36,7 +36,7 @@ $string['pathtoclamdesc'] = 'If the running method is set to "command line", ent
 $string['pathtounixsocket'] = 'Unix domain socket';
 $string['pathtounixsocketdesc'] = 'If the running method is set to "Unix domain socket", enter the path to ClamAV Unix socket here. On Debian Linux this will be /var/run/clamav/clamd.ctl. Please make sure that clamav daemon has read access to uploaded files, the easiest way to ensure that is to add \'clamav\' user to your webserver group (\'www-data\' on Debian Linux).';
 $string['pluginname'] = 'ClamAV antivirus';
-$string['privacy:metadata'] = 'The ClamAV Antivirus plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The ClamAV antivirus plugin does not store any personal data.';
 $string['quarantinedir'] = 'Quarantine directory';
 $string['runningmethod'] = 'Running method';
 $string['runningmethoddesc'] = 'Method of running ClamAV. Command line is used by default, however on Unix systems better performance can be obtained by using system sockets.';
index fe62b3a..ffe8c16 100644 (file)
@@ -47,7 +47,7 @@ $string['recover'] = 'Recover';
 $string['infostatus'] = 'Information';
 $string['warningstatus'] = 'Warning';
 $string['autosaves'] = 'Editor autosave information';
-$string['privacy:metadata:database:atto_autosave'] = 'Editor drafts which was automatically saved.';
-$string['privacy:metadata:database:atto_autosave:userid'] = 'The ID of the user who\'s data was saved.';
+$string['privacy:metadata:database:atto_autosave'] = 'Automatically saved text editor drafts.';
+$string['privacy:metadata:database:atto_autosave:userid'] = 'The ID of the user whose data was saved.';
 $string['privacy:metadata:database:atto_autosave:drafttext'] = 'The text which was saved.';
 $string['privacy:metadata:database:atto_autosave:timemodified'] = 'The time that content was modified.';
index e4df7ef..354bda9 100644 (file)
@@ -25,4 +25,4 @@
  */
 
 $string['pluginname'] = 'Plain text area';
-$string['privacy:metadata'] = 'The editor_textarea plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Plain text area editor plugin does not store any personal data.';
index 82dd895..8008f97 100644 (file)
@@ -894,4 +894,4 @@ $string['advanced:cut_desc'] = 'Cut';
 $string['advanced:paste_desc'] = 'Paste';
 $string['advanced:shortcuts_desc'] = 'Accessibility help';
 $string['autosave:restore_content'] = 'Restore auto-saved content';
-$string['privacy:metadata'] = 'The editor_tinymce plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The TinyMCE HTML editor plugin does not store any personal data.';
index 1a6ceff..fca1d43 100644 (file)
@@ -26,4 +26,4 @@ $string['pluginname'] = 'Toolbar Toggle';
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['pdw:desc'] = 'Toolbar Toggle';
-$string['privacy:metadata'] = 'The TinyMCE Toolebar toggle plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The TinyMCE Toolbar toggle plugin does not store any personal data.';
index affe8e6..8474022 100644 (file)
@@ -46,7 +46,7 @@
         ]
     }
 }}
-<div class="form-filetypes-descriptions">
+<div class="form-filetypes-descriptions w-100">
 {{#hasdescriptions}}
     <ul class="list-unstyled unstyled">
         {{#descriptions}}
index 7cde09c..dfe2ad6 100644 (file)
@@ -505,7 +505,7 @@ class core_component_testcase extends advanced_testcase {
         $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile'));
 
         // Without namespace it returns classes/ classes.
-        $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_mobile', ''));
+        $this->assertCount(3, core_component::get_component_classes_in_namespace('tool_mobile', ''));
         $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_filetypes'));
     }
 
index 3ba1e84..51c2d73 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js differ
index 3c20d46..e75c59c 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js differ
index 3ba1e84..51c2d73 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js differ
index 0a13ee5..cddd2ca 100644 (file)
@@ -63,14 +63,14 @@ Y.extend(CONFIRM, M.core.notification.info, {
         this.publish('complete');
         this.publish('complete-yes');
         this.publish('complete-no');
-        this._yesButton = Y.Node.create('<input type="button" class="btn btn-primary m-r-1" id="id_yuiconfirmyes-' +
+        this._yesButton = Y.Node.create('<input type="button" class="btn btn-primary" id="id_yuiconfirmyes-' +
                                         this.get('COUNT') + '" value="' + this.get(CONFIRMYES) + '" />');
-        this._noButton = Y.Node.create('<input type="button" class="btn btn-secondary m-r-1" id="id_yuiconfirmno-' +
+        this._noButton = Y.Node.create('<input type="button" class="btn btn-secondary" id="id_yuiconfirmno-' +
                                         this.get('COUNT') + '" value="' + this.get(CONFIRMNO) + '" />');
         this._question = Y.Node.create('<div class="confirmation-message">' + this.get(QUESTION) + '</div>');
         var content = Y.Node.create('<div class="confirmation-dialogue"></div>')
                         .append(this._question)
-                        .append(Y.Node.create('<div class="confirmation-buttons form-inline"></div>')
+                        .append(Y.Node.create('<div class="confirmation-buttons form-inline justify-content-around"></div>')
                             .append(this._yesButton)
                             .append(this._noButton));
         this.get(BASE).addClass('moodle-dialogue-confirm');
index 666fcda..f95ee5a 100644 (file)
@@ -43,19 +43,19 @@ $string['notconfigured'] = 'The Airnotifier server hasn\'t been configured so Ai
 $string['pluginname'] = 'Mobile';
 $string['privacy:appiddescription'] = 'This is an identifier to the application being used.';
 $string['privacy:enableddescription'] = 'If this device is enabled for airnotifier.';
-$string['privacy:metadata:enabled'] = 'If the airnotifier device is enabled.';
+$string['privacy:metadata:enabled'] = 'Whether the airnotifier device is enabled.';
 $string['privacy:metadata:date'] = 'The date that the message was sent.';
 $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external site to be ultimately delivered to the mobile device of the user.';
 $string['privacy:metadata:fullmessage'] = 'The full message.';
 $string['privacy:metadata:notification'] = 'If this message is a notification.';
-$string['privacy:metadata:smallmessage'] = 'An section of the message.';
+$string['privacy:metadata:smallmessage'] = 'A section of the message.';
 $string['privacy:metadata:subject'] = 'The subject line of the message.';
 $string['privacy:metadata:tableexplanation'] = 'Airnotifier device information is stored here.';
 $string['privacy:metadata:userdeviceid'] = 'The ID linking to the user\'s mobile device';
-$string['privacy:metadata:userfromfullname'] = 'The full name of the user that sent the message.';
+$string['privacy:metadata:userfromfullname'] = 'The full name of the user who sent the message.';
 $string['privacy:metadata:userfromid'] = 'The user ID of the author of the message.';
-$string['privacy:metadata:userid'] = 'The user ID.';
-$string['privacy:metadata:username'] = 'The username for the user.';
+$string['privacy:metadata:userid'] = 'The ID of the user who sent the message.';
+$string['privacy:metadata:username'] = 'The username of the user.';
 $string['privacy:metadata:usersubsystem'] = 'This plugin is connected to the user subsystem.';
 $string['privacy:subcontext'] = 'Message Airnotifier';
 $string['sitemustberegistered'] = 'In order to use the public Airnotifier instance you must register your site with Moodle.net';
index b40b681..91c854c 100644 (file)
@@ -35,8 +35,8 @@ $string['jabberpassword'] = 'Jabber password';
 $string['jabberport'] = 'Jabber port';
 $string['notconfigured'] = 'The Jabber server hasn\'t been configured so Jabber messages cannot be sent';
 $string['pluginname'] = 'Jabber';
-$string['privacy:metadata:contexturl'] = 'The url to view the event.';
+$string['privacy:metadata:contexturl'] = 'The URL to view the event.';
 $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external Jabber server to be ultimately delivered as a notification message to the recipient.';
-$string['privacy:metadata:smallmessage'] = 'The small version of the message.';
+$string['privacy:metadata:smallmessage'] = 'A section of the message.';
 $string['privacy:metadata:userfrom'] = 'The user sending the notification message.';
 $string['privacy:metadata:userto'] = 'The recipient of the notification message.';
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 (file)
index 0000000..c935972
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <adrian@moodle.com>
+ *
+ * @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 <adrian@moodle.com>
+ * @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/assignfeedback_provider.php b/mod/assign/classes/privacy/assignfeedback_provider.php
new file mode 100644 (file)
index 0000000..c93852a
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <adrian@moodle.com>
+ *
+ * @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/assignsubmission_provider.php b/mod/assign/classes/privacy/assignsubmission_provider.php
new file mode 100644 (file)
index 0000000..84c545e
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the assignsubmission_provider interface.
+ *
+ * Assignment Sub plugins should implement this if they store personal information.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @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 assignsubmission_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_submission 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_submission(int $userid, contextlist $contextlist);
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by
+     * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those
+     * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above.
+     *
+     * @param  useridlist $useridlist A user ID list object that you can append your user IDs to.
+     */
+    public static function get_student_user_ids(useridlist $useridlist);
+
+    /**
+     * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the
+     * context and userid.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin.
+     */
+    public static function export_submission_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 Information to use to delete user data for this submission.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata);
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $exportdata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $exportdata);
+}
diff --git a/mod/assign/classes/privacy/feedback_legacy_polyfill.php b/mod/assign/classes/privacy/feedback_legacy_polyfill.php
new file mode 100644 (file)
index 0000000..2fd6bdd
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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/classes/privacy/provider.php b/mod/assign/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..bbe0713
--- /dev/null
@@ -0,0 +1,496 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    mod_assign
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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/submission_legacy_polyfill.php b/mod/assign/classes/privacy/submission_legacy_polyfill.php
new file mode 100644 (file)
index 0000000..9b088d9
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait submission_legacy_polyfill {
+
+    /**
+     * Retrieves the contextids associated with the provided userid for this subplugin.
+     * NOTE if your subplugin must have an entry in the assign_submission 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_submission(int $userid, contextlist $contextlist) {
+        return static::_get_context_for_userid_within_submission($userid, $contextlist);
+    }
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by
+     * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those
+     * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above.
+     *
+     * @param  useridlist $useridlist A user ID list object that you can append your user IDs to.
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        return static::_get_student_user_ids($useridlist);
+    }
+
+    /**
+     * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the
+     * context and userid.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin.
+     */
+    public static function export_submission_user_data(assign_plugin_request_data $exportdata) {
+        return static::_export_submission_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 Information to use to delete user data for this submission.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata) {
+        return static::_delete_submission_for_context($requestdata);
+    }
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $exportdata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $exportdata) {
+        return static::_delete_submission_for_userid($exportdata);
+    }
+}
diff --git a/mod/assign/classes/privacy/useridlist.php b/mod/assign/classes/privacy/useridlist.php
new file mode 100644 (file)
index 0000000..430d6eb
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the mod_assign useridlist
+ *
+ * This is for collecting a list of user IDs
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @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 <adrian@moodle.com>
+ * @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/feedback/comments/classes/privacy/provider.php b/mod/assign/feedback/comments/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ac9aa7f
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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]);
+    }
+}
index 525eee6..52d8681 100644 (file)
  * @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 (file)
index 0000000..3649452
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignfeedback_comments.
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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 = '<p>first comment for this test</p>';
+        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 = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second student.</p>';
+        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 = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second student.</p>';
+        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 (file)
index 0000000..e61e781
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_editpdf
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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
+    }
+}
index fe6a7ec..1f7fac4 100644 (file)
@@ -73,6 +73,13 @@ $string['pagenumber'] = 'Page {$a}';
 $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 (file)
index 0000000..2291d6c
--- /dev/null
@@ -0,0 +1,244 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignfeedback_editpdf.
+ *
+ * @package    assignfeedback_editpdf
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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 (file)
index 0000000..f2f609f
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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]);
+    }
+}
index 20d4069..3765b00 100644 (file)
@@ -37,6 +37,8 @@ $string['feedbackfileadded'] = 'New feedback file "{$a->filename}" for student "
 $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 (file)
index 0000000..5537755
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignfeedback_file.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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 = '<p>first comment for this test</p>';
+        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 = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second submission.</p>';
+        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 = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second submission.</p>';
+        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 (file)
index 0000000..f3793ad
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_offline
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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';
+    }
+}
index aefde95..d5722ff 100644 (file)
@@ -38,6 +38,7 @@ $string['importgrades'] = 'Confirm changes in grading worksheet';
 $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 &quot;UTF-8&quot;';
+$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';
index 9ae84d1..8e606c7 100644 (file)
@@ -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: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>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.</li></ul>';
@@ -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
index 830531e..eb65233 100644 (file)
@@ -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/submission/comments/classes/privacy/provider.php b/mod/assign/submission/comments/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..6f95fd2
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignsubmission_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 \core_comment\privacy\provider as comments_provider;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, \mod_assign\privacy\assignsubmission_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_comment', 'privacy:metadata:commentpurpose');
+        return $collection;
+    }
+
+    /**
+     * It is possible to make a comment as a teacher without creating an entry in the submission table, so this is required
+     * to find those entries.
+     *
+     * @param  int $userid The user ID that we are finding contexts for.
+     * @param  contextlist $contextlist A context list to add sql and params to for contexts.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) {
+        $sql = "SELECT contextid
+                  FROM {comments}
+                 WHERE component = :component
+                       AND commentarea = :commentarea
+                       AND userid = :userid";
+        $params = ['userid' => $userid, 'component' => 'assignsubmission_comments', 'commentarea' => 'submission_comments'];
+        $contextlist->add_from_sql($sql, $params);
+    }
+
+    /**
+     * Due to the fact that we can't rely on the queries in the mod_assign provider we have to add some additional sql.
+     *
+     * @param  \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students.
+     */
+    public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) {
+        $params = ['assignid' => $useridlist->get_assignid(), 'commentuserid' => $useridlist->get_teacherid(),
+                'commentuserid2' => $useridlist->get_teacherid()];
+        $sql = "SELECT DISTINCT c.userid AS id
+                  FROM {comments} c
+                  JOIN (SELECT c.itemid
+                          FROM {comments} c
+                          JOIN {assign_submission} s ON s.id = c.itemid AND s.assignment = :assignid
+                         WHERE c.userid = :commentuserid) aa ON aa.itemid = c.itemid
+                 WHERE c.userid NOT IN (:commentuserid2)";
+        $useridlist->add_from_sql($sql, $params);
+    }
+
+    /**
+     * 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_submission_user_data(assign_plugin_request_data $exportdata) {
+        $component = 'assignsubmission_comments';
+        $commentarea = 'submission_comments';
+
+        $userid = ($exportdata->get_user() != null);
+        $submission = $exportdata->get_pluginobject();
+
+        // For the moment we are only showing the comments made by this user.
+        comments_provider::export_comments($exportdata->get_context(), $component, $commentarea, $submission->id,
+                $exportdata->get_subcontext(), $userid);
+    }
+
+    /**
+     * Delete all the comments made for this context.
+     *
+     * @param  assign_plugin_request_data $requestdata Data to fulfill the deletion request.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata) {
+        comments_provider::delete_comments_for_all_users($requestdata->get_context(), 'assignsubmission_comments',
+                'submission_comments');
+    }
+
+    /**
+     * A call to this method should delete user data (where practical) using the userid and submission.
+     *
+     * @param  assign_plugin_request_data $exportdata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $exportdata) {
+        // Create an approved context list to delete the comments.
+        $contextlist = new \core_privacy\local\request\approved_contextlist($exportdata->get_user(), 'assignsubmission_comments',
+            [$exportdata->get_context()->id]);
+        comments_provider::delete_comments_for_user($contextlist, 'assignsubmission_comments', 'submission_comments');
+    }
+}
index d63312b..78da642 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['blindmarkingname'] = 'Participant {$a}';
 $string['blindmarkingviewfullname'] = 'Participant {$a->participantnumber} ({$a->participantfullname})';
+$string['privacy:metadata:commentpurpose'] = 'Comments between the student and teacher about a submission.';
 $string['default'] = 'Enabled by default';
 $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.';
 $string['enabled'] = 'Submission comments';
diff --git a/mod/assign/submission/comments/tests/privacy_test.php b/mod/assign/submission/comments/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..471174b
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignsubmission_comments.
+ *
+ * @package    assignsubmission_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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/tests/privacy_test.php');
+
+/**
+ * Unit tests for mod/assign/submission/comments/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignsubmission_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  string   $submissiontext Submission text
+     * @return array   Submission plugin object and the submission object and the comment object.
+     */
+    protected function create_comment_submission($assign, $student, $submissiontext) {
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        $plugin = $assign->get_submission_plugin_by_type('comments');
+
+        $context = $assign->get_context();
+        $options = new stdClass();
+        $options->area = 'submission_comments';
+        $options->course = $assign->get_course();
+        $options->context = $context;
+        $options->itemid = $submission->id;
+        $options->component = 'assignsubmission_comments';
+        $options->showcount = true;
+        $options->displaycancel = true;
+
+        $comment = new comment($options);
+        $comment->set_post_permission(true);
+
+        $this->setUser($student);
+
+        $comment->add($submissiontext);
+
+        return [$plugin, $submission, $comment];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignsubmission_comments');
+        $collection = \assignsubmission_comments\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test returning the context for a user who has made a comment in an assignment.
+     */
+    public function test_get_context_for_userid_within_submission() {
+        $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();
+
+        $studentcomment = 'Comment from user 1';
+        list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment);
+        $teachercomment = 'From the teacher';
+        $this->setUser($user2);
+        $comment->add($teachercomment);
+
+        $contextlist = new \core_privacy\local\request\contextlist();
+        \assignsubmission_comments\privacy\provider::get_context_for_userid_within_submission($user2->id, $contextlist);
+        $this->assertEquals($context->id, $contextlist->get_contextids()[0]);
+    }
+
+    /**
+     * Test returning student ids given a user ID.
+     */
+    public function test_get_student_user_ids() {
+        $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();
+
+        $studentcomment = 'Comment from user 1';
+        list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment);
+        $teachercomment = 'From the teacher';
+        $this->setUser($user2);
+        $comment->add($teachercomment);
+
+        $useridlist = new mod_assign\privacy\useridlist($user2->id, $assign->get_instance()->id);
+        \assignsubmission_comments\privacy\provider::get_student_user_ids($useridlist);
+        $this->assertEquals($user1->id, $useridlist->get_userids()[0]->id);
+    }
+
+    /**
+     * Test that comments are exported for a user.
+     */
+    public function test_export_submission_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();
+
+        $studentcomment = 'Comment from user 1';
+        list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment);
+        $teachercomment = 'From the teacher';
+        $this->setUser($user2);
+        $comment->add($teachercomment);
+
+        $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, $submission);
+        \assignsubmission_comments\privacy\provider::export_submission_user_data($exportdata);
+        $exportedcomments = $writer->get_data(['Comments']);
+        $this->assertCount(2, $exportedcomments->comments);
+        $this->assertContains($studentcomment, $exportedcomments->comments[0]->content);
+        $this->assertContains($teachercomment, $exportedcomments->comments[1]->content);
+    }
+
+    /**
+     * Test that all comments are deleted for this context.
+     */
+    public function test_delete_submission_for_context() {
+        global $DB;
+        $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();
+
+        $studentcomment = 'Comment from user 1';
+        list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment);
+        $studentcomment = 'Comment from user 2';
+        list($plugin2, $submission2, $comment2) = $this->create_comment_submission($assign, $user2, $studentcomment);
+        $teachercomment1 = 'From the teacher';
+        $teachercomment2 = 'From the teacher for second student.';
+        $this->setUser($user3);
+        $comment->add($teachercomment1);
+        $comment2->add($teachercomment2);
+
+        // Only need the context in this plugin for this operation.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        \assignsubmission_comments\privacy\provider::delete_submission_for_context($requestdata);
+
+        $results = $DB->get_records('comments', ['contextid' => $context->id]);
+        $this->assertEmpty($results);
+    }
+
+    /**
+     * Test that the comments for a user are deleted.
+     */
+    public function test_delete_submission_for_userid() {
+        global $DB;
+        $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();
+
+        $studentcomment = 'Comment from user 1';
+        list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment);
+        $studentcomment = 'Comment from user 2';
+        list($plugin2, $submission2, $comment2) = $this->create_comment_submission($assign, $user2, $studentcomment);
+        $teachercomment1 = 'From the teacher';
+        $teachercomment2 = 'From the teacher for second student.';
+        $this->setUser($user3);
+        $comment->add($teachercomment1);
+        $comment2->add($teachercomment2);
+
+        // Provide full details to delete the comments.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, null, [], $user1);
+        \assignsubmission_comments\privacy\provider::delete_submission_for_userid($requestdata);
+
+        $results = $DB->get_records('comments', ['contextid' => $context->id]);
+        // We are only deleting the comments for user1 (one comment) so we should have three left.
+        $this->assertCount(3, $results);
+        foreach ($results as $result) {
+            // Check that none of the comments are from user1.
+            $this->assertNotEquals($user1->id, $result->userid);
+        }
+    }
+}
diff --git a/mod/assign/submission/file/classes/privacy/provider.php b/mod/assign/submission/file/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..a10e67a
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignsubmission_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\writer;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, \mod_assign\privacy\assignsubmission_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;
+    }
+
+    /**
+     * This is covered by mod_assign provider and the query on assign_submissions.
+     *
+     * @param  int $userid The user ID that we are finding contexts for.
+     * @param  contextlist $contextlist A context list to add sql and params to for contexts.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) {
+        // This is already fetched from mod_assign.
+    }
+
+    /**
+     * This is also covered by the mod_assign provider and it's queries.
+     *
+     * @param  \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students.
+     */
+    public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) {
+        // No need.
+    }
+
+    /**
+     * 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_submission_user_data(assign_plugin_request_data $exportdata) {
+        // We currently don't show submissions to teachers when exporting their data.
+        $context = $exportdata->get_context();
+        if ($exportdata->get_user() != null) {
+            return null;
+        }
+        $user = new \stdClass();
+        $assign = $exportdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignsubmission', 'file');
+        $files = $plugin->get_files($exportdata->get_pluginobject(), $user);
+        foreach ($files as $file) {
+            $userid = $exportdata->get_pluginobject()->userid;
+            writer::with_context($exportdata->get_context())->export_file($exportdata->get_subcontext(), $file);
+
+            // Plagiarism data.
+            $coursecontext = $context->get_course_context();
+            \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $exportdata->get_subcontext(), [
+                'cmid' => $context->instanceid,
+                'course' => $coursecontext->instanceid,
+                'userid' => $userid,
+                'file' => $file
+            ]);
+        }
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     *
+     * @param  assign_plugin_request_data $requestdata Information useful for deleting user data.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context());
+
+        $fs = get_file_storage();
+        $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA);
+
+        // Delete records from assignsubmission_file table.
+        $DB->delete_records('assignsubmission_file', ['assignment' => $requestdata->get_assign()->get_instance()->id]);
+    }
+
+    /**
+     * A call to this method should delete user data (where practicle) using the userid and submission.
+     *
+     * @param  assign_plugin_request_data $deletedata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context());
+
+        $submissionid = $deletedata->get_pluginobject()->id;
+
+        $fs = get_file_storage();
+        $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA,
+                $submissionid);
+
+        $DB->delete_records('assignsubmission_file', ['assignment' => $deletedata->get_assign()->get_instance()->id,
+                'submission' => $submissionid]);
+    }
+}
index 45dd3ff..f566ef5 100644 (file)
@@ -43,6 +43,7 @@ $string['maximumsubmissionsize'] = 'Maximum submission size';
 $string['maximumsubmissionsize_help'] = 'Files uploaded by students may be up to this size.';
 $string['numfilesforlog'] = 'The number of file(s) : {$a} file(s).';
 $string['pluginname'] = 'File submissions';
+$string['privacy:metadata:filepurpose'] = 'The files loaded for this assignment submission';
 $string['siteuploadlimit'] = 'Site upload limit';
 $string['submissionfilearea'] = 'Uploaded submission files';
 // Deprecated since Moodle 3.4.
diff --git a/mod/assign/submission/file/tests/privacy_test.php b/mod/assign/submission/file/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..61f6841
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignsubmission_file.
+ *
+ * @package    assignsubmission_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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/tests/privacy_test.php');
+
+/**
+ * Unit tests for mod/assign/submission/file/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignsubmission_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  string   $filename       filename for the file submission
+     * @return array   Submission plugin object and the submission object.
+     */
+    protected function create_file_submission($assign, $student, $filename) {
+        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' => $filename
+        );
+        $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);
+
+        return [$plugin, $submission];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignsubmission_file');
+        $collection = \assignsubmission_file\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test that submission files are exported for a user.
+     */
+    public function test_export_submission_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();
+
+        $studentfilename = 'user1file.pdf';
+        list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename);
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+
+        // The student should have a file submission.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, ['Attempt 1']);
+        \assignsubmission_file\privacy\provider::export_submission_user_data($exportdata);
+        // print_object($writer);
+        $storedfile = $writer->get_files(['Attempt 1'])['user1file.pdf'];
+        $this->assertInstanceOf('stored_file', $storedfile);
+        $this->assertEquals($studentfilename, $storedfile->get_filename());
+    }
+
+    /**
+     * Test that all submission files are deleted for this context.
+     */
+    public function test_delete_submission_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();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $studentfilename = 'user1file.pdf';
+        list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename);
+        $student2filename = 'user2file.pdf';
+        list($plugin2, $submission2) = $this->create_file_submission($assign, $user2, $studentfilename);
+
+        // Only need the context and assign object in this plugin for this operation.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        \assignsubmission_file\privacy\provider::delete_submission_for_context($requestdata);
+        // This checks that there are no files in this submission.
+        $this->assertTrue($plugin->is_empty($submission));
+        $this->assertTrue($plugin2->is_empty($submission2));
+    }
+
+    /**
+     * Test that the comments for a user are deleted.
+     */
+    public function test_delete_submission_for_userid() {
+        $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();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $studentfilename = 'user1file.pdf';
+        list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename);
+        $student2filename = 'user2file.pdf';
+        list($plugin2, $submission2) = $this->create_file_submission($assign, $user2, $studentfilename);
+
+        // Only need the context and assign object in this plugin for this operation.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, [], $user1);
+        \assignsubmission_file\privacy\provider::delete_submission_for_userid($requestdata);
+        // This checks that there are no files in this submission.
+        $this->assertTrue($plugin->is_empty($submission));
+        // There should be files here.
+        $this->assertFalse($plugin2->is_empty($submission2));
+    }
+}
diff --git a/mod/assign/submission/onlinetext/classes/privacy/provider.php b/mod/assign/submission/onlinetext/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..27a7d56
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_onlinetext
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignsubmission_onlinetext\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\writer;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignsubmission_onlinetext
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, \mod_assign\privacy\assignsubmission_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 {
+        $detail = [
+                    'assignment' => 'privacy:metadata:assignmentid',
+                    'submission' => 'privacy:metadata:submissionpurpose',
+                    'onlinetext' => 'privacy:metadata:textpurpose'
+                  ];
+        $collection->add_database_table('assignsubmission_onlinetext', $detail, 'privacy:metadata:tablepurpose');
+        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
+        return $collection;
+    }
+
+    /**
+     * This is covered by mod_assign provider and the query on assign_submissions.
+     *
+     * @param  int $userid The user ID that we are finding contexts for.
+     * @param  contextlist $contextlist A context list to add sql and params to for contexts.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) {
+        // This is already fetched from mod_assign.
+    }
+
+    /**
+     * This is also covered by the mod_assign provider and it's queries.
+     *
+     * @param  \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students.
+     */
+    public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) {
+        // No need.
+    }
+
+    /**
+     * 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_submission_user_data(assign_plugin_request_data $exportdata) {
+        // We currently don't show submissions to teachers when exporting their data.
+        if ($exportdata->get_user() != null) {
+            return null;
+        }
+        // Retrieve text for this submission.
+        $assign = $exportdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignsubmission', 'onlinetext');
+        $submission = $exportdata->get_pluginobject();
+        $editortext = $plugin->get_editor_text('onlinetext', $submission->id);
+        $context = $exportdata->get_context();
+        if (!empty($editortext)) {
+            $submissiontext = new \stdClass();
+            $submissiontext->text = writer::with_context($context)->rewrite_pluginfile_urls([], '', '', '', $editortext);
+            $currentpath = $exportdata->get_subcontext();
+            $currentpath[] = get_string('privacy:path', 'assignsubmission_onlinetext');
+            writer::with_context($context)
+                    ->export_area_files($currentpath, 'assignsubmission_onlinetext', 'submissions_onlinetext', $submission->id)
+                    // Add the text to the exporter.
+                    ->export_data($currentpath, $submissiontext);
+
+            // Handle plagiarism data.
+            $coursecontext = $context->get_course_context();
+            $userid = $submission->userid;
+            \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $currentpath, [
+                'cmid' => $context->instanceid,
+                'course' => $coursecontext->instanceid,
+                'userid' => $userid,
+                'content' => $editortext,
+                'assignment' => $submission->assignment
+            ]);
+        }
+    }
+
+    /**
+     * 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_submission_for_context(assign_plugin_request_data $requestdata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context());
+
+        // Delete related files.
+        $fs = get_file_storage();
+        $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_onlinetext',
+                ASSIGNSUBMISSION_ONLINETEXT_FILEAREA);
+
+        // Delete the records in the table.
+        $DB->delete_records('assignsubmission_onlinetext', ['assignment' => $requestdata->get_assign()->get_instance()->id]);
+    }
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     *
+     * @param  assign_plugin_request_data $deletedata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context());
+
+        $submissionid = $deletedata->get_pluginobject()->id;
+
+        // Delete related files.
+        $fs = get_file_storage();
+        $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA,
+                $submissionid);
+
+        // Delete the records in the table.
+        $DB->delete_records('assignsubmission_onlinetext', ['assignment' => $deletedata->get_assign()->get_instance()->id,
+                'submission' => $submissionid]);
+    }
+}
index bb4e383..1b65578 100644 (file)
@@ -32,9 +32,15 @@ $string['nosubmission'] = 'Nothing has been submitted for this assignment';
 $string['onlinetext'] = 'Online text';
 $string['onlinetextfilename'] = 'onlinetext.html';
 $string['onlinetextsubmission'] = 'Allow online text submission';
-$string['pluginname'] = 'Online text submissions';
 $string['numwords'] = '({$a} words)';
 $string['numwordsforlog'] = 'Submission word count: {$a} words';
+$string['pluginname'] = 'Online text submissions';
+$string['privacy:metadata:assignmentid'] = 'Assignment identifier';
+$string['privacy:metadata:filepurpose'] = 'Files that are embedded in the text submission.';
+$string['privacy:metadata:submissionpurpose'] = 'The submission ID that links to submissions for the user.';
+$string['privacy:metadata:tablepurpose'] = 'Stores the text submission for each attempt.';
+$string['privacy:metadata:textpurpose'] = 'The actual text submitted for this attempt of the assignment.';
+$string['privacy:path'] = 'Submission Text';
 $string['wordlimit'] = 'Word limit';
 $string['wordlimit_help'] = 'If online text submissions are enabled, this is the maximum number ' .
         'of words that each student will be allowed to submit.';
diff --git a/mod/assign/submission/onlinetext/tests/privacy_test.php b/mod/assign/submission/onlinetext/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..ce1dd50
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignsubmission_onlinetext.
+ *
+ * @package    assignsubmission_onlinetext
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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/tests/privacy_test.php');
+
+/**
+ * Unit tests for mod/assign/submission/onlinetext/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignsubmission_online_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  string   $text           Submission text.
+     * @return array   Submission plugin object and the submission object.
+     */
+    protected function create_online_submission($assign, $student, $text) {
+        global $CFG;
+
+        $this->setUser($student->id);
+        $submission = $assign->get_user_submission($student->id, true);
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => $text,
+            'format' => FORMAT_PLAIN
+        );
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        return [$plugin, $submission];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignsubmission_file');
+        $collection = \assignsubmission_onlinetext\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test that submission files and text are exported for a user.
+     */
+    public function test_export_submission_user_data() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $submissiontext = 'Just some text';
+        list($plugin, $submission) = $this->create_online_submission($assign, $user1, $submissiontext);
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+
+        // The student should have some text submitted.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, ['Attempt 1']);
+        \assignsubmission_onlinetext\privacy\provider::export_submission_user_data($exportdata);
+        $this->assertEquals($submissiontext, $writer->get_data(['Attempt 1',
+                get_string('privacy:path', 'assignsubmission_onlinetext')])->text);
+    }
+
+    /**
+     * Test that all submission files are deleted for this context.
+     */
+    public function test_delete_submission_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();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $studenttext = 'Student one\'s text.';
+        list($plugin, $submission) = $this->create_online_submission($assign, $user1, $studenttext);
+        $studenttext2 = 'Student two\'s text.';
+        list($plugin2, $submission2) = $this->create_online_submission($assign, $user2, $studenttext2);
+
+        // Only need the context and assign object in this plugin for this operation.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        \assignsubmission_onlinetext\privacy\provider::delete_submission_for_context($requestdata);
+        // This checks that there is no content for these submissions.
+        $this->assertTrue($plugin->is_empty($submission));
+        $this->assertTrue($plugin2->is_empty($submission2));
+    }
+
+    /**
+     * Test that the comments for a user are deleted.
+     */
+    public function test_delete_submission_for_userid() {
+        $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();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $studenttext = 'Student one\'s text.';
+        list($plugin, $submission) = $this->create_online_submission($assign, $user1, $studenttext);
+        $studenttext2 = 'Student two\'s text.';
+        list($plugin2, $submission2) = $this->create_online_submission($assign, $user2, $studenttext2);
+
+        // Need more data for this operation.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, [], $user1);
+        \assignsubmission_onlinetext\privacy\provider::delete_submission_for_userid($requestdata);
+        // This checks that there is no content for the first submission.
+        $this->assertTrue($plugin->is_empty($submission));
+        // But there is for the second submission.
+        $this->assertFalse($plugin2->is_empty($submission2));
+    }
+}
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 (file)
index 0000000..94f3ebf
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Unit tests for the privacy legacy polyfill for mod_assign.
+ *
+ * @package     mod_assign
+ * @category    test
+ * @copyright   2018 Adrian Greeve <adriangreeve.com>
+ * @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 <adriangreeve.com>
+ * @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 <adriangreeve.com>
+ * @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 <adriangreeve.com>
+ * @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() {
+    }
+}
diff --git a/mod/assign/tests/privacy_submission_legacy_polyfill_test.php b/mod/assign/tests/privacy_submission_legacy_polyfill_test.php
new file mode 100644 (file)
index 0000000..41ab5e7
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Unit tests for the privacy legacy polyfill for mod_assign.
+ *
+ * @package     mod_assign
+ * @category    test
+ * @copyright   2018 Adrian Greeve <adriangreeve.com>
+ * @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/submissionplugin.php');
+require_once($CFG->dirroot . '/mod/assign/submission/comments/locallib.php');
+
+/**
+ * Unit tests for the assignment submission subplugins API's privacy legacy_polyfill.
+ *
+ * @copyright   2018 Adrian Greeve <adriangreeve.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_assignsubmission_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_submission shim.
+     */
+    public function test_get_context_for_userid_within_submission() {
+        $userid = 21;
+        $contextlist = new \core_privacy\local\request\contextlist();
+        $mock = $this->createMock(test_assignsubmission_legacy_polyfill_mock_wrapper::class);
+        $mock->expects($this->once())
+            ->method('get_return_value')
+            ->with('_get_context_for_userid_within_submission', [$userid, $contextlist]);
+        test_legacy_polyfill_submission_provider::$mock = $mock;
+        test_legacy_polyfill_submission_provider::get_context_for_userid_within_submission($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_assignsubmission_legacy_polyfill_mock_wrapper::class);
+        $mock->expects($this->once())
+            ->method('get_return_value')
+            ->with('_get_student_user_ids', [$useridlist]);
+        test_legacy_polyfill_submission_provider::$mock = $mock;
+        test_legacy_polyfill_submission_provider::get_student_user_ids($useridlist);
+    }
+
+    /**
+     * Test the export_submission_user_data shim.
+     */
+    public function test_export_submission_user_data() {
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->create_instance(['course' => $course]);
+        $context = context_system::instance();
+        $subplugin = new assign_submission_comments($assign, 'comment');
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        $mock = $this->createMock(test_assignsubmission_legacy_polyfill_mock_wrapper::class);
+        $mock->expects($this->once())
+            ->method('get_return_value')
+            ->with('_export_submission_user_data', [$requestdata]);
+        test_legacy_polyfill_submission_provider::$mock = $mock;
+        test_legacy_polyfill_submission_provider::export_submission_user_data($requestdata);
+    }
+
+    /**
+     * Test the delete_submission_for_context shim.
+     */
+    public function test_delete_submission_for_context() {
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->create_instance(['course' => $course]);
+        $context = context_system::instance();
+        $subplugin = new assign_submission_comments($assign, 'comment');
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        $mock = $this->createMock(test_assignsubmission_legacy_polyfill_mock_wrapper::class);
+        $mock->expects($this->once())
+            ->method('get_return_value')
+            ->with('_delete_submission_for_context', [$requestdata]);
+        test_legacy_polyfill_submission_provider::$mock = $mock;
+        test_legacy_polyfill_submission_provider::delete_submission_for_context($requestdata);
+    }
+
+    /**
+     * Test the delete submission for grade shim.
+     */
+    public function test_delete_submission_for_userid() {
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->create_instance(['course' => $course]);
+        $context = context_system::instance();
+        $subplugin = new assign_submission_comments($assign, 'comment');
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        $mock = $this->createMock(test_assignsubmission_legacy_polyfill_mock_wrapper::class);
+        $mock->expects($this->once())
+            ->method('get_return_value')
+            ->with('_delete_submission_for_userid', [$requestdata]);
+        test_legacy_polyfill_submission_provider::$mock = $mock;
+        test_legacy_polyfill_submission_provider::delete_submission_for_userid($requestdata);
+    }
+}
+/**
+ * Legacy polyfill test class for the assignsubmission_provider.
+ *
+ * @copyright   2018 Adrian Greeve <adriangreeve.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_legacy_polyfill_submission_provider implements \mod_assign\privacy\assignsubmission_provider {
+    use \mod_assign\privacy\submission_legacy_polyfill;
+    /**
+     * @var test_legacy_polyfill_submission_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_submission(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 it is possible that a student ID will not be returned by
+     * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those
+     * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above.
+     *
+     * @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());
+    }
+
+    /**
+     * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the
+     * context and userid.
+     * 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_submission_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_submission_for_context(\mod_assign\privacy\assign_plugin_request_data $requestdata) {
+        static::$mock->get_return_value(__FUNCTION__, func_get_args());
+    }
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     * 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_submission_for_userid(\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 <adriangreeve.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_assignsubmission_legacy_polyfill_mock_wrapper {
+    /**
+     * Get the return value for the specified item.
+     */
+    public function get_return_value() {
+    }
+}
diff --git a/mod/assign/tests/privacy_test.php b/mod/assign/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..a9800bc
--- /dev/null
@@ -0,0 +1,562 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for unit tests for mod_assign.
+ *
+ * @package    mod_assign
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @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 <adrian@moodle.com>
+ * @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);
+    }
+}
index 06d30eb..8fc4e4c 100644 (file)
@@ -596,7 +596,7 @@ class mod_choice_external extends external_api {
                 'choiceid' => new external_value(PARAM_INT, 'choice instance id'),
                 'responses' => new external_multiple_structure(
                     new external_value(PARAM_INT, 'response id'),
-                    'Array of response ids, empty for deleting all the user responses',
+                    'Array of response ids, empty for deleting all the current user responses.',
                     VALUE_DEFAULT,
                     array()
                 ),
@@ -608,7 +608,7 @@ class mod_choice_external extends external_api {
      * Delete the given submitted responses in a choice
      *
      * @param int $choiceid the choice instance id
-     * @param array $responses the response ids,  empty for deleting all the user responses
+     * @param array $responses the response ids,  empty for deleting all the current user responses
      * @return array status information and warnings
      * @throws moodle_exception
      * @since Moodle 3.0
@@ -633,33 +633,38 @@ class mod_choice_external extends external_api {
 
         require_capability('mod/choice:choose', $context);
 
-        // If we have the capability, delete all the passed responses.
-        if (has_capability('mod/choice:deleteresponses', $context)) {
-            if (empty($params['responses'])) {
-                // Get all the responses for the choice.
-                $params['responses'] = array_keys(choice_get_all_responses($choice));
-            }
-            $status = choice_delete_responses($params['responses'], $choice, $cm, $course);
-        } else if ($choice->allowupdate) {
-            // Check if we can delate our own responses.
-            $timenow = time();
-            if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
-                throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
+        $candeleteall = has_capability('mod/choice:deleteresponses', $context);
+        if ($candeleteall || $choice->allowupdate) {
+
+            // Check if we can delete our own responses.
+            if (!$candeleteall) {
+                $timenow = time();
+                if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
+                    throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
+                }
             }
-            // Delete only our responses.
-            $myresponses = array_keys(choice_get_my_response($choice));
 
             if (empty($params['responses'])) {
-                $todelete = $myresponses;
+                // No responses indicated so delete only my responses.
+                $todelete = array_keys(choice_get_my_response($choice));
             } else {
+                // Fill an array with the responses that can be deleted for this choice.
+                if ($candeleteall) {
+                    // Teacher/managers can delete any.
+                    $allowedresponses = array_keys(choice_get_all_responses($choice));
+                } else {
+                    // Students can delete only their own responses.
+                    $allowedresponses = array_keys(choice_get_my_response($choice));
+                }
+
                 $todelete = array();
                 foreach ($params['responses'] as $response) {
-                    if (!in_array($response, $myresponses)) {
+                    if (!in_array($response, $allowedresponses)) {
                         $warnings[] = array(
                             'item' => 'response',
                             'itemid' => $response,
                             'warningcode' => 'nopermissions',
-                            'message' => 'No permission to delete this response'
+                            'message' => 'Invalid response id, the response does not exist or you are not allowed to delete it.'
                         );
                     } else {
                         $todelete[] = $response;
index ee568e2..fb5e6de 100644 (file)
@@ -111,7 +111,7 @@ $string['previewonly'] = 'This is just a preview of the available options for th
 $string['privacy'] = 'Privacy of results';
 $string['privacy:metadata:choice_answers'] = 'Information about the user\'s chosen answer(s) for a given choice activity';
 $string['privacy:metadata:choice_answers:choiceid'] = 'The ID of the choice activity the user is providing answer for';
-$string['privacy:metadata:choice_answers:optionid'] = 'The ID of option that the user selected';
+$string['privacy:metadata:choice_answers:optionid'] = 'The ID of the option that the user selected.';
 $string['privacy:metadata:choice_answers:userid'] = 'The ID of the user answering this choice activity';