Merge branch 'MDL-46513_master' of git://github.com/markn86/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 6 Jun 2018 07:59:21 +0000 (15:59 +0800)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 6 Jun 2018 09:16:34 +0000 (11:16 +0200)
271 files changed:
.travis.yml
admin/roles/classes/privacy/provider.php
admin/tool/customlang/db/upgrade.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/manager_observer.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/task/initiate_data_request_task.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/db/messages.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/tests/manager_observer_test.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/classes/privacy/provider.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/mobile/classes/api.php
admin/tool/monitor/db/upgrade.php
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/classes/privacy/provider.php
admin/tool/policy/index.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/tests/behat/consent.feature
admin/tool/policy/tests/privacy_provider_test.php
admin/tool/recyclebin/classes/category_bin.php
admin/tool/recyclebin/classes/course_bin.php
admin/tool/usertours/db/upgrade.php
auth/cas/db/upgrade.php
auth/db/db/upgrade.php
auth/email/db/upgrade.php
auth/ldap/db/upgrade.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
auth/none/db/upgrade.php
auth/oauth2/classes/privacy/provider.php
auth/oauth2/db/upgrade.php
auth/shibboleth/db/upgrade.php
backup/backup.class.php
blocks/badges/db/upgrade.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_summary/db/upgrade.php
blocks/html/db/upgrade.php
blocks/navigation/db/upgrade.php
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/classes/privacy/provider.php
blocks/recent_activity/db/upgrade.php
blocks/recent_activity/lang/en/block_recent_activity.php
blocks/rss_client/classes/privacy/provider.php
blocks/rss_client/db/upgrade.php
blocks/rss_client/tests/privacy_test.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
calendar/classes/privacy/provider.php
cohort/classes/privacy/provider.php
composer.json
composer.lock
course/classes/management_renderer.php
course/format/renderer.php
course/format/topics/db/upgrade.php
course/format/weeks/db/upgrade.php
course/management.php
course/renderer.php
course/tests/courselib_test.php
enrol/classes/privacy/provider.php
enrol/database/db/upgrade.php
enrol/flatfile/classes/privacy/provider.php
enrol/flatfile/db/upgrade.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/flatfile/tests/privacy_provider_test.php [new file with mode: 0644]
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/lti/db/upgrade.php
enrol/manual/db/upgrade.php
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
filter/mathjaxloader/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/mediaplugin/styles.css
filter/tex/db/upgrade.php
grade/classes/privacy/provider.php
grade/grading/form/guide/db/upgrade.php
grade/grading/form/rubric/db/upgrade.php
grade/report/overview/db/upgrade.php
grade/report/user/db/upgrade.php
grade/tests/privacy_test.php
index.php
install/lang/tl/admin.php
install/lang/tl/error.php
install/lang/tl/install.php
lang/en/admin.php
lang/en/grades.php
lang/en/moodle.php
lang/en/portfolio.php
lang/en/user.php
lib/accesslib.php
lib/amd/build/tag.min.js
lib/amd/src/tag.js
lib/antivirus/clamav/db/upgrade.php
lib/behat/classes/behat_config_util.php
lib/classes/oauth2/api.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/privacy/provider.php
lib/classes/useragent.php
lib/db/upgrade.php
lib/ddl/mysql_sql_generator.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/atto/plugins/recordrtc/pix/i/audiortc.svg [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/i/videortc.svg [new file with mode: 0644]
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/editor/tinymce/readme_moodle.txt
lib/editor/tinymce/tiny_mce/3.5.11/themes/advanced/skins/moodle/ui.css
lib/filebrowser/tests/file_browser_test.php
lib/filestorage/file_storage.php
lib/filestorage/tests/file_storage_test.php
lib/ltiprovider/readme_moodle.txt
lib/ltiprovider/src/ToolProvider/ToolProvider.php
lib/moodlelib.php
lib/outputrenderers.php
lib/templates/single_select.mustache
lib/tests/accesslib_test.php
lib/tests/useragent_test.php
media/player/videojs/classes/plugin.php
message/output/email/db/upgrade.php
message/output/jabber/db/upgrade.php
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/db/upgrade.php
message/output/popup/mark_notification_read.php [new file with mode: 0644]
message/templates/message_area_contact.mustache
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/file/db/upgrade.php
mod/assign/renderable.php
mod/assign/renderer.php
mod/assign/styles.css
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assignment/classes/privacy/provider.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/chat/classes/privacy/provider.php
mod/chat/db/upgrade.php
mod/chat/lang/en/chat.php
mod/choice/db/upgrade.php
mod/data/db/upgrade.php
mod/feedback/db/upgrade.php
mod/folder/db/upgrade.php
mod/forum/classes/privacy/provider.php
mod/forum/db/upgrade.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/rsslib.php
mod/forum/styles.css
mod/forum/tests/privacy_provider_test.php
mod/forum/tests/rsslib_test.php [new file with mode: 0644]
mod/glossary/classes/privacy/provider.php
mod/glossary/db/upgrade.php
mod/glossary/import.php
mod/glossary/version.php
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/db/upgrade.php
mod/lesson/renderer.php
mod/lesson/tests/behat/lesson_delete_answers.feature
mod/lesson/tests/behat/lesson_edit_cluster.feature
mod/lesson/tests/behat/lesson_edit_pages.feature
mod/lti/classes/privacy/provider.php
mod/lti/db/upgrade.php
mod/lti/lang/en/lti.php
mod/lti/tests/privacy_provider_test.php
mod/page/db/upgrade.php
mod/quiz/db/upgrade.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/resource/db/upgrade.php
mod/scorm/db/upgrade.php
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
npm-shrinkwrap.json
package.json
pix/t/online.png [new file with mode: 0644]
pix/t/online.svg [new file with mode: 0644]
portfolio/boxnet/db/upgrade.php
portfolio/classes/privacy/provider.php
portfolio/googledocs/db/upgrade.php
portfolio/picasa/db/upgrade.php
portfolio/tests/privacy_provider_test.php
privacy/classes/manager.php
privacy/classes/manager_observer.php [new file with mode: 0644]
privacy/tests/fixtures/provider_a.php [new file with mode: 0644]
privacy/tests/fixtures/provider_throwing_exception.php [new file with mode: 0644]
privacy/tests/manager_test.php
privacy/tests/provider_test.php
question/behaviour/manualgraded/db/upgrade.php
question/tests/behat/edit_questions_standard_tags.feature [new file with mode: 0644]
question/type/calculated/db/upgrade.php
question/type/calculated/question.php
question/type/calculated/tests/variablesubstituter_test.php
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-debug.js
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-min.js
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd.js
question/type/ddimageortext/yui/src/ddimageortext/js/ddimageortext.js
question/type/ddmarker/db/upgrade.php
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd-debug.js
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd-min.js
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd.js
question/type/ddmarker/yui/src/ddmarker/js/ddmarker.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js
question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js
question/type/edit_question_form.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
repository/boxnet/db/upgrade.php
repository/dropbox/db/upgrade.php
repository/flickr/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/onedrive/db/upgrade.php
repository/picasa/db/upgrade.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modules.scss
theme/boost/upgrade.txt
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/more/db/upgrade.php
theme/upgrade.txt
user/classes/participants_table.php
user/classes/privacy/provider.php
user/templates/add_bulk_note.mustache
version.php
webservice/xmlrpc/locallib.php

index a1217e8..0b6690a 100644 (file)
@@ -56,7 +56,7 @@ matrix:
     include:
           # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
         - php: 7.2
-          env: DB=none     TASK=GRUNT   NVM_VERSION='8.9'
+          env: DB=none     TASK=GRUNT   NVM_VERSION='lts/carbon'
 
     exclude:
         # MySQL - it's just too slow.
index ee8bcea..e420f9e 100644 (file)
@@ -313,9 +313,6 @@ class provider implements
         // Don't belong to the modifier user.
 
         // Remove data from role_assignments.
-        if (empty($context)) {
-            return;
-        }
         $DB->delete_records('role_assignments', ['contextid' => $context->id]);
     }
     /**
index a7ff6a3..e771cc9 100644 (file)
@@ -38,5 +38,8 @@ function xmldb_tool_customlang_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 7bffc9f..05efe9c 100644 (file)
@@ -451,7 +451,7 @@ class api {
         $message->courseid          = $SITE->id;
         $message->component         = 'tool_dataprivacy';
         $message->name              = 'contactdataprotectionofficer';
-        $message->userfrom          = $requestedby;
+        $message->userfrom          = $requestedby->id;
         $message->replyto           = $requestedby->email;
         $message->replytoname       = $requestedby->fullname;
         $message->subject           = $subject;
index e71422d..3d20e5d 100644 (file)
@@ -91,6 +91,7 @@ abstract class expired_contexts_manager {
         }
 
         $privacymanager = new \core_privacy\manager();
+        $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
 
         foreach ($this->get_context_levels() as $level) {
 
diff --git a/admin/tool/dataprivacy/classes/manager_observer.php b/admin/tool/dataprivacy/classes/manager_observer.php
new file mode 100644 (file)
index 0000000..ca613a4
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * Class \tool_dataprivacy\manager_observer.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A failure observer for the \core_privacy\manager.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager_observer implements \core_privacy\manager_observer {
+    /**
+     * Notifies all DPOs that an exception occurred.
+     *
+     * @param \Throwable $e
+     * @param string $component
+     * @param string $interface
+     * @param string $methodname
+     * @param array $params
+     */
+    public function handle_component_failure($e, $component, $interface, $methodname, array $params) {
+        // Get the list of the site Data Protection Officers.
+        $dpos = api::get_site_dpos();
+
+        $messagesubject = get_string('exceptionnotificationsubject', 'tool_dataprivacy');
+        $a = (object)[
+            'fullmethodname' => \core_privacy\manager::get_provider_classname_for_component($component) . '::' . $methodname,
+            'component' => $component,
+            'message' => $e->getMessage(),
+            'backtrace' => $e->getTraceAsString()
+        ];
+        $messagebody = get_string('exceptionnotificationbody', 'tool_dataprivacy', $a);
+
+        // Email the data request to the Data Protection Officer(s)/Admin(s).
+        foreach ($dpos as $dpo) {
+            $message = new \core\message\message();
+            $message->courseid          = SITEID;
+            $message->component         = 'tool_dataprivacy';
+            $message->name              = 'notifyexceptions';
+            $message->userfrom          = \core_user::get_noreply_user();
+            $message->subject           = $messagesubject;
+            $message->fullmessageformat = FORMAT_HTML;
+            $message->notification      = 1;
+            $message->userto            = $dpo;
+            $message->fullmessagehtml   = $messagebody;
+            $message->fullmessage       = html_to_text($messagebody);
+
+            // Send message.
+            message_send($message);
+        }
+    }
+}
index 7607643..6282cc4 100644 (file)
@@ -40,6 +40,8 @@ class metadata_registry {
      */
     public function get_registry_metadata() {
         $manager = new \core_privacy\manager();
+        $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
         $pluginman = \core_plugin_manager::instance();
         $contributedplugins = $this->get_contrib_list();
         $metadata = $manager->get_metadata_for_components();
index 0cebeb0..2ee4a0a 100644 (file)
@@ -97,6 +97,8 @@ class initiate_data_request_task extends adhoc_task {
 
         // Add the list of relevant contexts to the request, and mark all as pending approval.
         $privacymanager = new \core_privacy\manager();
+        $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+
         $contextlistcollection = $privacymanager->get_contexts_for_userid($datarequest->get('userid'));
         api::add_request_contexts_with_status($contextlistcollection, $requestid, contextlist_context::STATUS_PENDING);
 
index 589ef01..6a93217 100644 (file)
@@ -88,6 +88,8 @@ class process_data_request_task extends adhoc_task {
 
             // Export the data.
             $manager = new \core_privacy\manager();
+            $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
             $exportedcontent = $manager->export_user_data($approvedclcollection);
 
             $fs = get_file_storage();
@@ -110,6 +112,8 @@ class process_data_request_task extends adhoc_task {
 
             // Delete the data.
             $manager = new \core_privacy\manager();
+            $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
             $manager->delete_data_for_user($approvedclcollection);
         }
 
index cd7f575..83a5c90 100644 (file)
@@ -53,8 +53,9 @@ if ($manage) {
 $PAGE->set_context($context);
 
 // If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
-if (!\tool_dataprivacy\api::can_contact_dpo()) {
-    redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), \core\output\notification::NOTIFY_ERROR);
+// That is, unless you have sufficient capabilities to perform this on behalf of a user.
+if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
+    redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
 }
 
 $mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
index 685d9b2..2c1d239 100644 (file)
@@ -41,4 +41,12 @@ $messageproviders = [
             'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
         ]
     ],
+
+    // Notify Data Protection Officer about exceptions.
+    'notifyexceptions' => [
+        'defaults' => [
+            'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+        ],
+        'capability'  => 'tool/dataprivacy:managedatarequests'
+    ],
 ];
index 8b089c7..40dc1e7 100644 (file)
@@ -101,6 +101,8 @@ $string['errorrequestalreadyexists'] = 'You already have an ongoing request.';
 $string['errorrequestnotfound'] = 'Request not found';
 $string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.';
 $string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.';
+$string['exceptionnotificationsubject'] = "Exception occured while processing privacy data";
+$string['exceptionnotificationbody'] = "<p>Exception occured while calling <b>{\$a->fullmethodname}</b>.<br>This means that plugin <b>{\$a->component}</b> did not complete processing data. Below you can find exception information that can be passed to the plugin developer.</p><pre>{\$a->message}<br>\n\n{\$a->backtrace}</pre>";
 $string['expiredretentionperiodtask'] = 'Expired retention period';
 $string['expiry'] = 'Expiry';
 $string['expandplugin'] = 'Expand and collapse plugin.';
@@ -148,6 +150,7 @@ $string['lawfulbases'] = 'Lawful bases';
 $string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see <a href="https://gdpr-info.eu/art-6-gdpr/" target="_blank">GDPR Art. 6.1</a>';
 $string['messageprovider:contactdataprotectionofficer'] = 'Data requests';
 $string['messageprovider:datarequestprocessingresults'] = 'Data request processing results';
+$string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications';
 $string['message'] = 'Message';
 $string['messagelabel'] = 'Message:';
 $string['moduleinstancename'] = '{$a->instancename} ({$a->modulename})';
diff --git a/admin/tool/dataprivacy/tests/manager_observer_test.php b/admin/tool/dataprivacy/tests/manager_observer_test.php
new file mode 100644 (file)
index 0000000..6c71027
--- /dev/null
@@ -0,0 +1,117 @@
+<?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/>.
+
+/**
+ * Tests for the manager observer.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * API tests.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_manager_observer_testcase extends advanced_testcase {
+
+    /**
+     * Helper to set andn return two users who are DPOs.
+     */
+    protected function setup_site_dpos() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $generator = new testing_data_generator();
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+
+        $context = context_system::instance();
+
+        // Give the manager role with the capability to manage data requests.
+        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+        // Assign both users as manager.
+        role_assign($managerroleid, $u1->id, $context->id);
+        role_assign($managerroleid, $u2->id, $context->id);
+
+        // Only map the manager role to the DPO role.
+        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+
+        return \tool_dataprivacy\api::get_site_dpos();
+    }
+
+    /**
+     * Ensure that when users are configured as DPO, they are sent an message upon failure.
+     */
+    public function test_handle_component_failure() {
+        $this->resetAfterTest();
+
+        // Create another user who is not a DPO.
+        $this->getDataGenerator()->create_user();
+
+        // Create the DPOs.
+        $dpos = $this->setup_site_dpos();
+
+        $observer = new \tool_dataprivacy\manager_observer();
+
+        // Handle the failure, catching messages.
+        $mailsink = $this->redirectMessages();
+        $mailsink->clear();
+        $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
+
+        // Messages should be sent to both DPOs only.
+        $this->assertEquals(2, $mailsink->count());
+
+        $messages = $mailsink->get_messages();
+        $messageusers = array_map(function($message) {
+            return $message->useridto;
+        }, $messages);
+
+        $this->assertEquals(array_keys($dpos), $messageusers, '', 0.0, 0, true);
+    }
+
+    /**
+     * Ensure that when no user is configured as DPO, the message is sent to admin instead.
+     */
+    public function test_handle_component_failure_no_dpo() {
+        $this->resetAfterTest();
+
+        // Create another user who is not a DPO or admin.
+        $this->getDataGenerator()->create_user();
+
+        $observer = new \tool_dataprivacy\manager_observer();
+
+        $mailsink = $this->redirectMessages();
+        $mailsink->clear();
+        $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
+
+        // Messages should have been sent only to the admin.
+        $this->assertEquals(1, $mailsink->count());
+
+        $messages = $mailsink->get_messages();
+        $message = reset($messages);
+
+        $admin = \core_user::get_user_by_username('admin');
+        $this->assertEquals($admin->id, $message->useridto);
+    }
+}
index 3561d57..7b7bde4 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018051400;
+$plugin->version   = 2018051401;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 06d7441..2c7e2b0 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_tool_log_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index f9c8a8b..7b6fed2 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_logstore_database_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 2761ca2..4ee8007 100644 (file)
@@ -51,7 +51,7 @@ class provider implements
      * @return collection A listing of user data stored through this system.
      */
     public static function get_metadata(collection $collection) : collection {
-        $collection->add_database_table('log', [
+        $collection->add_database_table('logstore_standard_log', [
             'eventname' => 'privacy:metadata:log:eventname',
             'userid' => 'privacy:metadata:log:userid',
             'relateduserid' => 'privacy:metadata:log:relateduserid',
index 610b61c..f0dd415 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_logstore_standard_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 43c8464..d6c88c3 100644 (file)
@@ -343,7 +343,8 @@ class api {
         $availablemods = core_plugin_manager::instance()->get_plugins_of_type('mod');
         $coursemodules = array();
         $appsupportedmodules = array('assign', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'imscp',
-                                        'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki');
+            'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop');
+
         foreach ($availablemods as $mod) {
             if (in_array($mod->name, $appsupportedmodules)) {
                 $coursemodules['$mmCourseDelegate_mmaMod' . ucfirst($mod->name)] = $mod->displayname;
index 1358647..67fbb42 100644 (file)
@@ -76,5 +76,8 @@ function xmldb_tool_monitor_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5ede161..af6ebd6 100644 (file)
@@ -69,6 +69,9 @@ class page_agreedocs implements renderable, templatable {
     /** @var array Info or error messages to show. */
     protected $messages = [];
 
+    /** @var bool This is an existing user (rather than non-loggedin/guest). */
+    protected $isexistinguser;
+
     /**
      * Prepare the page for rendering.
      *
@@ -87,6 +90,7 @@ class page_agreedocs implements renderable, templatable {
 
         $this->action = $action;
 
+        $this->isexistinguser = isloggedin() && !isguestuser();
         $behalfid = $behalfid ?: $USER->id;
         if ($realuser->id != $behalfid) {
             $this->behalfuser = core_user::get_user($behalfid, '*', MUST_EXIST);
@@ -112,7 +116,7 @@ class page_agreedocs implements renderable, templatable {
     protected function accept_and_revoke_policies() {
         global $USER;
 
-        if (!empty($USER->id)) {
+        if ($this->isexistinguser) {
             // Existing user.
             if (!empty($this->action) && confirm_sesskey()) {
                 // The form has been sent. Update policies acceptances according to $this->agreedocs.
@@ -182,15 +186,13 @@ class page_agreedocs implements renderable, templatable {
      * Before display the consent page, the user has to view all the still-non-accepted policy docs.
      * This function checks if the non-accepted policy docs have been shown and redirect to them.
      *
-     * @param array $userid User identifier who wants to access to the consent page.
-     * @param url $returnurl URL to return after shown the policy docs.
+     * @param int $userid User identifier who wants to access to the consent page.
+     * @param moodle_url $returnurl URL to return after shown the policy docs.
      */
     protected function redirect_to_policies($userid, $returnurl = null) {
-        global $USER;
-
-        $acceptances = api::get_user_acceptances($userid);
         $allpolicies = $this->policies;
-        if (!empty($userid)) {
+        if ($this->isexistinguser) {
+            $acceptances = api::get_user_acceptances($userid);
             foreach ($allpolicies as $policy) {
                 if (api::is_user_version_accepted($userid, $policy->id, $acceptances)) {
                     // If this version is accepted by the user, remove from the pending policies list.
@@ -234,16 +236,30 @@ class page_agreedocs implements renderable, templatable {
     }
 
     /**
-     * Redirect to $SESSION->wantsurl if defined or to $CFG->wwwroot if not.
+     * Redirect to signup page if defined or to $CFG->wwwroot if not.
      */
     protected function redirect_to_previous_url() {
         global $SESSION;
 
-        if (!empty($SESSION->wantsurl)) {
-            $returnurl = $SESSION->wantsurl;
-            unset($SESSION->wantsurl);
+        if ($this->isexistinguser) {
+            // Existing user.
+            if (!empty($SESSION->wantsurl)) {
+                $returnurl = $SESSION->wantsurl;
+                unset($SESSION->wantsurl);
+            } else {
+                $returnurl = new moodle_url('/admin/tool/policy/user.php');
+            }
         } else {
-            $returnurl = (new moodle_url('/admin/tool/policy/user.php'))->out();
+            // Non-authenticated user.
+            $issignup = \cache::make('core', 'presignup')->get('tool_policy_issignup');
+            if ($issignup) {
+                // User came here from signup page - redirect back there.
+                $returnurl = new moodle_url('/login/signup.php');
+                \cache::make('core', 'presignup')->set('tool_policy_issignup', false);
+            } else {
+                // Guests should not be on this page unless it's part of signup - redirect home.
+                $returnurl = new moodle_url('/');
+            }
         }
 
         redirect($returnurl);
@@ -255,35 +271,35 @@ class page_agreedocs implements renderable, templatable {
      * @param int $userid
      */
     protected function prepare_global_page_access($userid) {
-        global $PAGE, $SESSION, $SITE, $USER;
+        global $PAGE, $SITE, $USER;
 
         // Guest users or not logged users (but the users during the signup process) are not allowed to access to this page.
-        $newsignupuser = !empty($SESSION->wantsurl) && strpos($SESSION->wantsurl, 'login/signup.php') !== false;
-        if (isguestuser() || (empty($USER->id) && !$newsignupuser)) {
+        $newsignupuser = \cache::make('core', 'presignup')->get('tool_policy_issignup');
+        if (!$this->isexistinguser && !$newsignupuser) {
             $this->redirect_to_previous_url();
         }
 
         // Check for correct user capabilities.
-        if (!empty($USER->id)) {
+        if ($this->isexistinguser) {
             // For existing users, it's needed to check if they have the capability for accepting policies.
             api::can_accept_policies($this->behalfid, true);
         } else {
             // For new users, the behalfid parameter is ignored.
-            if ($this->behalfid != $USER->id) {
+            if ($this->behalfid) {
                 redirect(new moodle_url('/admin/tool/policy/index.php'));
             }
         }
 
         // If the current user has the $USER->policyagreed = 1 or $userpolicyagreed = 1
-        // and $SESSION->wantsurl is defined, redirect to the return page.
-        $hasagreedsignupuser = empty($USER->id) && $this->signupuserpolicyagreed;
+        // redirect to the return page.
+        $hasagreedsignupuser = !$this->isexistinguser && $this->signupuserpolicyagreed;
         $hasagreedloggeduser = $USER->id == $userid && !empty($USER->policyagreed);
         if (!is_siteadmin() && ($hasagreedsignupuser || $hasagreedloggeduser)) {
             $this->redirect_to_previous_url();
         }
 
         $myparams = [];
-        if (!empty($USER->id) && !empty($this->behalfid) && $this->behalfid != $USER->id) {
+        if ($this->isexistinguser && !empty($this->behalfid) && $this->behalfid != $USER->id) {
             $myparams['userid'] = $this->behalfid;
         }
         $myurl = new moodle_url('/admin/tool/policy/index.php', $myparams);
@@ -308,7 +324,6 @@ class page_agreedocs implements renderable, templatable {
         global $USER;
 
         // Get all the policy version acceptances for this user.
-        $acceptances = api::get_user_acceptances($userid);
         $lang = current_language();
         foreach ($this->policies as $policy) {
             // Get a link to display the full policy document.
@@ -320,9 +335,10 @@ class page_agreedocs implements renderable, templatable {
             $policymodal = html_writer::link($policy->url, $policy->name, $policyattributes);
 
             // Check if this policy version has been agreed or not.
-            if (!empty($userid)) {
+            if ($this->isexistinguser) {
                 // Existing user.
                 $versionagreed = false;
+                $acceptances = api::get_user_acceptances($userid);
                 $policy->versionacceptance = api::get_user_version_acceptance($userid, $policy->id, $acceptances);
                 if (!empty($policy->versionacceptance)) {
                     // The policy version has ever been agreed. Check if status = 1 to know if still is accepted.
@@ -352,13 +368,13 @@ class page_agreedocs implements renderable, templatable {
      * Export the page data for the mustache template.
      *
      * @param renderer_base $output renderer to be used to render the page elements.
-     * @return stdClass
+     * @return \stdClass
      */
     public function export_for_template(renderer_base $output) {
         global $USER;
 
         $myparams = [];
-        if (!empty($USER->id) && !empty($this->behalfid) && $this->behalfid != $USER->id) {
+        if ($this->isexistinguser && !empty($this->behalfid) && $this->behalfid != $USER->id) {
             $myparams['userid'] = $this->behalfid;
         }
         $data = (object) [
index 7de5edc..67e4e51 100644 (file)
@@ -49,11 +49,11 @@ class provider implements
     /**
      * Return the fields which contain personal data.
      *
-     * @param collection $items A reference to the collection to use to store the metadata.
-     * @return collection The updated collection of metadata items.
+     * @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 $items) : collection {
-        $items->add_database_table(
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table(
             'tool_policy_acceptances',
             [
                 'policyversionid' => 'privacy:metadata:acceptances:policyversionid',
@@ -68,7 +68,29 @@ class provider implements
             'privacy:metadata:acceptances'
         );
 
-        return $items;
+        $collection->add_database_table(
+            'tool_policy_versions',
+            [
+                'name' => 'privacy:metadata:versions:name',
+                'type' => 'privacy:metadata:versions:type',
+                'audience' => 'privacy:metadata:versions:audience',
+                'archived' => 'privacy:metadata:versions:archived',
+                'usermodified' => 'privacy:metadata:versions:usermodified',
+                'timecreated' => 'privacy:metadata:versions:timecreated',
+                'timemodified' => 'privacy:metadata:versions:timemodified',
+                'policyid' => 'privacy:metadata:versions:policyid',
+                'revision' => 'privacy:metadata:versions:revision',
+                'summary' => 'privacy:metadata:versions:summary',
+                'summaryformat' => 'privacy:metadata:versions:summaryformat',
+                'content' => 'privacy:metadata:versions:content',
+                'contentformat' => 'privacy:metadata:versions:contentformat',
+            ],
+            'privacy:metadata:versions'
+        );
+
+        $collection->add_subsystem_link('core_files', [], 'privacy:metadata:subsystem:corefiles');
+
+        return $collection;
     }
 
     /**
@@ -79,11 +101,34 @@ class provider implements
      */
     public static function get_contexts_for_userid(int $userid) : contextlist {
         $contextlist = new contextlist();
-        $contextlist->add_from_sql('SELECT DISTINCT c.id
-            FROM {tool_policy_acceptances} a
-            JOIN {context} c ON a.userid = c.instanceid AND c.contextlevel = ?
-            WHERE a.userid = ? OR a.usermodified = ?',
-            [CONTEXT_USER, $userid, $userid]);
+
+        // Policies a user has modified.
+        $sql = "SELECT c.id
+                  FROM {context} c
+                  JOIN {tool_policy_versions} v ON v.usermodified = :userid
+                 WHERE c.contextlevel = :contextlevel";
+        $params = [
+            'contextlevel' => CONTEXT_SYSTEM,
+            'userid' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Policies a user has accepted.
+        $sql = "SELECT c.id
+                  FROM {context} c
+                  JOIN {tool_policy_acceptances} a ON c.instanceid = a.userid
+                 WHERE
+                    c.contextlevel = :contextlevel
+                   AND (
+                    a.userid = :userid OR a.usermodified = :usermodified
+                   )";
+        $params = [
+            'contextlevel' => CONTEXT_USER,
+            'userid' => $userid,
+            'usermodified' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
         return $contextlist;
     }
 
@@ -94,39 +139,13 @@ class provider implements
      */
     public static function export_user_data(approved_contextlist $contextlist) {
         global $DB;
+
+        // Export user agreements.
         foreach ($contextlist->get_contexts() as $context) {
-            if ($context->contextlevel != CONTEXT_USER) {
-                continue;
-            }
-            $user = $contextlist->get_user();
-            $agreements = $DB->get_records_sql('SELECT a.id, a.userid, v.name, v.revision, a.usermodified, a.timecreated,
-                  a.timemodified, a.note, v.archived, p.currentversionid, a.status, a.policyversionid
-                FROM {tool_policy_acceptances} a
-                JOIN {tool_policy_versions} v ON v.id=a.policyversionid
-                JOIN {tool_policy} p ON v.policyid = p.id
-                WHERE a.userid = ? AND (a.userid = ? OR a.usermodified = ?)
-                ORDER BY a.userid, v.archived, v.timecreated DESC',
-                [$context->instanceid, $user->id, $user->id]);
-            foreach ($agreements as $agreement) {
-                $context = \context_user::instance($agreement->userid);
-                $subcontext = [
-                    get_string('userpoliciesagreements', 'tool_policy'),
-                    transform::user($agreement->userid)
-                ];
-                $name = 'policyagreement-' . $agreement->policyversionid;
-                $agreementcontent = (object) [
-                    'userid' => transform::user($agreement->userid),
-                    'status' => $agreement->status,
-                    'versionid' => $agreement->policyversionid,
-                    'name' => $agreement->name,
-                    'revision' => $agreement->revision,
-                    'isactive' => transform::yesno($agreement->policyversionid == $agreement->currentversionid),
-                    'usermodified' => transform::user($agreement->usermodified),
-                    'timecreated' => transform::datetime($agreement->timecreated),
-                    'timemodified' => transform::datetime($agreement->timemodified),
-                    'note' => $agreement->note,
-                ];
-                writer::with_context($context)->export_related_data($subcontext, $name, $agreementcontent);
+            if ($context->contextlevel == CONTEXT_USER) {
+                static::export_policy_agreements_for_context($context);
+            } else if ($context->contextlevel == CONTEXT_SYSTEM) {
+                static::export_authored_policies($contextlist->get_user());
             }
         }
     }
@@ -135,6 +154,7 @@ class provider implements
      * Delete all data for all users in the specified context.
      *
      * We never delete user agreements to the policies because they are part of privacy data.
+     * We never delete policy versions because they are part of privacy data.
      *
      * @param \context $context The context to delete in.
      */
@@ -145,9 +165,149 @@ class provider implements
      * Delete all user data for the specified user, in the specified contexts.
      *
      * We never delete user agreements to the policies because they are part of privacy data.
+     * We never delete policy versions because they are part of privacy data.
      *
      * @param approved_contextlist $contextlist A list of contexts approved for deletion.
      */
     public static function delete_data_for_user(approved_contextlist $contextlist) {
     }
+
+    /**
+     * Export all policy agreements relating to the specified user context.
+     *
+     * @param \context_user $context The context to export
+     */
+    protected static function export_policy_agreements_for_context(\context_user $context) {
+        global $DB;
+
+        $sysctx = \context_system::instance();
+        $fs = get_file_storage();
+        $agreementsql = "
+            SELECT
+                a.id AS agreementid, a.userid, a.timemodified, a.note, a.status,
+                a.policyversionid AS versionid, a.usermodified, a.timecreated,
+                v.id, v.archived, v.name, v.revision,
+                v.summary, v.summaryformat,
+                v.content, v.contentformat,
+                p.currentversionid
+             FROM {tool_policy_acceptances} a
+             JOIN {tool_policy_versions} v ON v.id = a.policyversionid
+             JOIN {tool_policy} p ON v.policyid = p.id
+            WHERE a.userid = :userid OR a.usermodified = :usermodified";
+
+        // Fetch all agreements related to this user.
+        $agreements = $DB->get_recordset_sql($agreementsql, [
+            'userid' => $context->instanceid,
+            'usermodified' => $context->instanceid,
+        ]);
+
+        $basecontext = [
+            get_string('privacyandpolicies', 'admin'),
+            get_string('useracceptances', 'tool_policy'),
+        ];
+
+        foreach ($agreements as $agreement) {
+            $subcontext = array_merge($basecontext, [get_string('policynamedversion', 'tool_policy', $agreement)]);
+
+            $summary = writer::with_context($context)->rewrite_pluginfile_urls(
+                $subcontext,
+                'tool_policy',
+                'policydocumentsummary',
+                $agreement->versionid,
+                $agreement->summary
+            );
+            $content = writer::with_context($context)->rewrite_pluginfile_urls(
+                $subcontext,
+                'tool_policy',
+                'policydocumentcontent',
+                $agreement->versionid,
+                $agreement->content
+            );
+            $agreementcontent = (object) [
+                'name' => $agreement->name,
+                'revision' => $agreement->revision,
+                'isactive' => transform::yesno($agreement->versionid == $agreement->currentversionid),
+                'isagreed' => transform::yesno($agreement->status),
+                'agreedby' => transform::user($agreement->usermodified),
+                'timecreated' => transform::datetime($agreement->timecreated),
+                'timemodified' => transform::datetime($agreement->timemodified),
+                'note' => $agreement->note,
+                'summary' => format_text($summary, $agreement->summaryformat),
+                'content' => format_text($content, $agreement->contentformat),
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $agreementcontent);
+            // Manually export the files as they reside in the system context so we can't use
+            // the write's helper methods.
+            foreach ($fs->get_area_files($sysctx->id, 'tool_policy', 'policydocumentsummary', $agreement->versionid) as $file) {
+                writer::with_context($context)->export_file($subcontext, $file);
+            }
+            foreach ($fs->get_area_files($sysctx->id, 'tool_policy', 'policydocumentcontent', $agreement->versionid) as $file) {
+                writer::with_context($context)->export_file($subcontext, $file);
+            }
+        }
+        $agreements->close();
+    }
+
+    /**
+     * Export all policy agreements that the user authored.
+     *
+     * @param stdClass $user The user who has created the policies to export.
+     */
+    protected static function export_authored_policies(\stdClass $user) {
+        global $DB;
+
+        // Authored policies are exported against the system.
+        $context = \context_system::instance();
+        $basecontext = [
+            get_string('policydocuments', 'tool_policy'),
+        ];
+
+        $sql = "SELECT v.id,
+                       v.name,
+                       v.revision,
+                       v.summary,
+                       v.content,
+                       v.archived,
+                       v.usermodified,
+                       v.timecreated,
+                       v.timemodified,
+                       p.currentversionid
+                  FROM {tool_policy_versions} v
+                  JOIN {tool_policy} p ON p.id = v.policyid
+                 WHERE v.usermodified = :userid";
+        $versions = $DB->get_recordset_sql($sql, ['userid' => $user->id]);
+        foreach ($versions as $version) {
+            $subcontext = array_merge($basecontext, [get_string('policynamedversion', 'tool_policy', $version)]);
+
+            $versioncontent = (object) [
+                'name' => $version->name,
+                'revision' => $version->revision,
+                'summary' => writer::with_context($context)->rewrite_pluginfile_urls(
+                    $subcontext,
+                    'tool_policy',
+                    'policydocumentsummary',
+                    $version->id,
+                    $version->summary
+                ),
+                'content' => writer::with_context($context)->rewrite_pluginfile_urls(
+                    $subcontext,
+                    'tool_policy',
+                    'policydocumentcontent',
+                    $version->id,
+                    $version->content
+                ),
+                'isactive' => transform::yesno($version->id == $version->currentversionid),
+                'isarchived' => transform::yesno($version->archived),
+                'createdbyme' => transform::yesno($version->usermodified == $user->id),
+                'timecreated' => transform::datetime($version->timecreated),
+                'timemodified' => transform::datetime($version->timemodified),
+            ];
+            writer::with_context($context)
+                ->export_data($subcontext, $versioncontent)
+                ->export_area_files($subcontext, 'tool_policy', 'policydocumentsummary', $version->id)
+                ->export_area_files($subcontext, 'tool_policy', 'policydocumentcontent', $version->id);
+        }
+        $versions->close();
+    }
 }
index 4be8554..067d8fa 100644 (file)
@@ -45,7 +45,7 @@ $PAGE->set_pagelayout('standard');
 $PAGE->set_url('/admin/tool/policy/index.php');
 $PAGE->set_popup_notification_allowed(false);
 
-if (!empty($USER->id)) {
+if (isloggedin() && !isguestuser()) {
     // Existing user.
     $haspermissionagreedocs = api::can_accept_policies($behalfid);
 } else {
index efd944d..c451c1a 100644 (file)
@@ -124,18 +124,35 @@ $string['policydoctype0'] = 'Site policy';
 $string['policydoctype1'] = 'Privacy policy';
 $string['policydoctype2'] = 'Third parties policy';
 $string['policydoctype99'] = 'Other policy';
+$string['policydocuments'] = 'Policy documents';
+$string['policynamedversion'] = 'Policy {$a->name} (version {$a->revision} - {$a->id})';
 $string['policyversionacceptedinbehalf'] = 'Consent for this policy has been given on your behalf.';
 $string['policyversionacceptedinotherlang'] = 'Consent for this policy version has been given in a different language.';
 $string['previousversions'] = '{$a} previous versions';
-$string['privacy:metadata:acceptances'] = 'Information from policy agreements made by site users';
-$string['privacy:metadata:acceptances:policyversionid'] = 'The ID of the accepted version 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.';
-$string['privacy:metadata:acceptances:timecreated'] = 'The time when the user agreed to the policy';
-$string['privacy:metadata:acceptances:timemodified'] = 'The time when the user modified their agreement';
-$string['privacy:metadata:acceptances:note'] = 'Any comments added by a user when giving consent on behalf of another user';
+$string['privacy:metadata:acceptances'] = 'Information about policy agreements made by users.';
+$string['privacy:metadata:acceptances:policyversionid'] = 'The version of the policy for which consent was given.';
+$string['privacy:metadata:acceptances:userid'] = 'The user for whom this policy agreement relates to.';
+$string['privacy:metadata:acceptances:status'] = 'The status of the agreement.';
+$string['privacy:metadata:acceptances:lang'] = 'The language used to display the policy when consent was given.';
+$string['privacy:metadata:acceptances:usermodified'] = 'The user who gave consent for the policy, if made on behalf of another user.';
+$string['privacy:metadata:acceptances:timecreated'] = 'The time when the user agreed to the policy.';
+$string['privacy:metadata:acceptances:timemodified'] = 'The time when the user updated their agreement.';
+$string['privacy:metadata:acceptances:note'] = 'Any comments added by a user when giving consent on behalf of another user.';
+$string['privacy:metadata:subsystem:corefiles'] = 'The policy tool stores files included in the summary and full policy.';
+$string['privacy:metadata:versions'] = 'Policy version information.';
+$string['privacy:metadata:versions:name'] = 'The name of the policy.';
+$string['privacy:metadata:versions:type'] = 'Policy type.';
+$string['privacy:metadata:versions:audience'] = 'The type of users required to give their consent.';
+$string['privacy:metadata:versions:archived'] = 'The policy status (active or inactive).';
+$string['privacy:metadata:versions:usermodified'] = 'The user who modified the policy.';
+$string['privacy:metadata:versions:timecreated'] = 'The time that this version of the policy was created.';
+$string['privacy:metadata:versions:timemodified'] = 'The time that this version of the policy was updated.';
+$string['privacy:metadata:versions:policyid'] = 'The policy that this version is associated with.';
+$string['privacy:metadata:versions:revision'] = 'The revision name of this version of the policy.';
+$string['privacy:metadata:versions:summary'] = 'The summary of this version of the policy.';
+$string['privacy:metadata:versions:summaryformat'] = 'The format of the summary field.';
+$string['privacy:metadata:versions:content'] = 'The content of this version of the policy.';
+$string['privacy:metadata:versions:contentformat'] = 'The format of the content field.';
 $string['privacysettings'] = 'Privacy settings';
 $string['readpolicy'] = 'Please read our {$a}';
 $string['refertofullpolicytext'] = 'Please refer to the full {$a} if you would like to review the text.';
@@ -154,7 +171,6 @@ $string['status2'] = 'Inactive';
 $string['useracceptancecount'] = '{$a->agreedcount} of {$a->userscount} ({$a->percent}%)';
 $string['useracceptancecountna'] = 'N/A';
 $string['useracceptances'] = 'User agreements';
-$string['userpoliciesagreements'] = 'User agreements to policies';
 $string['userpolicysettings'] = 'Policies';
 $string['usersaccepted'] = 'Agreements';
 $string['viewarchived'] = 'View previous versions';
index 6c9afa9..3073212 100644 (file)
@@ -116,7 +116,7 @@ function tool_policy_standard_footer_html() {
  * Hooks redirection to policy acceptance pages before sign up.
  */
 function tool_policy_pre_signup_requests() {
-    global $CFG, $SESSION;
+    global $CFG;
 
     // Do nothing if we are not set as the site policies handler.
     if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
@@ -127,7 +127,7 @@ function tool_policy_pre_signup_requests() {
     $userpolicyagreed = cache::make('core', 'presignup')->get('tool_policy_userpolicyagreed');
     if (!empty($policies) && !$userpolicyagreed) {
         // Redirect to "Policy" pages for consenting before creating the user.
-        $SESSION->wantsurl = (new \moodle_url('/login/signup.php'))->out();
+        cache::make('core', 'presignup')->set('tool_policy_issignup', 1);
         redirect(new \moodle_url('/admin/tool/policy/index.php'));
     }
 }
index ec9aae6..fde43cc 100644 (file)
@@ -611,3 +611,55 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I should see "Policies and agreements"
     And I should see "No permission to agree to the policies on behalf of this user"
     And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
+
+  Scenario: Accept policy on sign up as a guest, one policy
+    Given the following config values are set as admin:
+      | registerauth    | email |
+      | passwordpolicy  | 0     |
+      | sitepolicyhandler | tool_policy |
+    Given the following policies exist:
+      | Policy | Name             | Revision | Content    | Summary     | Status   |
+      | P1     | This site policy |          | full text1 | short text1 | archived |
+      | P1     | This site policy |          | full text2 | short text2 | active   |
+      | P1     | This site policy |          | full text3 | short text3 | draft    |
+    And I am on site homepage
+    And I follow "Log in"
+    # First log in as a guest
+    And I press "Log in as a guest"
+    # Now sign up
+    And I follow "Log in"
+    When I press "Create new account"
+    Then I should see "This site policy"
+    And I should see "short text2"
+    And I should see "full text2"
+    And I press "Next"
+    And I should see "Please agree to the following policies"
+    And I should see "This site policy"
+    And I should see "short text2"
+    And I should not see "full text2"
+    And I set the field "I agree to the This site policy" to "1"
+    And I press "Next"
+    And I should not see "I understand and agree"
+    And I set the following fields to these values:
+      | Username      | user1                 |
+      | Password      | user1                 |
+      | Email address | user1@address.invalid |
+      | Email (again) | user1@address.invalid |
+      | First name    | User1                 |
+      | Surname       | L1                    |
+    And I press "Create my new account"
+    And I should see "Confirm your account"
+    And I should see "An email should have been sent to your address at user1@address.invalid"
+    And I confirm email for "user1"
+    And I should see "Thanks, User1 L1"
+    And I should see "Your registration has been confirmed"
+    And I open my profile in edit mode
+    And the field "First name" matches value "User1"
+    And I log out
+    # Confirm that user can login and browse the site.
+    And I log in as "user1"
+    And I follow "Profile" in the user menu
+    # User can see his own agreements in the profile.
+    And I follow "Policies and agreements"
+    And "Agreed" "icon" should exist in the "This site policy" "table_row"
+    And I log out
index d392cc1..78556c9 100644 (file)
@@ -42,6 +42,9 @@ class tool_policy_privacy_provider_testcase extends \core_privacy\tests\provider
     /** @var stdClass The user object. */
     protected $user;
 
+    /** @var stdClass The manager user object. */
+    protected $manager;
+
     /**
      * Setup function. Will create a user.
      */
@@ -50,31 +53,15 @@ class tool_policy_privacy_provider_testcase extends \core_privacy\tests\provider
 
         $generator = $this->getDataGenerator();
         $this->user = $generator->create_user();
-    }
 
-    /**
-     * Test for provider::get_metadata().
-     */
-    public function test_get_metadata() {
-        $collection = new collection('tool_policy');
-        $newcollection = provider::get_metadata($collection);
-        $itemcollection = $newcollection->get_collection();
-        $this->assertCount(1, $itemcollection);
-
-        $table = reset($itemcollection);
-        $this->assertEquals('tool_policy_acceptances', $table->get_name());
-
-        $privacyfields = $table->get_privacy_fields();
-        $this->assertArrayHasKey('policyversionid', $privacyfields);
-        $this->assertArrayHasKey('userid', $privacyfields);
-        $this->assertArrayHasKey('status', $privacyfields);
-        $this->assertArrayHasKey('lang', $privacyfields);
-        $this->assertArrayHasKey('usermodified', $privacyfields);
-        $this->assertArrayHasKey('timecreated', $privacyfields);
-        $this->assertArrayHasKey('timemodified', $privacyfields);
-        $this->assertArrayHasKey('note', $privacyfields);
-
-        $this->assertEquals('privacy:metadata:acceptances', $table->get_summary());
+        // Create manager user.
+        $this->manager = $generator->create_user();
+        $syscontext = context_system::instance();
+        $rolemanagerid = create_role('Policy manager', 'policymanager', 'Can manage policy documents');
+        assign_capability('tool/policy:managedocs', CAP_ALLOW, $rolemanagerid, $syscontext->id);
+        assign_capability('tool/policy:acceptbehalf', CAP_ALLOW, $rolemanagerid, $syscontext->id);
+        role_assign($rolemanagerid, $this->manager->id, $syscontext->id);
+        accesslib_clear_all_caches_for_unit_testing();
     }
 
     /**
@@ -84,16 +71,22 @@ class tool_policy_privacy_provider_testcase extends \core_privacy\tests\provider
         global $CFG;
 
         // When there are no policies or agreements context list is empty.
+        $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->manager->id);
+        $this->assertEmpty($contextlist);
         $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->user->id);
         $this->assertEmpty($contextlist);
 
         // Create a policy.
-        $this->setAdminUser();
+        $this->setUser($this->manager);
         $CFG->sitepolicyhandler = 'tool_policy';
         $policy = $this->add_policy();
         api::make_current($policy->get('id'));
 
-        // When there are no agreements context list is empty.
+        // After creating a policy, there should be manager context.
+        $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->manager->id);
+        $this->assertEquals(1, $contextlist->count());
+
+        // But when there are no agreements, user context list is empty.
         $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->user->id);
         $this->assertEmpty($contextlist);
 
@@ -106,13 +99,23 @@ class tool_policy_privacy_provider_testcase extends \core_privacy\tests\provider
         $this->assertEquals(1, $contextlist->count());
     }
 
-    public function test_export_own_agreements() {
-        global $CFG, $USER;
+    public function test_export_agreements() {
+        global $CFG;
+
+        $otheruser = $this->getDataGenerator()->create_user();
+        $otherusercontext = \context_user::instance($otheruser->id);
 
-        // Create policies and agree to them as admin.
-        $this->setAdminUser();
-        $admin = fullclone($USER);
-        $admincontext = \context_user::instance($admin->id);
+        // Create policies and agree to them as manager.
+        $this->setUser($this->manager);
+        $managercontext = \context_user::instance($this->manager->id);
+        $systemcontext = \context_system::instance();
+        $agreementsubcontext = [
+            get_string('privacyandpolicies', 'admin'),
+            get_string('useracceptances', 'tool_policy')
+        ];
+        $versionsubcontext = [
+            get_string('policydocuments', 'tool_policy')
+        ];
         $CFG->sitepolicyhandler = 'tool_policy';
         $policy1 = $this->add_policy();
         api::make_current($policy1->get('id'));
@@ -127,94 +130,161 @@ class tool_policy_privacy_provider_testcase extends \core_privacy\tests\provider
 
         // Request export for this user.
         $contextlist = provider::get_contexts_for_userid($this->user->id);
+        $this->assertCount(1, $contextlist);
         $this->assertEquals([$usercontext->id], $contextlist->get_contextids());
 
         $approvedcontextlist = new approved_contextlist($this->user, 'tool_policy', [$usercontext->id]);
         provider::export_user_data($approvedcontextlist);
 
-        // User can not see admin's agreements but can see his own.
-        $writer = writer::with_context($admincontext);
-        $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
-        $this->assertEmpty($dataadmin);
+        // User can not see manager's agreements but can see his own.
+        $writer = writer::with_context($managercontext);
+        $this->assertFalse($writer->has_any_data());
 
         $writer = writer::with_context($usercontext);
-        $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
-        $this->assertCount(2, (array) $datauser);
-        $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
-        $this->assertEquals($this->user->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
-        $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
-        $this->assertEquals($this->user->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
+        $this->assertTrue($writer->has_any_data());
+
+        // Test policy 1.
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy1->get('name'), $datauser->name);
+        $this->assertEquals($this->user->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+        // Test policy 2.
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy2->get('name'), $datauser->name);
+        $this->assertEquals($this->user->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
     }
 
-    public function test_export_agreements_on_behalf() {
-        global $CFG, $USER;
+    public function test_export_agreements_for_other() {
+        global $CFG;
+
+        $managercontext = \context_user::instance($this->manager->id);
+        $systemcontext = \context_system::instance();
+        $usercontext = \context_user::instance($this->user->id);
 
-        // Create policies.
-        $this->setAdminUser();
-        $admin = fullclone($USER);
+        // Create policies and agree to them as manager.
+        $this->setUser($this->manager);
+        $agreementsubcontext = [
+            get_string('privacyandpolicies', 'admin'),
+            get_string('useracceptances', 'tool_policy')
+        ];
+        $versionsubcontext = [
+            get_string('policydocuments', 'tool_policy')
+        ];
         $CFG->sitepolicyhandler = 'tool_policy';
         $policy1 = $this->add_policy();
         api::make_current($policy1->get('id'));
         $policy2 = $this->add_policy();
         api::make_current($policy2->get('id'));
-
-        // Agree to the policies for oneself and for another user.
-        $usercontext = \context_user::instance($this->user->id);
-        $admincontext = \context_user::instance($USER->id);
         api::accept_policies([$policy1->get('id'), $policy2->get('id')]);
-        api::accept_policies([$policy1->get('id'), $policy2->get('id')], $this->user->id, 'Mynote');
 
-        // Request export for this user.
-        $contextlist = provider::get_contexts_for_userid($this->user->id);
-        $this->assertEquals([$usercontext->id], $contextlist->get_contextids());
-
-        $writer = writer::with_context($usercontext);
-        $this->assertFalse($writer->has_any_data());
+        // Agree to the other user's policies.
+        api::accept_policies([$policy1->get('id'), $policy2->get('id')], $this->user->id, 'My note');
+
+        // Request export for the manager.
+        $contextlist = provider::get_contexts_for_userid($this->manager->id);
+        $this->assertCount(3, $contextlist);
+        $this->assertEquals(
+            [$managercontext->id, $usercontext->id, $systemcontext->id],
+            $contextlist->get_contextids(),
+            '',
+            0.0,
+            1,
+            true
+        );
 
         $approvedcontextlist = new approved_contextlist($this->user, 'tool_policy', [$usercontext->id]);
         provider::export_user_data($approvedcontextlist);
 
-        // User can not see admin's agreements but can see his own.
-        $writer = writer::with_context($admincontext);
-        $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
-        $this->assertEmpty($dataadmin);
+        // The user context has data.
+        $writer = writer::with_context($usercontext);
+        $this->assertTrue($writer->has_any_data());
 
+        // Test policy 1.
         $writer = writer::with_context($usercontext);
-        $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
-        $this->assertCount(2, (array) $datauser);
-        $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
-        $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
-        $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy1->get('id')]->note);
-        $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
-        $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
-        $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy2->get('id')]->note);
-
-        // Request export for the admin.
-        writer::reset();
-        $contextlist = provider::get_contexts_for_userid($USER->id);
-        $this->assertEquals([$admincontext->id, $usercontext->id], $contextlist->get_contextids(), '', 0.0, 10, true);
-
-        $approvedcontextlist = new approved_contextlist($USER, 'tool_policy', $contextlist->get_contextids());
-        provider::export_user_data($approvedcontextlist);
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy1->get('name'), $datauser->name);
+        $this->assertEquals($this->manager->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+        // Test policy 2.
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy2->get('name'), $datauser->name);
+        $this->assertEquals($this->manager->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
+    }
 
-        // Admin can see all four agreements.
-        $writer = writer::with_context($admincontext);
-        $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
-        $this->assertCount(2, (array) $dataadmin);
-        $this->assertEquals($policy1->get('name'), $dataadmin['policyagreement-'.$policy1->get('id')]->name);
-        $this->assertEquals($admin->id, $dataadmin['policyagreement-'.$policy1->get('id')]->usermodified);
-        $this->assertEquals($policy2->get('name'), $dataadmin['policyagreement-'.$policy2->get('id')]->name);
-        $this->assertEquals($admin->id, $dataadmin['policyagreement-'.$policy2->get('id')]->usermodified);
+    public function test_export_created_policies() {
+        global $CFG;
 
-        $writer = writer::with_context($usercontext);
-        $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
-        $this->assertCount(2, (array) $datauser);
-        $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
-        $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
-        $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy1->get('id')]->note);
-        $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
-        $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
-        $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy2->get('id')]->note);
+        // Create policies and agree to them as manager.
+        $this->setUser($this->manager);
+        $managercontext = \context_user::instance($this->manager->id);
+        $systemcontext = \context_system::instance();
+        $agreementsubcontext = [
+            get_string('privacyandpolicies', 'admin'),
+            get_string('useracceptances', 'tool_policy')
+        ];
+        $versionsubcontext = [
+            get_string('policydocuments', 'tool_policy')
+        ];
+        $CFG->sitepolicyhandler = 'tool_policy';
+        $policy1 = $this->add_policy();
+        api::make_current($policy1->get('id'));
+        $policy2 = $this->add_policy();
+        api::make_current($policy2->get('id'));
+        api::accept_policies([$policy1->get('id'), $policy2->get('id')]);
+
+        // Agree to the policies for oneself.
+        $contextlist = provider::get_contexts_for_userid($this->manager->id);
+        $this->assertCount(2, $contextlist);
+        $this->assertEquals([$managercontext->id, $systemcontext->id], $contextlist->get_contextids(), '', 0.0, 1, true);
+
+        $approvedcontextlist = new approved_contextlist($this->manager, 'tool_policy', $contextlist->get_contextids());
+        provider::export_user_data($approvedcontextlist);
+
+        // User has agreed to policies.
+        $writer = writer::with_context($managercontext);
+        $this->assertTrue($writer->has_any_data());
+
+        // Test policy 1.
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy1->get('name'), $datauser->name);
+        $this->assertEquals($this->manager->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+        // Test policy 2.
+        $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+        $datauser = $writer->get_data($subcontext);
+        $this->assertEquals($policy2->get('name'), $datauser->name);
+        $this->assertEquals($this->manager->id, $datauser->agreedby);
+        $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+        $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
+
+        // User can see policy documents.
+        $writer = writer::with_context($systemcontext);
+        $this->assertTrue($writer->has_any_data());
+
+        $subcontext = array_merge($versionsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+        $dataversion = $writer->get_data($subcontext);
+        $this->assertEquals($policy1->get('name'), $dataversion->name);
+        $this->assertEquals(get_string('yes'), $dataversion->createdbyme);
+
+        $subcontext = array_merge($versionsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+        $dataversion = $writer->get_data($subcontext);
+        $this->assertEquals($policy2->get('name'), $dataversion->name);
+        $this->assertEquals(get_string('yes'), $dataversion->createdbyme);
     }
 
     /**
index 26d8b53..b08b6a5 100644 (file)
@@ -288,13 +288,24 @@ class category_bin extends base_bin {
         global $DB;
 
         // Grab the course category context.
-        $context = \context_coursecat::instance($this->_categoryid);
-
-        // Delete the files.
-        $fs = get_file_storage();
-        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
-        foreach ($files as $file) {
-            $file->delete();
+        $context = \context_coursecat::instance($this->_categoryid, IGNORE_MISSING);
+        if (!empty($context)) {
+            // Delete the files.
+            $fs = get_file_storage();
+            $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
+        } else {
+            // Course category has been deleted. Find records using $item->id as this is unique for coursecat recylebin.
+            $files = $DB->get_recordset('files', [
+                'component' => 'tool_recyclebin',
+                'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
+                'itemid' => $item->id,
+            ]);
+            $fs = get_file_storage();
+            foreach ($files as $filer) {
+                $file = $fs->get_file_instance($filer);
+                $file->delete();
+            }
+            $files->close();
         }
 
         // Delete the record.
@@ -302,6 +313,11 @@ class category_bin extends base_bin {
             'id' => $item->id
         ));
 
+        // The coursecat might have been deleted, check we have a context before triggering event.
+        if (!$context) {
+            return;
+        }
+
         // Fire event.
         $event = \tool_recyclebin\event\category_bin_item_deleted::create(array(
             'objectid' => $item->id,
index e935d69..fb5d779 100644 (file)
@@ -274,13 +274,25 @@ class course_bin extends base_bin {
         global $DB;
 
         // Grab the course context.
-        $context = \context_course::instance($this->_courseid);
-
-        // Delete the files.
-        $fs = get_file_storage();
-        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id);
-        foreach ($files as $file) {
-            $file->delete();
+        $context = \context_course::instance($this->_courseid, IGNORE_MISSING);
+
+        if (!empty($context)) {
+            // Delete the files.
+            $fs = get_file_storage();
+            $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id);
+        } else {
+            // Course context has been deleted. Find records using $item->id as this is unique for course bin recyclebin.
+            $files = $DB->get_recordset('files', [
+                'component' => 'tool_recyclebin',
+                'filearea' => TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA,
+                'itemid' => $item->id,
+            ]);
+            $fs = get_file_storage();
+            foreach ($files as $filer) {
+                $file = $fs->get_file_instance($filer);
+                $file->delete();
+            }
+            $files->close();
         }
 
         // Delete the record.
index c564472..088b76b 100644 (file)
@@ -51,5 +51,8 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index f3999b3..f331335 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_cas_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 621a17e..57bdd1e 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_db_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 9af8c79..0ad4e2d 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_email_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 02f1323..2132366 100644 (file)
@@ -66,5 +66,8 @@ function xmldb_auth_ldap_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 03dbdb2..40499cd 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_manual_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 9db6da9..09690f0 100644 (file)
@@ -47,5 +47,8 @@ function xmldb_auth_mnet_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 981d9ee..aa1ad76 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_none_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5a3476a..76d34a5 100644 (file)
@@ -120,10 +120,6 @@ class provider implements
      * @param  \context $context The context to delete data for.
      */
     public static function delete_data_for_all_users_in_context(\context $context) {
-        if (empty($context)) {
-            return;
-        }
-
         if ($context->contextlevel != CONTEXT_USER) {
             return;
         }
index d213cce..4d7a093 100644 (file)
@@ -44,5 +44,8 @@ function xmldb_auth_oauth2_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 43e07dd..9dbf7c3 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_auth_shibboleth_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 66e5ce8..c09fb9a 100644 (file)
@@ -140,7 +140,7 @@ abstract class backup implements checksumable {
     /**
      * Usually same than major release zero version, mainly for informative/historic purposes.
      */
-    const RELEASE = '3.5';
+    const RELEASE = '3.6';
 
     /**
      * Cipher to be used in backup and restore operations.
index bb9b3cf..a352f44 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_block_badges_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 4403a77..81eec4e 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index dbe19a5..3e768b0 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index d01face..30d70e3 100644 (file)
@@ -55,5 +55,8 @@ function xmldb_block_community_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index f0837fc..ce5ac4b 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 763a538..d0872b4 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_course_summary_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5ad7f22..930ebde 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_block_html_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 07b4381..f6fca69 100644 (file)
@@ -64,5 +64,8 @@ function xmldb_block_navigation_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 18ee857..144a6d5 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 8e998d5..b024b5b 100644 (file)
 
 namespace block_recent_activity\privacy;
 
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -33,15 +37,61 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider,
+            \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns metadata.
+     *
+     * @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 {
+
+        // This plugin defines a db table but it is not considered personal data and, therefore, not exported or deleted.
+        $collection->add_database_table('block_recent_activity', [
+            'courseid' => 'privacy:metadata:block_recent_activity:courseid',
+            'cmid' => 'privacy:metadata:block_recent_activity:cmid',
+            'timecreated' => 'privacy:metadata:block_recent_activity:timecreated',
+            'userid' => 'privacy:metadata:block_recent_activity:userid',
+            'action' => 'privacy:metadata:block_recent_activity:action',
+            'modname' => 'privacy:metadata:block_recent_activity:modname'
+        ], 'privacy:metadata:block_recent_activity');
+
+        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 {
+        return new 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) {
+    }
+
+    /**
+     * 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) {
+    }
 
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Delete all user data for the specified user, in the specified contexts.
      *
-     * @return string
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
      */
-    public static function get_reason() : string {
-        return 'privacy:metadata';
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
     }
 }
index d9bf407..570c792 100644 (file)
@@ -56,5 +56,8 @@ function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 3f6eadb..a569a7a 100644 (file)
 
 $string['pluginname'] = 'Recent activity';
 $string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
+$string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days';
+$string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted';
+$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id';
+$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id';
+$string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)';
+$string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed';
+$string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action';
 $string['recent_activity:addinstance'] = 'Add a new recent activity block';
 $string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block';
 $string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block';
index edc8a2b..c1ea54f 100644 (file)
@@ -87,16 +87,21 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
      * @param approved_contextlist $contextlist The approved contexts to export information for.
      */
     public static function export_user_data(approved_contextlist $contextlist) {
+        $rssdata = [];
         $results = static::get_records($contextlist->get_user()->id);
         foreach ($results as $result) {
-            $data = (object) [
+            $rssdata[] = (object) [
                 'title' => $result->title,
                 'preferredtitle' => $result->preferredtitle,
                 'description' => $result->description,
                 'shared' => \core_privacy\local\request\transform::yesno($result->shared),
                 'url' => $result->url
             ];
-
+        }
+        if (!empty($rssdata)) {
+            $data = (object) [
+                'feeds' => $rssdata,
+            ];
             \core_privacy\local\request\writer::with_context($contextlist->current())->export_data([
                     get_string('pluginname', 'block_rss_client')], $data);
         }
index d968dda..c4888e8 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_block_rss_client_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index dfd37c1..901d198 100644 (file)
@@ -63,6 +63,7 @@ class block_rss_client_testcase extends provider_testcase {
         $user = $this->getDataGenerator()->create_user();
         $context = context_user::instance($user->id);
 
+        $this->add_rss_feed($user);
         $this->add_rss_feed($user);
 
         $writer = \core_privacy\local\request\writer::with_context($context);
@@ -70,11 +71,13 @@ class block_rss_client_testcase extends provider_testcase {
         $this->export_context_data_for_user($user->id, $context, 'block_rss_client');
 
         $data = $writer->get_data([get_string('pluginname', 'block_rss_client')]);
-        $this->assertEquals('BBC News - World', $data->title);
-        $this->assertEquals('World News', $data->preferredtitle);
-        $this->assertEquals('Description: BBC News - World', $data->description);
-        $this->assertEquals(get_string('no'), $data->shared);
-        $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $data->url);
+        $this->assertCount(2, $data->feeds);
+        $feed1 = reset($data->feeds);
+        $this->assertEquals('BBC News - World', $feed1->title);
+        $this->assertEquals('World News', $feed1->preferredtitle);
+        $this->assertEquals('Description: BBC News - World', $feed1->description);
+        $this->assertEquals(get_string('no'), $feed1->shared);
+        $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $feed1->url);
     }
 
     /**
index 0200032..c8b43b7 100644 (file)
@@ -58,5 +58,8 @@ function xmldb_block_section_links_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 7e5e56a..285658f 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 2a9dceb..842c428 100644 (file)
@@ -64,5 +64,8 @@ function xmldb_block_settings_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index d859862..4fc58de 100644 (file)
@@ -187,10 +187,6 @@ class provider implements
      * @param   context $context Transform the specific context to delete data for.
      */
     public static function delete_data_for_all_users_in_context(\context $context) {
-        if (empty($context)) {
-            return;
-        }
-
         // Delete all Calendar Events in the specified context in batches.
         if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context))) {
             self::delete_batch_records('event', 'id', $eventids);
index 7b8bb62..021cef9 100644 (file)
@@ -143,10 +143,6 @@ class provider implements
      * @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;
         }
index 3c6ddd6..5b526cc 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.5.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.35.1",
+        "moodlehq/behat-extension": "3.36.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index c79a846..e8aaa2f 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "f047772340a956bbf2c02d91536b11c6",
+    "content-hash": "956ce0b653b805efb6a9a483f8c9a847",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-browserkit-driver",
-            "version": "v1.3.2",
+            "version": "1.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkBrowserKitDriver.git",
-                "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb"
+                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb",
-                "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb",
+                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
+                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
                 "shasum": ""
             },
             "require": {
                 "behat/mink": "^1.7.1@dev",
                 "php": ">=5.3.6",
-                "symfony/browser-kit": "~2.3|~3.0",
-                "symfony/dom-crawler": "~2.3|~3.0"
+                "symfony/browser-kit": "~2.3|~3.0|~4.0",
+                "symfony/dom-crawler": "~2.3|~3.0|~4.0"
             },
             "require-dev": {
-                "silex/silex": "~1.2",
-                "symfony/phpunit-bridge": "~2.7|~3.0"
+                "mink/driver-testsuite": "dev-master",
+                "symfony/http-kernel": "~2.3|~3.0|~4.0"
             },
             "type": "mink-driver",
             "extra": {
                 "browser",
                 "testing"
             ],
-            "time": "2016-03-05T08:59:47+00:00"
+            "time": "2018-05-02T09:25:31+00:00"
         },
         {
             "name": "behat/mink-extension",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.3.0",
+            "version": "6.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "ext-curl": "*",
-                "phpunit/phpunit": "^4.0 || ^5.0",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
                 "psr/log": "^1.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.2-dev"
+                    "dev-master": "6.3-dev"
                 }
             },
             "autoload": {
                 "rest",
                 "web service"
             ],
-            "time": "2017-06-22T18:50:49+00:00"
+            "time": "2018-04-22T15:46:56+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.35.1",
+            "version": "v3.36.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e"
+                "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/e6e92fd551185f73603bad5694e854f3f6906e0e",
-                "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/ba8c4b8b323e05f7af128604f3f3dc60c953135a",
+                "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a",
                 "shasum": ""
             },
             "require": {
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "GPLv3"
+                "GPL-3.0-or-later"
             ],
             "authors": [
                 {
                 "Behat",
                 "moodle"
             ],
-            "time": "2018-01-24T14:09:40+00:00"
+            "time": "2018-02-04T18:04:02+00:00"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.7.5",
+            "version": "1.7.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
+                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
-                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
+                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
                 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
-                "sebastian/comparator": "^1.1|^2.0",
+                "sebastian/comparator": "^1.1|^2.0|^3.0",
                 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
             },
             "require-dev": {
                 "spy",
                 "stub"
             ],
-            "time": "2018-02-19T10:16:54+00:00"
+            "time": "2018-04-18T13:57:24+00:00"
         },
         {
             "name": "phpunit/dbunit",
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.3.0",
+            "version": "5.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1"
+                "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1",
-                "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
+                "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-12-06T09:29:45+00:00"
+            "time": "2018-04-06T15:36:58+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.7",
+            "version": "6.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9"
+                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9",
-                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
+                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-02-26T07:01:09+00:00"
+            "time": "2018-04-10T11:38:34+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.6",
+            "version": "5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
+                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
-                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
+                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
                 "shasum": ""
             },
             "require": {
                 "mock",
                 "xunit"
             ],
-            "time": "2018-01-06T05:45:45+00:00"
+            "time": "2018-05-29T13:50:43+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "490f27762705c8489bd042fe3e9377a191dba9b4"
+                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/490f27762705c8489bd042fe3e9377a191dba9b4",
-                "reference": "490f27762705c8489bd042fe3e9377a191dba9b4",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
+                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-03-19T22:32:39+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "05e10567b529476a006b00746c5f538f1636810e"
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e",
-                "reference": "05e10567b529476a006b00746c5f538f1636810e",
+                "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c",
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9|>=7.0.8",
-                "symfony/filesystem": "~2.8|~3.0|~4.0"
+                "symfony/filesystem": "~2.8|~3.0|~4.0",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "conflict": {
                 "symfony/dependency-injection": "<3.3",
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-14T10:03:57+00:00"
+            "time": "2018-05-14T16:49:53+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a"
+                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/544655f1fc078a9cd839fdda2b7b1e64627c826a",
-                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/d2ce52290b648ae33b5301d09bc14ee378612914",
+                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-03T14:55:07+00:00"
+            "time": "2018-05-16T12:49:49+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc"
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
-                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68",
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-28T21:49:22+00:00"
+            "time": "2018-05-16T14:03:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d"
+                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
-                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8",
+                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
             "require-dev": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-22T10:48:49+00:00"
+            "time": "2018-05-01T22:53:27+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "58990682ac3fdc1f563b7e705452921372aad11d"
+                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/58990682ac3fdc1f563b7e705452921372aad11d",
-                "reference": "58990682ac3fdc1f563b7e705452921372aad11d",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8",
+                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-14T10:03:57+00:00"
+            "time": "2018-04-06T07:35:25+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-22T10:48:49+00:00"
+            "time": "2018-05-16T08:49:21+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.8-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                },
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "time": "2018-04-30T19:57:29+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.7.0",
+            "version": "v1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
+                "reference": "3296adf6a6454a050679cde90f95350ad604b171"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
-                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
+                "reference": "3296adf6a6454a050679cde90f95350ad604b171",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.7-dev"
+                    "dev-master": "1.8-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2018-01-30T19:27:44+00:00"
+            "time": "2018-04-26T10:06:28+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.36",
+            "version": "v2.8.41",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9"
+                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/756f614c5061729ea245ac6717231f7e3bfb74f9",
-                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9",
+                "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
+                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-12T17:44:58+00:00"
+            "time": "2018-05-15T21:17:45+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
index 56e47e9..f66cd8c 100644 (file)
@@ -484,9 +484,11 @@ class core_course_management_renderer extends plugin_renderer_base {
      * @param course_in_list $course The currently selected course.
      * @param int $page The page being displayed.
      * @param int $perpage The number of courses to display per page.
+     * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
      * @return string
      */
-    public function course_listing(coursecat $category = null, course_in_list $course = null, $page = 0, $perpage = 20) {
+    public function course_listing(coursecat $category = null, course_in_list $course = null, $page = 0, $perpage = 20,
+        $viewmode = 'default') {
 
         if ($category === null) {
             $html = html_writer::start_div('select-a-category');
@@ -527,13 +529,13 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::tag('h3', $category->get_formatted_name(),
             array('id' => 'course-listing-title', 'tabindex' => '0'));
         $html .= $this->course_listing_actions($category, $course, $perpage);
-        $html .= $this->listing_pagination($category, $page, $perpage);
+        $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
         $html .= html_writer::start_tag('ul', array('class' => 'ml-1 course-list', 'role' => 'group'));
         foreach ($category->get_courses($options) as $listitem) {
             $html .= $this->course_listitem($category, $listitem, $courseid);
         }
         $html .= html_writer::end_tag('ul');
-        $html .= $this->listing_pagination($category, $page, $perpage, true);
+        $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
         $html .= $this->course_bulk_actions($category);
         $html .= html_writer::end_div();
         return $html;
@@ -546,9 +548,10 @@ class core_course_management_renderer extends plugin_renderer_base {
      * @param int $page The current page.
      * @param int $perpage The number of courses to display per page.
      * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
+     * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
      * @return string
      */
-    protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false) {
+    protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false, $viewmode = 'default') {
         $html = '';
         $totalcourses = $category->get_courses_count();
         $totalpages = ceil($totalcourses / $perpage);
@@ -582,7 +585,12 @@ class core_course_management_renderer extends plugin_renderer_base {
             }
         }
         $items = array();
-        $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+        if ($viewmode !== 'default') {
+            $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
+                'view' => $viewmode));
+        } else {
+            $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+        }
         if ($page > 0) {
             $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
             $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
index b630ad8..357c0a3 100644 (file)
@@ -231,7 +231,12 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o .= $this->section_availability($section);
 
         $o .= html_writer::start_tag('div', array('class' => 'summary'));
-        $o .= $this->format_summary_text($section);
+        if ($section->uservisible || $section->visible) {
+            // Show summary if section is available or has availability restriction information.
+            // Do not show summary if section is hidden but we still display it because of course setting
+            // "Hidden sections are shown in collapsed form".
+            $o .= $this->format_summary_text($section);
+        }
         $o .= html_writer::end_tag('div');
 
         return $o;
@@ -409,7 +414,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     }
 
     /**
-     * Generate a summary of a section for display on the 'coruse index page'
+     * Generate a summary of a section for display on the 'course index page'
      *
      * @param stdClass $section The course_section entry from DB
      * @param stdClass $course The course entry from DB
@@ -443,13 +448,18 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         }
         $o .= $this->output->heading($title, 3, 'section-title');
 
+        $o .= $this->section_availability($section);
         $o.= html_writer::start_tag('div', array('class' => 'summarytext'));
-        $o.= $this->format_summary_text($section);
+
+        if ($section->uservisible || $section->visible) {
+            // Show summary if section is available or has availability restriction information.
+            // Do not show summary if section is hidden but we still display it because of course setting
+            // "Hidden sections are shown in collapsed form".
+            $o .= $this->format_summary_text($section);
+        }
         $o.= html_writer::end_tag('div');
         $o.= $this->section_activity_summary($section, $course, null);
 
-        $o .= $this->section_availability($section);
-
         $o .= html_writer::end_tag('div');
         $o .= html_writer::end_tag('li');
 
@@ -552,6 +562,10 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         if (!$section->visible) {
             if ($canviewhidden) {
                 $o .= $this->courserenderer->availability_info(get_string('hiddenfromstudents'), 'ishidden');
+            } else {
+                // We are here because of the setting "Hidden sections are shown in collapsed form".
+                // Student can not see the section contents but can see its name.
+                $o .= $this->courserenderer->availability_info(get_string('notavailable'), 'ishidden');
             }
         } else if (!$section->uservisible) {
             if ($section->availableinfo) {
@@ -590,7 +604,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * Show if something is on on the course clipboard (moving around)
      *
      * @param stdClass $course The course entry from DB
-     * @param int $sectionno The section number in the coruse which is being dsiplayed
+     * @param int $sectionno The section number in the course which is being displayed
      * @return string HTML to output.
      */
     protected function course_activity_clipboard($course, $sectionno = null) {
@@ -620,7 +634,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      *
      * @param stdClass $course The course entry from DB
      * @param array $sections The course_sections entries from the DB
-     * @param int $sectionno The section number in the coruse which is being dsiplayed
+     * @param int $sectionno The section number in the course which is being displayed
      * @return array associative array with previous and next section link
      */
     protected function get_nav_links($course, $sections, $sectionno) {
@@ -665,7 +679,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     /**
      * Generate the header html of a stealth section
      *
-     * @param int $sectionno The section number in the coruse which is being dsiplayed
+     * @param int $sectionno The section number in the course which is being displayed
      * @return string HTML to output.
      */
     protected function stealth_section_header($sectionno) {
@@ -695,7 +709,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     /**
      * Generate the html for a hidden section
      *
-     * @param int $sectionno The section number in the coruse which is being dsiplayed
+     * @param int $sectionno The section number in the course which is being displayed
      * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
      * @return string HTML to output.
      */
@@ -769,20 +783,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $course = course_get_format($course)->get_course();
 
         // Can we view the section in question?
-        if (!($sectioninfo = $modinfo->get_section_info($displaysection))) {
-            // This section doesn't exist
-            print_error('unknowncoursesection', 'error', null, $course->fullname);
-            return;
-        }
-
-        if (!$sectioninfo->uservisible) {
-            if (!$course->hiddensections) {
-                echo $this->start_section_list();
-                echo $this->section_hidden($displaysection, $course->id);
-                echo $this->end_section_list();
-            }
-            // Can't view this section.
-            return;
+        if (!($sectioninfo = $modinfo->get_section_info($displaysection)) || !$sectioninfo->uservisible) {
+            // This section doesn't exist or is not available for the user.
+            // We actually already check this in course/view.php but just in case exit from this function as well.
+            print_error('unknowncoursesection', 'error', course_get_url($course),
+                format_string($course->fullname));
         }
 
         // Copy activity clipboard..
@@ -891,18 +896,12 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 continue;
             }
             // Show the section if the user is permitted to access it, OR if it's not available
-            // but there is some available info text which explains the reason & should display.
+            // but there is some available info text which explains the reason & should display,
+            // OR it is hidden but the course has a setting to display hidden sections as unavilable.
             $showsection = $thissection->uservisible ||
-                    ($thissection->visible && !$thissection->available &&
-                    !empty($thissection->availableinfo));
+                    ($thissection->visible && !$thissection->available && !empty($thissection->availableinfo)) ||
+                    (!$thissection->visible && !$course->hiddensections);
             if (!$showsection) {
-                // If the hiddensections option is set to 'show hidden sections in collapsed
-                // form', then display the hidden section message - UNLESS the section is
-                // hidden by the availability system, which is set to hide the reason.
-                if (!$course->hiddensections && $thissection->available) {
-                    echo $this->section_hidden($section, $course->id);
-                }
-
                 continue;
             }
 
index 3f35b53..e4e4ac1 100644 (file)
@@ -59,5 +59,8 @@ function xmldb_format_topics_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018030900, 'format', 'topics');
     }
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index a68f713..893d716 100644 (file)
@@ -95,5 +95,8 @@ function xmldb_format_weeks_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018030900, 'format', 'weeks');
     }
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 8c4d6ff..b083e28 100644 (file)
@@ -495,7 +495,7 @@ if ($displaycategorylisting) {
 if ($displaycourselisting) {
     echo $renderer->grid_column_start($coursesize, 'course-listing');
     if (!$issearching) {
-        echo $renderer->course_listing($category, $course, $page, $perpage);
+        echo $renderer->course_listing($category, $course, $page, $perpage, $viewmode);
     } else {
         list($courses, $coursescount, $coursestotal) =
             \core_course\management\helper::search_courses($search, $blocklist, $modulelist, $page, $perpage);
index 68767ff..b3cd970 100644 (file)
@@ -2311,6 +2311,208 @@ class core_course_renderer extends plugin_renderer_base {
 
         return $hubdescription;
     }
+
+    /**
+     * Output frontpage summary text and frontpage modules (stored as section 1 in site course)
+     *
+     * This may be disabled in settings
+     *
+     * @return string
+     */
+    public function frontpage_section1() {
+        global $SITE, $USER;
+
+        $output = '';
+        $editing = $this->page->user_is_editing();
+
+        if ($editing) {
+            // Make sure section with number 1 exists.
+            course_create_sections_if_missing($SITE, 1);
+        }
+
+        $modinfo = get_fast_modinfo($SITE);
+        $section = $modinfo->get_section_info(1);
+        if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
+            $output .= $this->box_start('generalbox sitetopic');
+
+            // If currently moving a file then show the current clipboard.
+            if (ismoving($SITE->id)) {
+                $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
+                $output .= '<p><font size="2">';
+                $cancelcopyurl = new moodle_url('/course/mod.php', ['cancelcopy' => 'true', 'sesskey' => sesskey()]);
+                $output .= "$stractivityclipboard&nbsp;&nbsp;(" . html_writer::link($cancelcopyurl, get_string('cancel')) .')';
+                $output .= '</font></p>';
+            }
+
+            $context = context_course::instance(SITEID);
+
+            // If the section name is set we show it.
+            if (trim($section->name) !== '') {
+                $output .= $this->heading(
+                    format_string($section->name, true, array('context' => $context)),
+                    2,
+                    'sectionname'
+                );
+            }
+
+            $summarytext = file_rewrite_pluginfile_urls($section->summary,
+                'pluginfile.php',
+                $context->id,
+                'course',
+                'section',
+                $section->id);
+            $summaryformatoptions = new stdClass();
+            $summaryformatoptions->noclean = true;
+            $summaryformatoptions->overflowdiv = true;
+
+            $output .= format_text($summarytext, $section->summaryformat, $summaryformatoptions);
+
+            if ($editing && has_capability('moodle/course:update', $context)) {
+                $streditsummary = get_string('editsummary');
+                $editsectionurl = new moodle_url('/course/editsection.php', ['id' => $section->id]);
+                $output .= html_writer::link($editsectionurl, $this->pix_icon('t/edit', $streditsummary)) .
+                    "<br /><br />";
+            }
+
+            $output .= $this->course_section_cm_list($SITE, $section);
+
+            $output .= $this->course_section_add_cm_control($SITE, $section->section);
+            $output .= $this->box_end();
+        }
+
+        return $output;
+    }
+
+    /**
+     * Output news for the frontpage (extract from site-wide news forum)
+     *
+     * @param stdClass $newsforum record from db table 'forum' that represents the site news forum
+     * @return string
+     */
+    protected function frontpage_news($newsforum) {
+        global $CFG, $SITE, $SESSION, $USER;
+        require_once($CFG->dirroot .'/mod/forum/lib.php');
+
+        $output = '';
+
+        if (isloggedin()) {
+            $SESSION->fromdiscussion = $CFG->wwwroot;
+            $subtext = '';
+            if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
+                if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
+                    $subtext = get_string('unsubscribe', 'forum');
+                }
+            } else {
+                $subtext = get_string('subscribe', 'forum');
+            }
+            $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
+            $output .= html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
+        }
+
+        ob_start();
+        forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
+        $output .= ob_get_contents();
+        ob_end_clean();
+
+        return $output;
+    }
+
+    /**
+     * Renders part of frontpage with a skip link (i.e. "My courses", "Site news", etc.)
+     *
+     * @param string $skipdivid
+     * @param string $contentsdivid
+     * @param string $header Header of the part
+     * @param string $contents Contents of the part
+     * @return string
+     */
+    protected function frontpage_part($skipdivid, $contentsdivid, $header, $contents) {
+        $output = html_writer::link('#' . $skipdivid,
+            get_string('skipa', 'access', core_text::strtolower(strip_tags($header))),
+            array('class' => 'skip-block skip'));
+
+        // Wrap frontpage part in div container.
+        $output .= html_writer::start_tag('div', array('id' => $contentsdivid));
+        $output .= $this->heading($header);
+
+        $output .= $contents;
+
+        // End frontpage part div container.
+        $output .= html_writer::end_tag('div');
+
+        $output .= html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => $skipdivid));
+        return $output;
+    }
+
+    /**
+     * Outputs contents for frontpage as configured in $CFG->frontpage or $CFG->frontpageloggedin
+     *
+     * @return string
+     */
+    public function frontpage() {
+        global $CFG, $SITE;
+
+        $output = '';
+
+        if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
+            $frontpagelayout = $CFG->frontpageloggedin;
+        } else {
+            $frontpagelayout = $CFG->frontpage;
+        }
+
+        foreach (explode(',', $frontpagelayout) as $v) {
+            switch ($v) {
+                // Display the main part of the front page.
+                case FRONTPAGENEWS:
+                    if ($SITE->newsitems) {
+                        // Print forums only when needed.
+                        require_once($CFG->dirroot .'/mod/forum/lib.php');
+                        if (($newsforum = forum_get_course_forum($SITE->id, 'news')) &&
+                                ($forumcontents = $this->frontpage_news($newsforum))) {
+                            $newsforumcm = get_fast_modinfo($SITE)->instances['forum'][$newsforum->id];
+                            $output .= $this->frontpage_part('skipsitenews', 'site-news-forum',
+                                $newsforumcm->get_formatted_name(), $forumcontents);
+                        }
+                    }
+                    break;
+
+                case FRONTPAGEENROLLEDCOURSELIST:
+                    $mycourseshtml = $this->frontpage_my_courses();
+                    if (!empty($mycourseshtml)) {
+                        $output .= $this->frontpage_part('skipmycourses', 'frontpage-course-list',
+                            get_string('mycourses'), $mycourseshtml);
+                        break;
+                    }
+                    // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
+
+                case FRONTPAGEALLCOURSELIST:
+                    $availablecourseshtml = $this->frontpage_available_courses();
+                    if (!empty($availablecourseshtml)) {
+                        $output .= $this->frontpage_part('skipavailablecourses', 'frontpage-available-course-list',
+                            get_string('availablecourses'), $availablecourseshtml);
+                    }
+                    break;
+
+                case FRONTPAGECATEGORYNAMES:
+                    $output .= $this->frontpage_part('skipcategories', 'frontpage-category-names',
+                        get_string('categories'), $this->frontpage_categories_list());
+                    break;
+
+                case FRONTPAGECATEGORYCOMBO:
+                    $output .= $this->frontpage_part('skipcourses', 'frontpage-category-combo',
+                        get_string('courses'), $this->frontpage_combo_list());
+                    break;
+
+                case FRONTPAGECOURSESEARCH:
+                    $output .= $this->box($this->course_search_form('', 'short'), 'mdl-align');
+                    break;
+
+            }
+            $output .= '<br />';
+        }
+
+        return $output;
+    }
 }
 
 /**
index 546b020..249948d 100644 (file)
@@ -863,7 +863,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Test move the marked section down..
         move_section_to($course, 2, 4);
 
-        // Verify that the coruse marker has been moved along with the section..
+        // Verify that the course marker has been moved along with the section..
         $course = $DB->get_record('course', array('id' => $course->id));
         $this->assertEquals(4, $course->marker);
 
index 46a0c63..e4f33ed 100644 (file)
@@ -164,9 +164,6 @@ class provider implements
     public static function delete_data_for_all_users_in_context(\context $context) {
         global $DB;
 
-        if (empty($context)) {
-            return;
-        }
         // Sanity check that context is at the User context level.
         if ($context->contextlevel == CONTEXT_COURSE) {
             $sql = "SELECT ue.id
@@ -230,4 +227,16 @@ class provider implements
         $DB->delete_records_select('user_enrolments', "id $sql", $params);
     }
 
+    /**
+     * Get the subcontext for export.
+     *
+     * @param array $subcontext Any additional subcontext to use.
+     * @return array The array containing the full subcontext, i.e. [enrolments, subcontext]
+     */
+    public static function get_subcontext(array $subcontext) {
+        return array_merge(
+            [get_string('privacy:metadata:user_enrolments', 'core_enrol')],
+            $subcontext
+        );
+    }
 }
index 351cf66..38122c2 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_database_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5e403cd..93e5d83 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 namespace enrol_flatfile\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\writer;
+use core_privacy\local\request\transform;
+
 defined('MOODLE_INTERNAL') || die();
 /**
  * Privacy Subsystem for enrol_flatfile implementing null_provider.
@@ -28,14 +35,148 @@ defined('MOODLE_INTERNAL') || die();
  * @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\null_provider {
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\plugin\provider {
+
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Returns meta data about this system.
      *
-     * @return  string
+     * @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_reason() : string {
-        return 'privacy:metadata';
+    public static function get_metadata(collection $collection) : collection {
+        return $collection->add_database_table('enrol_flatfile', [
+            'action' => 'privacy:metadata:enrol_flatfile:action',
+            'roleid' => 'privacy:metadata:enrol_flatfile:roleid',
+            'userid' => 'privacy:metadata:enrol_flatfile:userid',
+            'courseid' => 'privacy:metadata:enrol_flatfile:courseid',
+            'timestart' => 'privacy:metadata:enrol_flatfile:timestart',
+            'timeend' => 'privacy:metadata:enrol_flatfile:timeend',
+            'timemodified' => 'privacy:metadata:enrol_flatfile:timemodified'
+        ], 'privacy:metadata:enrol_flatfile');
     }
-}
\ No newline at end of file
+
+    /**
+     * 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 c.id
+                  FROM {enrol_flatfile} ef
+                  JOIN {context} c ON c.contextlevel = ? AND c.instanceid = ef.courseid
+                 WHERE ef.userid = ?";
+        $params = [CONTEXT_COURSE, $userid];
+
+        $contextlist = new contextlist();
+        $contextlist->set_component('enrol_flatfile');
+        return $contextlist->add_from_sql($sql, $params);
+    }
+
+    /**
+     * 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;
+
+        // Ensure all contexts are CONTEXT_COURSE.
+        $contexts = static::validate_contextlist_contexts($contextlist);
+        if (empty($contexts)) {
+            return;
+        }
+
+        // Get the context instance ids from the contexts. These  are the course ids..
+        $contextinstanceids = array_map(function($context) {
+            return $context->instanceid;
+        }, $contexts);
+        $userid = $contextlist->get_user()->id;
+
+        // Now, we just need to fetch and format all entries corresponding to the contextids provided.
+        $sql = "SELECT ef.action, r.shortname, ef.courseid, ef.timestart, ef.timeend, ef.timemodified
+                  FROM {enrol_flatfile} ef
+                  JOIN {context} c ON c.contextlevel = :contextlevel AND c.instanceid = ef.courseid
+                  JOIN {role} r ON r.id = ef.roleid
+                 WHERE ef.userid = :userid";
+        $params = ['contextlevel' => CONTEXT_COURSE, 'userid' => $userid];
+        list($insql, $inparams) = $DB->get_in_or_equal($contextinstanceids, SQL_PARAMS_NAMED);
+        $sql .= " AND ef.courseid $insql";
+        $params = array_merge($params, $inparams);
+
+        $futureenrolments = $DB->get_recordset_sql($sql, $params);
+        $enrolmentdata = [];
+        foreach ($futureenrolments as $futureenrolment) {
+            // It's possible to have more than one future enrolment per course.
+            $futureenrolment->timestart = transform::datetime($futureenrolment->timestart);
+            $futureenrolment->timeend = transform::datetime($futureenrolment->timeend);
+            $futureenrolment->timemodified = transform::datetime($futureenrolment->timemodified);
+            $enrolmentdata[$futureenrolment->courseid][] = $futureenrolment;
+        }
+        $futureenrolments->close();
+
+        // And finally, write out the data to the relevant course contexts.
+        $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+        foreach ($enrolmentdata as $courseid => $enrolments) {
+            $data = (object) [
+                'pendingenrolments' => $enrolments,
+            ];
+            writer::with_context(\context_course::instance($courseid))->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) {
+        if ($context->contextlevel != CONTEXT_COURSE) {
+            return;
+        }
+        global $DB;
+        $DB->delete_records('enrol_flatfile', ['courseid' => $context->instanceid]);
+    }
+
+    /**
+     * 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) {
+        // Only delete data from contexts which are at the COURSE_MODULE contextlevel.
+        $contexts = self::validate_contextlist_contexts($contextlist);
+        if (empty($contexts)) {
+            return;
+        }
+
+        // Get the course ids based on the provided contexts.
+        $contextinstanceids = array_map(function($context) {
+            return $context->instanceid;
+        }, $contextlist->get_contexts());
+
+        global $DB;
+        $user = $contextlist->get_user();
+        list($insql, $inparams) = $DB->get_in_or_equal($contextinstanceids, SQL_PARAMS_NAMED);
+        $params = array_merge(['userid' => $user->id], $inparams);
+        $sql = "userid = :userid AND courseid $insql";
+        $DB->delete_records_select('enrol_flatfile', $sql, $params);
+    }
+
+    /**
+     * Simple sanity check on the contextlist contexts, making sure they're of CONTEXT_COURSE contextlevel.
+     *
+     * @param approved_contextlist $contextlist
+     * @return array the array of contexts filtered to only include those of CONTEXT_COURSE contextlevel.
+     */
+    protected static function validate_contextlist_contexts(approved_contextlist $contextlist) {
+        return array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_COURSE) {
+                $carry[] = $context;
+            }
+            return $carry;
+        }, []);
+    }
+}
index 59ae42e..91590e5 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_flatfile_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 6f444e3..e815bf9 100644 (file)
@@ -29,6 +29,7 @@ $string['filelockedmail'] = 'The text file you are using for file-based enrolmen
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
 $string['flatfile:manage'] = 'Manage user enrolments manually';
 $string['flatfile:unenrol'] = 'Unenrol users from the course manually';
+$string['flatfileenrolments'] = 'Flat file (CSV) enrolments';
 $string['flatfilesync'] = 'Flat file enrolment sync';
 $string['location'] = 'File location';
 $string['location_desc'] = 'Specify full path to the enrolment file. The file is automatically deleted after processing.';
@@ -61,4 +62,11 @@ It could look something like this:
    del, student, 17, CF101
    add, student, 21, CF101, 1091115000, 1091215000
 </pre>';
-$string['privacy:metadata'] = 'The Flat file (CSV) enrolment plugin does not store any personal data.';
+$string['privacy:metadata:enrol_flatfile'] = 'The Flat file (CSV) enrolment plugin may store personal data relating to future enrolments in the enrol_flatfile table.';
+$string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected at the given date.';
+$string['privacy:metadata:enrol_flatfile:courseid'] = 'The courseid to which the enrolment relates.';
+$string['privacy:metadata:enrol_flatfile:roleid'] = 'The id of the role to be assigned or revoked.';
+$string['privacy:metadata:enrol_flatfile:timestart'] = 'The time at which the enrolment change starts.';
+$string['privacy:metadata:enrol_flatfile:timeend'] = 'The time at which the enrolment change ends.';
+$string['privacy:metadata:enrol_flatfile:timemodified'] = 'The modification time of this enrolment change.';
+$string['privacy:metadata:enrol_flatfile:userid'] = 'The id of the user to which the role assignment relates.';
diff --git a/enrol/flatfile/tests/privacy_provider_test.php b/enrol/flatfile/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..5ad5dbb
--- /dev/null
@@ -0,0 +1,243 @@
+<?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 tests for enrol_flatfile.
+ *
+ * @package    enrol_flatfile
+ * @category   test
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use core_privacy\tests\provider_testcase;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use enrol_flatfile\privacy\provider;
+
+/**
+ * Privacy tests for enrol_flatfile.
+ *
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_flatfile_privacy_testcase extends provider_testcase {
+
+    /** @var \stdClass $user1 a test user.*/
+    protected $user1;
+
+    /** @var \stdClass $user2 a test user.*/
+    protected $user2;
+
+    /** @var \context $coursecontext1 a course context.*/
+    protected $coursecontext1;
+
+    /** @var \context $coursecontext2 a course context.*/
+    protected $coursecontext2;
+
+    /** @var \context $coursecontext3 a course context.*/
+    protected $coursecontext3;
+
+    /**
+     * Called before every test.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Verify that get_metadata returns the database table mapping.
+     */
+    public function test_get_metadata() {
+        $collection = new collection('enrol_flatfile');
+        $collection = provider::get_metadata($collection);
+        $collectiondata = $collection->get_collection();
+        $this->assertNotEmpty($collectiondata);
+        $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $collectiondata[0]);
+    }
+
+    /**
+     * Verify that the relevant course contexts are returned for users with pending enrolment records.
+     */
+    public function test_get_contexts_for_user() {
+        global $DB;
+        // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+        $this->create_future_enrolments();
+
+        $this->assertEquals(3, $DB->count_records('enrol_flatfile'));
+
+        // We expect to see 2 entries for user1, in course1 and course3.
+        $contextlist = provider::get_contexts_for_userid($this->user1->id);
+        $this->assertEquals(2, $contextlist->count());
+        $contextids = $contextlist->get_contextids();
+        $this->assertContains($this->coursecontext1->id, $contextids);
+        $this->assertContains($this->coursecontext3->id, $contextids);
+
+        // And 1 for user2 on course2.
+        $contextlist = provider::get_contexts_for_userid($this->user2->id);
+        $this->assertEquals(1, $contextlist->count());
+        $contextids = $contextlist->get_contextids();
+        $this->assertContains($this->coursecontext2->id, $contextids);
+    }
+
+    /**
+     * Verify the export includes any future enrolment records for the user.
+     */
+    public function test_export_user_data() {
+        // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+        $this->create_future_enrolments();
+
+        // Get contexts containing user data.
+        $contextlist = provider::get_contexts_for_userid($this->user1->id);
+        $this->assertEquals(2, $contextlist->count());
+
+        $approvedcontextlist = new approved_contextlist(
+            $this->user1,
+            'enrol_flatfile',
+            $contextlist->get_contextids()
+        );
+
+        // Export for the approved contexts.
+        provider::export_user_data($approvedcontextlist);
+
+        // Verify we see one future course enrolment in course1, and one in course3.
+        $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+
+        $writer = writer::with_context($this->coursecontext1);
+        $this->assertNotEmpty($writer->get_data($subcontext));
+
+        $writer = writer::with_context($this->coursecontext3);
+        $this->assertNotEmpty($writer->get_data($subcontext));
+
+        // Verify we have nothing in course 2 for this user.
+        $writer = writer::with_context($this->coursecontext2);
+        $this->assertEmpty($writer->get_data($subcontext));
+    }
+
+    /**
+     * Verify export will limit any future enrolment records to only those contextids provided.
+     */
+    public function test_export_user_data_restricted_context_subset() {
+        // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+        $this->create_future_enrolments();
+
+        // Now, limit the export scope to just course1's context and verify only that data is seen in any export.
+        $subsetapprovedcontextlist = new approved_contextlist(
+            $this->user1,
+            'enrol_flatfile',
+            [$this->coursecontext1->id]
+        );
+
+        // Export for the approved contexts.
+        provider::export_user_data($subsetapprovedcontextlist);
+
+        // Verify we see one future course enrolment in course1 only.
+        $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+
+        $writer = writer::with_context($this->coursecontext1);
+        $this->assertNotEmpty($writer->get_data($subcontext));
+
+        // And nothing in the course3 context.
+        $writer = writer::with_context($this->coursecontext3);
+        $this->assertEmpty($writer->get_data($subcontext));
+    }
+
+    /**
+     * Verify that records can be deleted by context.
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+        // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+        $this->create_future_enrolments();
+
+        // Verify we have 1 future enrolments for course 1.
+        $this->assertEquals(1, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
+
+        // Now, run delete by context and confirm that record is removed.
+        provider::delete_data_for_all_users_in_context($this->coursecontext1);
+        $this->assertEquals(0, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+        // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+        $this->create_future_enrolments();
+
+        // Verify we have 2 future enrolments for course 1 and course 3.
+        $contextlist = provider::get_contexts_for_userid($this->user1->id);
+        $this->assertEquals(2, $contextlist->count());
+        $contextids = $contextlist->get_contextids();
+        $this->assertContains($this->coursecontext1->id, $contextids);
+        $this->assertContains($this->coursecontext3->id, $contextids);
+
+        $approvedcontextlist = new approved_contextlist(
+            $this->user1,
+            'enrol_flatfile',
+            $contextids
+        );
+
+        // Now, run delete for user and confirm that both records are removed.
+        provider::delete_data_for_user($approvedcontextlist);
+        $contextlist = provider::get_contexts_for_userid($this->user1->id);
+        $this->assertEquals(0, $contextlist->count());
+        $this->assertEquals(0, $DB->count_records('enrol_flatfile', ['userid' => $this->user1->id]));
+    }
+
+    /**
+     * Helper to sync a file and create the enrol_flatfile DB entries, for use with the get, export and delete tests.
+     */
+    protected function create_future_enrolments() {
+        global $CFG;
+        $this->user1 = $this->getDataGenerator()->create_user(['idnumber' => 'u1']);
+        $this->user2 = $this->getDataGenerator()->create_user(['idnumber' => 'u2']);
+
+        $course1 = $this->getDataGenerator()->create_course(['idnumber' => 'c1']);
+        $course2 = $this->getDataGenerator()->create_course(['idnumber' => 'c2']);
+        $course3 = $this->getDataGenerator()->create_course(['idnumber' => 'c3']);
+        $this->coursecontext1 = context_course::instance($course1->id);
+        $this->coursecontext2 = context_course::instance($course2->id);
+        $this->coursecontext3 = context_course::instance($course3->id);
+
+        $now = time();
+        $future = $now + 60 * 60 * 5;
+        $farfuture = $now + 60 * 60 * 24 * 5;
+
+        $file = "$CFG->dataroot/enrol.txt";
+        $data = "add,student,u1,c1,$future,0
+                 add,student,u2,c2,$future,0
+                 add,student,u1,c3,$future,$farfuture";
+        file_put_contents($file, $data);
+
+        $trace = new null_progress_trace();
+        $this->enable_plugin();
+        $flatfileplugin = enrol_get_plugin('flatfile');
+        $flatfileplugin->set_config('location', $file);
+        $flatfileplugin->sync($trace);
+    }
+
+    /**
+     * Enables the flatfile plugin for testing.
+     */
+    protected function enable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        $enabled['flatfile'] = true;
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+}
index cb6fb76..8785b4f 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_guest_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index a4e1df7..bc6dd13 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_enrol_imsenterprise_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 353a7ad..aae9a1d 100644 (file)
@@ -264,5 +264,8 @@ function xmldb_enrol_lti_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index cec4ea4..4760dde 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_manual_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 6f22cca..d313c11 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_mnet_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index bdbbbf6..ad195b1 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 4965298..d5e81ad 100644 (file)
@@ -51,5 +51,8 @@ function xmldb_enrol_self_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5294046..3080092 100644 (file)
@@ -183,5 +183,8 @@ MathJax.Hub.Config({
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index c89fab9..705aeaf 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_filter_mediaplugin_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 8b2c41d..9a17d5b 100644 (file)
@@ -13,7 +13,6 @@
     /* Make videos as wide as possible without being wider than their containers */
     width: 100vw;
     max-width: 100%;
-    height: auto;
 }
 
 .mediaplugin > div {
index 53cc010..7f85c8a 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_filter_tex_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 282253a..39ec59c 100644 (file)
@@ -60,6 +60,7 @@ class provider implements
      */
     public static function get_metadata(collection $collection) : collection {
 
+        // Tables without 'real' user information.
         $collection->add_database_table('grade_outcomes', [
             'timemodified' => 'privacy:metadata:outcomes:timemodified',
             'usermodified' => 'privacy:metadata:outcomes:usermodified',
@@ -80,6 +81,18 @@ class provider implements
             'loggeduser' => 'privacy:metadata:history:loggeduser',
         ], 'privacy:metadata:itemshistory');
 
+        $collection->add_database_table('scale', [
+            'userid' => 'privacy:metadata:scale:userid',
+            'timemodified' => 'privacy:metadata:scale:timemodified',
+        ], 'privacy:metadata:scale');
+
+        $collection->add_database_table('scale_history', [
+            'userid' => 'privacy:metadata:scale:userid',
+            'timemodified' => 'privacy:metadata:history:timemodified',
+            'loggeduser' => 'privacy:metadata:history:loggeduser',
+        ], 'privacy:metadata:scalehistory');
+
+        // Table with user information.
         $gradescommonfields = [
             'userid' => 'privacy:metadata:grades:userid',
             'usermodified' => 'privacy:metadata:grades:usermodified',
@@ -97,9 +110,24 @@ class provider implements
             'loggeduser' => 'privacy:metadata:history:loggeduser',
         ]), 'privacy:metadata:gradeshistory');
 
-        // The table grade_import_values is not reported because its data is temporary and only
+        // The following tables are reported but not exported/deleted because their data is temporary and only
         // used during an import. It's content is deleted after a successful, or failed, import.
 
+        $collection->add_database_table('grade_import_newitem', [
+            'itemname' => 'privacy:metadata:grade_import_newitem:itemname',
+            'importcode' => 'privacy:metadata:grade_import_newitem:importcode',
+            'importer' => 'privacy:metadata:grade_import_newitem:importer'
+        ], 'privacy:metadata:grade_import_newitem');
+
+        $collection->add_database_table('grade_import_values', [
+            'userid' => 'privacy:metadata:grade_import_values:userid',
+            'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade',
+            'feedback' => 'privacy:metadata:grade_import_values:feedback',
+            'importcode' => 'privacy:metadata:grade_import_values:importcode',
+            'importer' => 'privacy:metadata:grade_import_values:importer',
+            'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback'
+        ], 'privacy:metadata:grade_import_values');
+
         return $collection;
     }
 
@@ -118,18 +146,29 @@ class provider implements
               FROM {grade_outcomes} go
               JOIN {context} ctx
                 ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
-                OR (ctx.id = :syscontextid)
+                OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid)
              WHERE go.usermodified = :userid";
         $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
         $contextlist->add_from_sql($sql, $params);
 
-        // Add where appear in the history of outcomes, categories or items.
+        // Add where we modified scales.
+        $sql = "
+            SELECT DISTINCT ctx.id
+              FROM {scale} s
+              JOIN {context} ctx
+                ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel)
+                OR (s.courseid = 0 AND ctx.id = :syscontextid)
+             WHERE s.userid = :userid";
+        $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Add where appear in the history of outcomes, categories, scales or items.
         $sql = "
             SELECT DISTINCT ctx.id
               FROM {context} ctx
          LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND (
                    (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1)
-                OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid)
+                OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1)
             )
          LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND (
                    gch.courseid = ctx.instanceid
@@ -139,17 +178,28 @@ class provider implements
                    gih.courseid = ctx.instanceid
                AND ctx.contextlevel = :courselevel3
             )
+         LEFT JOIN {scale_history} sh
+                ON (sh.userid = :userid4 OR sh.loggeduser = :userid5)
+               AND (
+                       (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4)
+                    OR (sh.courseid = 0 AND ctx.id = :syscontextid2)
+            )
              WHERE goh.id IS NOT NULL
                 OR gch.id IS NOT NULL
-                OR gih.id IS NOT NULL";
+                OR gih.id IS NOT NULL
+                OR sh.id IS NOT NULL";
         $params = [
-            'syscontextid' => SYSCONTEXTID,
+            'syscontextid1' => SYSCONTEXTID,
+            'syscontextid2' => SYSCONTEXTID,
             'courselevel1' => CONTEXT_COURSE,
             'courselevel2' => CONTEXT_COURSE,
             'courselevel3' => CONTEXT_COURSE,
+            'courselevel4' => CONTEXT_COURSE,
             'userid1' => $userid,
             'userid2' => $userid,
             'userid3' => $userid,
+            'userid4' => $userid,
+            'userid5' => $userid,
         ];
         $contextlist->add_from_sql($sql, $params);
 
@@ -240,6 +290,9 @@ class provider implements
         // Export the outcomes.
         static::export_user_data_outcomes_in_contexts($contextlist);
 
+        // Export the scales.
+        static::export_user_data_scales_in_contexts($contextlist);
+
         // Export the historical grades which have become orphans (their grade items were deleted).
         // We place those in ther user context of the graded user.
         $userids = array_values(array_map(function($context) {
@@ -659,6 +712,100 @@ class provider implements
         });
     }
 
+    /**
+     * Export the user data related to scales.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     * @return void
+     */
+    protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) {
+        global $DB;
+
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+        $userid = $contextlist->get_user()->id;
+
+        // Reorganise the contexts.
+        $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_SYSTEM) {
+                $carry['in_system'] = true;
+            } else if ($context->contextlevel == CONTEXT_COURSE) {
+                $carry['courseids'][] = $context->instanceid;
+            }
+            return $carry;
+        }, [
+            'in_system' => false,
+            'courseids' => []
+        ]);
+
+        // Construct SQL.
+        $sqltemplateparts = [];
+        $templateparams = [];
+        if ($reduced['in_system']) {
+            $sqltemplateparts[] = '{prefix}.courseid = 0';
+        }
+        if (!empty($reduced['courseids'])) {
+            list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
+            $sqltemplateparts[] = "{prefix}.courseid $insql";
+            $templateparams = array_merge($templateparams, $inparams);
+        }
+        if (empty($sqltemplateparts)) {
+            return;
+        }
+        $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
+
+        // Export edited scales.
+        $sqlwhere = str_replace('{prefix}', 's', $sqltemplate);
+        $sql = "
+            SELECT s.id, s.courseid, s.name, s.timemodified
+              FROM {scale} s
+             WHERE $sqlwhere
+               AND s.userid = :userid
+          ORDER BY s.courseid, s.timemodified, s.id";
+        $params = array_merge($templateparams, ['userid' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+            $carry[] = [
+                'name' => $record->name,
+                'timemodified' => transform::datetime($record->timemodified),
+                'created_or_modified_by_you' => transform::yesno(true)
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+            writer::with_context($context)->export_related_data($relatedtomepath, 'scales',
+                (object) ['scales' => $data]);
+        });
+
+        // Export edits of scales history.
+        $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate);
+        $sql = "
+            SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser
+              FROM {scale_history} sh
+             WHERE $sqlwhere
+               AND sh.loggeduser = :userid1
+                OR sh.userid = :userid2
+          ORDER BY sh.courseid, sh.timemodified, sh.id";
+        $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) {
+            $carry[] = [
+                'name' => $record->name,
+                'timemodified' => transform::datetime($record->timemodified),
+                'author_of_change_was_you' => transform::yesno($record->userid == $userid),
+                'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid),
+                'action' => static::transform_history_action($record->action)
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+            writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history',
+                (object) ['modified_records' => $data]);
+        });
+    }
+
     /**
      * Extract grade_grade from a record.
      *
index 5677911..6566abf 100644 (file)
@@ -46,5 +46,8 @@ function xmldb_gradingform_guide_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 92e65e3..1150ad0 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_gradingform_rubric_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index b083ffb..7c212ca 100644 (file)
@@ -48,5 +48,8 @@ function xmldb_gradereport_overview_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index ea9d000..892e6b3 100644 (file)
@@ -38,5 +38,8 @@ function xmldb_gradereport_user_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 83605a1..f7dfc15 100644 (file)
@@ -64,6 +64,11 @@ class core_grades_privacy_testcase extends provider_testcase {
         $u4 = $dg->create_user();
         $u5 = $dg->create_user();
         $u6 = $dg->create_user();
+        $u7 = $dg->create_user();
+        $u8 = $dg->create_user();
+        $u9 = $dg->create_user();
+        $u10 = $dg->create_user();
+        $u11 = $dg->create_user();
 
         $sysctx = context_system::instance();
         $c1ctx = context_course::instance($c1->id);
@@ -80,16 +85,22 @@ class core_grades_privacy_testcase extends provider_testcase {
             'fullname' => 'go2']), false);
 
         // Nothing as of now.
-        foreach ([$u1, $u2, $u3, $u4] as $u) {
+        foreach ([$u1, $u2, $u3, $u4, $u5, $u6, $u7, $u8, $u9, $u10, $u11] as $u) {
             $contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids());
             $this->assertEmpty($contexts);
         }
 
         $go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]);
         $go0->insert();
-        $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
+        $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u11->id]);
         $go1->insert();
 
+        // Create scales.
+        $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']);
+        $s1->insert();
+        $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']);
+        $s2->insert();
+
         // User 2 creates history.
         $this->setUser($u2);
         $go0->shortname .= ' edited';
@@ -118,11 +129,18 @@ class core_grades_privacy_testcase extends provider_testcase {
         $this->setUser($u6);
         $gi2a->delete();
 
+        // User 9 creates history.
+        $this->setUser($u9);
+        $s1->name .= ' edited';
+        $s1->update();
+
         // Assert contexts.
         $contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids());
-        $this->assertCount(2, $contexts);
-        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $this->assertCount(1, $contexts);
         $this->assertArrayHasKey($sysctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u11->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
         $contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids());
         $this->assertCount(2, $contexts);
         $this->assertArrayHasKey($sysctx->id, $contexts);
@@ -140,6 +158,23 @@ class core_grades_privacy_testcase extends provider_testcase {
         $contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids());
         $this->assertCount(1, $contexts);
         $this->assertArrayHasKey($c2ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u7->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($sysctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u8->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u9->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($sysctx->id, $contexts);
+
+        // User 10 creates history.
+        $this->setUser($u10);
+        $s2->delete();
+
+        $contexts = array_flip(provider::get_contexts_for_userid($u10->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
     }
 
     public function test_get_contexts_for_userid_grades_and_history() {
@@ -609,6 +644,10 @@ class core_grades_privacy_testcase extends provider_testcase {
         $u4 = $dg->create_user();
         $u5 = $dg->create_user();
         $u6 = $dg->create_user();
+        $u7 = $dg->create_user();
+        $u8 = $dg->create_user();
+        $u9 = $dg->create_user();
+        $u10 = $dg->create_user();
 
         $sysctx = context_system::instance();
         $u1ctx = context_user::instance($u1->id);
@@ -641,6 +680,14 @@ class core_grades_privacy_testcase extends provider_testcase {
         $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
         $go1->insert();
 
+        // Create scales.
+        $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']);
+        $s1->insert();
+        $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']);
+        $s2->insert();
+        $s3 = new grade_scale(['name' => 's3', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c2->id, 'description' => '']);
+        $s3->insert();
+
         // User 2 creates history.
         $this->setUser($u2);
         $go0->shortname .= ' edited';
@@ -669,6 +716,15 @@ class core_grades_privacy_testcase extends provider_testcase {
         $this->setUser($u6);
         $gi2a->delete();
 
+        // User 9 creates history.
+        $this->setUser($u9);
+        $s1->name .= ' edited';
+        $s1->update();
+
+        // User 10 creates history.
+        $this->setUser($u10);
+        $s3->delete();
+
         $this->setAdminUser();
 
         // Export data for u1.
@@ -755,6 +811,74 @@ class core_grades_privacy_testcase extends provider_testcase {
         $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
         $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
             $data->modified_records[0]['action']);
+
+        // Export data for u7.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u7, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertCount(1, $data->scales);
+        $this->assertEquals($s1->name, $data->scales[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']);
+
+        // Export data for u8.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u8, 'core_grades', $allcontexts));
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertCount(1, $data->scales);
+        $this->assertEquals($s2->name, $data->scales[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']);
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history');
+        $this->assertCount(2, $data->modified_records);
+        $this->assertEquals($s3->name, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_change_was_you']);
+        $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_action_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'),
+            $data->modified_records[0]['action']);
+        $this->assertEquals($s3->name, $data->modified_records[1]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[1]['author_of_change_was_you']);
+        $this->assertEquals(transform::yesno(false), $data->modified_records[1]['author_of_action_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
+            $data->modified_records[1]['action']);
+
+        // Export data for u9.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u9, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($s1->name, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'),
+            $data->modified_records[0]['action']);
+
+        // Export data for u10.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u10, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales_history');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($s3->name, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
+            $data->modified_records[0]['action']);
     }
 
     /**
index 502093c..01b2b5f 100644 (file)
--- a/index.php
+++ b/index.php
@@ -111,215 +111,27 @@ $PAGE->set_heading($SITE->fullname);
 $courserenderer = $PAGE->get_renderer('core', 'course');
 echo $OUTPUT->header();
 
-// Print Section or custom info.
 $siteformatoptions = course_get_format($SITE)->get_format_options();
 $modinfo = get_fast_modinfo($SITE);
-$modnames = get_module_types_names();
-$modnamesplural = get_module_types_names(true);
 $modnamesused = $modinfo->get_used_module_names();
-$mods = $modinfo->get_cms();
 
+// Print Section or custom info.
 if (!empty($CFG->customfrontpageinclude)) {
+    // Pre-fill some variables that custom front page might use.
+    $modnames = get_module_types_names();
+    $modnamesplural = get_module_types_names(true);
+    $mods = $modinfo->get_cms();
+
     include($CFG->customfrontpageinclude);
 
 } else if ($siteformatoptions['numsections'] > 0) {
-    if ($editing) {
-        // Make sure section with number 1 exists.
-        course_create_sections_if_missing($SITE, 1);
-        // Re-request modinfo in case section was created.
-        $modinfo = get_fast_modinfo($SITE);
-    }
-    $section = $modinfo->get_section_info(1);
-    if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
-        echo $OUTPUT->box_start('generalbox sitetopic');
-
-        // If currently moving a file then show the current clipboard.
-        if (ismoving($SITE->id)) {
-            $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
-            echo '<p><font size="2">';
-            echo "$stractivityclipboard&nbsp;&nbsp;(<a href=\"course/mod.php?cancelcopy=true&amp;sesskey=".sesskey()."\">";
-            echo get_string('cancel') . '</a>)';
-            echo '</font></p>';
-        }
-
-        $context = context_course::instance(SITEID);
-
-        // If the section name is set we show it.
-        if (trim($section->name) !== '') {
-            echo $OUTPUT->heading(
-                format_string($section->name, true, array('context' => $context)),
-                2,
-                'sectionname'
-            );
-        }
-
-        $summarytext = file_rewrite_pluginfile_urls($section->summary,
-            'pluginfile.php',
-            $context->id,
-            'course',
-            'section',
-            $section->id);
-        $summaryformatoptions = new stdClass();
-        $summaryformatoptions->noclean = true;
-        $summaryformatoptions->overflowdiv = true;
-
-        echo format_text($summarytext, $section->summaryformat, $summaryformatoptions);
-
-        if ($editing && has_capability('moodle/course:update', $context)) {
-            $streditsummary = get_string('editsummary');
-            echo "<a title=\"$streditsummary\" " .
-                 " href=\"course/editsection.php?id=$section->id\">" . $OUTPUT->pix_icon('t/edit', $streditsummary) .
-                 "</a><br /><br />";
-        }
-
-        $courserenderer = $PAGE->get_renderer('core', 'course');
-        echo $courserenderer->course_section_cm_list($SITE, $section);
-
-        echo $courserenderer->course_section_add_cm_control($SITE, $section->section);
-        echo $OUTPUT->box_end();
-    }
+    echo $courserenderer->frontpage_section1();
 }
 // Include course AJAX.
 include_course_ajax($SITE, $modnamesused);
 
-if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
-    $frontpagelayout = $CFG->frontpageloggedin;
-} else {
-    $frontpagelayout = $CFG->frontpage;
-}
-
-foreach (explode(',', $frontpagelayout) as $v) {
-    switch ($v) {
-        // Display the main part of the front page.
-        case FRONTPAGENEWS:
-            if ($SITE->newsitems) {
-                // Print forums only when needed.
-                require_once($CFG->dirroot .'/mod/forum/lib.php');
-
-                if (! $newsforum = forum_get_course_forum($SITE->id, 'news')) {
-                    print_error('cannotfindorcreateforum', 'forum');
-                }
-
-                // Fetch news forum context for proper filtering to happen.
-                $newsforumcm = get_coursemodule_from_instance('forum', $newsforum->id, $SITE->id, false, MUST_EXIST);
-                $newsforumcontext = context_module::instance($newsforumcm->id, MUST_EXIST);
-
-                $forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
-                echo html_writer::link('#skipsitenews',
-                    get_string('skipa', 'access', core_text::strtolower(strip_tags($forumname))),
-                    array('class' => 'skip-block skip'));
-
-                // Wraps site news forum in div container.
-                echo html_writer::start_tag('div', array('id' => 'site-news-forum'));
-
-                if (isloggedin()) {
-                    $SESSION->fromdiscussion = $CFG->wwwroot;
-                    $subtext = '';
-                    if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
-                        if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
-                            $subtext = get_string('unsubscribe', 'forum');
-                        }
-                    } else {
-                        $subtext = get_string('subscribe', 'forum');
-                    }
-                    echo $OUTPUT->heading($forumname);
-                    $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
-                    echo html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
-                } else {
-                    echo $OUTPUT->heading($forumname);
-                }
-
-                forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
-
-                // End site news forum div container.
-                echo html_writer::end_tag('div');
+echo $courserenderer->frontpage();
 
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews'));
-            }
-        break;
-
-        case FRONTPAGEENROLLEDCOURSELIST:
-            $mycourseshtml = $courserenderer->frontpage_my_courses();
-            if (!empty($mycourseshtml)) {
-                echo html_writer::link('#skipmycourses',
-                    get_string('skipa', 'access', core_text::strtolower(get_string('mycourses'))),
-                    array('class' => 'skip skip-block'));
-
-                // Wrap frontpage course list in div container.
-                echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
-                echo $OUTPUT->heading(get_string('mycourses'));
-                echo $mycourseshtml;
-
-                // End frontpage course list div container.
-                echo html_writer::end_tag('div');
-
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses'));
-                break;
-            }
-            // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
-
-        case FRONTPAGEALLCOURSELIST:
-            $availablecourseshtml = $courserenderer->frontpage_available_courses();
-            if (!empty($availablecourseshtml)) {
-                echo html_writer::link('#skipavailablecourses',
-                    get_string('skipa', 'access', core_text::strtolower(get_string('availablecourses'))),
-                    array('class' => 'skip skip-block'));
-
-                // Wrap frontpage course list in div container.
-                echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
-                echo $OUTPUT->heading(get_string('availablecourses'));
-                echo $availablecourseshtml;
-
-                // End frontpage course list div container.
-                echo html_writer::end_tag('div');
-
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses'));
-            }
-        break;
-
-        case FRONTPAGECATEGORYNAMES:
-            echo html_writer::link('#skipcategories',
-                get_string('skipa', 'access', core_text::strtolower(get_string('categories'))),
-                array('class' => 'skip skip-block'));
-
-            // Wrap frontpage category names in div container.
-            echo html_writer::start_tag('div', array('id' => 'frontpage-category-names'));
-
-            echo $OUTPUT->heading(get_string('categories'));
-            echo $courserenderer->frontpage_categories_list();
-
-            // End frontpage category names div container.
-            echo html_writer::end_tag('div');
-
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories'));
-        break;
-
-        case FRONTPAGECATEGORYCOMBO:
-            echo html_writer::link('#skipcourses',
-                get_string('skipa', 'access', core_text::strtolower(get_string('courses'))),
-                array('class' => 'skip skip-block'));
-
-            // Wrap frontpage category combo in div container.
-            echo html_writer::start_tag('div', array('id' => 'frontpage-category-combo'));
-
-            echo $OUTPUT->heading(get_string('courses'));
-            echo $courserenderer->frontpage_combo_list();
-
-            // End frontpage category combo div container.
-            echo html_writer::end_tag('div');
-
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses'));
-        break;
-
-        case FRONTPAGECOURSESEARCH:
-            echo $OUTPUT->box($courserenderer->course_search_form('', 'short'), 'mdl-align');
-        break;
-
-    }
-    echo '<br />';
-}
 if ($editing && has_capability('moodle/course:create', context_system::instance())) {
     echo $courserenderer->add_new_course_button();
 }
index 483049b..12171d7 100644 (file)
@@ -38,7 +38,7 @@ $string['clitypevalue'] = 'iteklado ang halaga';
 $string['clitypevaluedefault'] = 'iteklado ang halaga, pindutin ang Enter para magamit ang default na halaga ({$a})';
 $string['cliunknowoption'] = 'Di-kilalang opsiyon:
  {$a}
-Gamit po ang --help na opsiyon';
+Gamitin po ang --help na opsiyon';
 $string['cliyesnoprompt'] = 'iteklado ang y (ibig sabihin ay yes/oo) o n (ibig sabihin ay no/hindi)';
-$string['environmentrequireinstall'] = 'ay kinakailangang maluklok/mabuhay';
+$string['environmentrequireinstall'] = 'ay dapat ma-install at ma-enable';
 $string['environmentrequireversion'] = 'ang bersiyon {$a->needed} ay kinakailangan at ang pinatatakbo mo ay {$a->current}';
index f921fad..ceb4063 100644 (file)
@@ -32,16 +32,16 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['cannotcreatelangdir'] = 'Hindi makalikha ng lang bgsk.';
 $string['cannotcreatetempdir'] = 'Hindi makalikha ng temp bgsk.';
-$string['cannotdownloadcomponents'] = 'Hindi mailusong ang mga piyesa';
-$string['cannotdownloadzipfile'] = 'Hindi mailusong ang sakong ZIP.';
-$string['cannotfindcomponent'] = 'Hindi makita ang piyesa.';
-$string['cannotsavemd5file'] = 'Hindi maisilid ang sakong md5.';
-$string['cannotsavezipfile'] = 'Hindi maisilid ang sakong ZIP.';
-$string['cannotunzipfile'] = 'Hindi mai-unzip ang sako.';
-$string['componentisuptodate'] = 'Bago ang piyesa.';
-$string['downloadedfilecheckfailed'] = 'Bigo ang  pagsusuri  sa inilusong na sako.';
-$string['invalidmd5'] = 'Ditanggap na md5';
-$string['missingrequiredfield'] = 'May ilang nawawalang pitak na kailangan';
+$string['cannotdownloadcomponents'] = 'Hindi mai-download ang mga sangkap';
+$string['cannotdownloadzipfile'] = 'Hindi mai-download ang ZIP file.';
+$string['cannotfindcomponent'] = 'Hindi makita ang component.';
+$string['cannotsavemd5file'] = 'Hindi mai-save ang file na md5.';
+$string['cannotsavezipfile'] = 'Hindi mai-save ang file na ZIP.';
+$string['cannotunzipfile'] = 'Hindi mai-unzip ang file.';
+$string['componentisuptodate'] = 'Up-to-date ang component.';
+$string['downloadedfilecheckfailed'] = 'Bigo ang  pagsusuri  sa idinownload na file.';
+$string['invalidmd5'] = 'Mali ang check variable - paki-ulit';
+$string['missingrequiredfield'] = 'May ilang nawawalang field na kailangan';
 $string['wrongdestpath'] = 'Mali ang patutunguhang landas';
 $string['wrongsourcebase'] = 'Mali ang URL base ng source.';
-$string['wrongzipfilename'] = 'Mali ang ngalan ng sako na ZIP';
+$string['wrongzipfilename'] = 'Mali ang pangalan ng ZIP file';
index 8824958..ae24c7c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['admindirname'] = 'Pang-Admin na Bugsok';
+$string['admindirname'] = 'Pang-Admin na direktoryo';
 $string['availablelangs'] = 'Magagamit na mga pakete ng wika';
 $string['chooselanguagehead'] = 'Pumilì ng wika';
 $string['chooselanguagesub'] = 'Pumili po ng wika para sa pagluluklok LAMANG.  Sa mga susunod na iskrin ay makakapili ka ng wika para sa site o tagagamit.';
-$string['dataroot'] = 'Bugsok ng Datos';
-$string['dbprefix'] = 'Unlapi ng mga teybol';
-$string['dirroot'] = 'Bugsok ng Moodle';
+$string['dataroot'] = 'Direktoryo ng Datos';
+$string['dbprefix'] = 'Unlapi ng mga table';
+$string['dirroot'] = 'Direktoryo ng Moodle';
 $string['environmenthead'] = 'Sinusuri ang kapaligiran mo...';
 $string['installation'] = 'Pagluklok';
 $string['langdownloaderror'] = 'Ikinalulungkot namin na ang wikang "{$a}" ay hindi nailuklok. Ang kabuuan ng pagluluklok ay itutuloy sa Ingles.';
 $string['memorylimithelp'] = '<p>Ang memory limit ng PHP para sa server mo ay kasalukuyang nakatakda sa {$a}.</p>
 
-<p>Maaaring magdulot ito ng mga problemang pangmemorya sa Moodle sa mga susunod na panahon, lalo na
-   kung marami kang binuhay na modyul at/o marami kang tagagamit.</p>
+<p>Maaaring magdulot ito ng mga problemang pangmemorya sa Moodle sa mga susunod na panahon, lalo na kung marami kang naka-enable na modyul at/o marami kang tagagamit.</p>
 
-<p>Iminumungkahi namin na isaayos mo ang PHP na may mas mataas na limit kung maaari, tulad ng 40M.
-    May iba\'t-ibang paraan na magagawa kayo upang ito ay maiisakatuparan:</p>
+<p>Iminumungkahi namin na isaayos mo ang PHP na may mas mataas na limit kung maaari, tulad ng 40M.  May iba\'t-ibang paraan na magagawa ka upang ito ay maisakatuparan:</p>
 <ol>
-<li>Kunga maaari mong gawin, muling ikompayl ang PHP na may <i>--enable-memory-limit</i>.
+<li>Kung maaari mong gawin, muling ikompayl ang PHP na may <i>--enable-memory-limit</i>.
      Pahihintulutan nito ang Moodle na itakda ang memory limit sa sarili nito.</li>
-<li>Kung mapapasok mo ang iyong sakong php.ini, mababago mo ang <b>memory_limit</b>
-    na kaayusan doon at gawin itong mga 40M.  Kung wala kang karapatang pasukin ito
+<li>Kung mapapasok mo ang iyong php.ini file, mababago mo ang <b>memory_limit</b>
+    na setting doon at gawin itong mga 40M.  Kung wala kang karapatang pasukin ito
     baka puwede mong hilingin sa administrador na gawin ito para sa iyo.</li>
-<li>Sa ilang PHP serve maaari kang lumikha ng isang sakong .htaccess sa bugsok ng Moodle
+<li>Sa ilang PHP server maaari kang lumikha ng isang file na .htaccess sa direktoryo ng Moodle
     na naglalaman ng linyang ito:
-    <p><blockquote>php_value memory_limit 40M</blockquote></p>
+   <blockquote><div>php_value memory_limit 40M</div></blockquote>
     <p>Subali\'t sa ilang server ay pipigilin nito ang paggana ng <b>lahat</b> ng pahinang PHP
-    (makakakita ka ng mga error kapag tumingin ka sa mga pahina) kaya\'t kakailanganin mong tanggalin ang sakong .htaccess.</p></li>
+    (makakakita ka ng mga error kapag tumingin ka sa mga pahina) kaya\'t kakailanganin mong tanggalin ang .htaccess file.</p></li>
 </ol>';
 $string['phpversion'] = 'Bersiyon ng PHP';
 $string['phpversionhelp'] = '<p>Kinakailangan ng Moodle ang isang bersiyon ng PHP na kahit man lamang 4.3.0. o 5.1.0 (ang 5.0.x ay maraming problema)</p>
@@ -69,6 +67,6 @@ $string['welcomep20'] = 'Nakikita mo ang pahinang ito dahil matagumpay mong nail
 $string['welcomep30'] = 'Ang lathala ng <strong>{$a->installername}</strong> na ito ay naglalaman ng mga aplikasyon na lilikha ng kapaligiran na tatakbuhan ng  <strong>Moodle</strong>, ito ay ang mga sumusunod:';
 $string['welcomep40'] = 'Nilalaman din ng paketeng ito ang  <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'Ang paggamit ng lahat ng aplikasyon sa paketeng ito ay alinsunod sa kani-kaniyang lisensiya.  Ang kumpletong pakete na <strong>{$a->installername}</strong> ay  <a href="http://www.opensource.org/docs/definition_plain.html">open source</a> at ipinamamahagi alinsunod sa lisensiyang <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
-$string['welcomep60'] = 'Dadalhin kayo ng mga sumusunod na pahina sa mga madaling hakbang upang maisaayos at mapatakbo ang <strong>Moodle</strong> sa kompyuter ninyo.  Kung gusto ninyo ay panatilihin ang umiiral o kaya ay baguhin ito ayon sa inyong pangangailangan.';
+$string['welcomep60'] = 'Dadalhin ka ng mga sumusunod na pahina sa mga madaling sundang hakbang upang maisaayos at mapatakbo ang <strong>Moodle</strong> sa kompyuter mo.  Maaari mong tanggapin ang default o kaya ay baguhin ito ayon sa inyong pangangailangan.';
 $string['welcomep70'] = 'Iklik ang "Susunod" na buton sa ibaba upang maituloy ang pasasaayos ng <strong>Moodle</strong>.';
 $string['wwwroot'] = 'Web address';
index 17be075..aa79e8e 100644 (file)
@@ -825,6 +825,7 @@ $string['order3'] = 'Third';
 $string['order4'] = 'Fourth';
 $string['outgoingmailconfig'] = 'Outgoing mail configuration';
 $string['overridetossl'] = 'HTTPS for logins has now been deprecated. This instance is now forced to SSL. To remedy this warning change your wwwroot in config.php to https://';
+$string['pageinfodebugsummary'] = 'This page is: {$a}';
 $string['passwordchangelogout'] = 'Log out after password change';
 $string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
 $string['passwordchangetokendeletion'] = 'Remove web service access tokens after password change';
index aec0710..b3d26eb 100644 (file)
@@ -615,6 +615,17 @@ $string['prefrows'] = 'Special rows';
 $string['prefshow'] = 'Show/hide toggles';
 $string['previewrows'] = 'Preview rows';
 $string['privacy:metadata:categorieshistory'] = 'A record of previous versions of grade categories';
+$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import';
+$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports';
+$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data';
+$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name';
+$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades';
+$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback';
+$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value';
+$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports';
+$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data';
+$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported';
+$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported';
 $string['privacy:metadata:grades'] = 'A record of grades';
 $string['privacy:metadata:grades:aggregationstatus'] = 'The aggregation status';
 $string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregation';
@@ -632,6 +643,10 @@ $string['privacy:metadata:outcomes'] = 'A record of outcomes';
 $string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified';
 $string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record';
 $string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes';
+$string['privacy:metadata:scale'] = 'A record of scales';
+$string['privacy:metadata:scale:timemodified'] = 'Time at which the record was last modified';
+$string['privacy:metadata:scale:userid'] = 'The user who last modified the record';
+$string['privacy:metadata:scalehistory'] = 'A record of previous versions of scales';
 $string['privacy:path:relatedtome'] = 'Related to me';
 $string['privacy:request:historyactiondelete'] = 'Delete';
 $string['privacy:request:historyactioninsert'] = 'Insert';
index d3b47b5..e83cbf3 100644 (file)
@@ -1563,6 +1563,16 @@ $string['privacy:metadata:events_queue:eventdata'] = 'The data stored in the eve
 $string['privacy:metadata:events_queue:stackdump'] = 'Any stacktrace associated with this event.';
 $string['privacy:metadata:events_queue:timecreated'] = 'The time that this event was created.';
 $string['privacy:metadata:events_queue:userid'] = 'The userid associated with this event.';
+$string['privacy:metadata:log'] = 'A collection of past events';
+$string['privacy:metadata:log:action'] = 'A description of the action';
+$string['privacy:metadata:log:cmid'] = 'cmid';
+$string['privacy:metadata:log:course'] = 'course';
+$string['privacy:metadata:log:info'] = 'Additional information';
+$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
+$string['privacy:metadata:log:module'] = 'module';
+$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:url'] = 'The URL related to the event';
+$string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
 $string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
 $string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
index 792cb6e..32d846a 100644 (file)
@@ -170,6 +170,16 @@ $string['privacy:metadata'] = 'The portfolio subsystem acts as a channel, passin
 $string['privacy:metadata:name'] = 'Name of the preference.';
 $string['privacy:metadata:instance'] = 'Identifier for the portfolio.';
 $string['privacy:metadata:instancesummary'] = 'This stores portfolio both instances and preferences for the portfolios user is using.';
+$string['privacy:metadata:portfolio_log'] = 'Log of portfolio transfers (used to later check for duplicates)';
+$string['privacy:metadata:portfolio_log:caller_class'] = 'Name of the class used to create the transfer';
+$string['privacy:metadata:portfolio_log:caller_component'] = 'Component name responsible for exporting';
+$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started)';
+$string['privacy:metadata:portfolio_log:userid'] = 'User who exported content';
+$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports, cleaned by cron after one day';
+$string['privacy:metadata:portfolio_tempdata:data'] = 'Export data';
+$string['privacy:metadata:portfolio_tempdata:expirytime'] = 'Time this record will expire';
+$string['privacy:metadata:portfolio_tempdata:instance'] = 'Portfolio plugin instance being used';
+$string['privacy:metadata:portfolio_tempdata:userid'] = 'User performing export';
 $string['privacy:metadata:value'] = 'Value for the preference';
 $string['privacy:metadata:userid'] = 'The user Identifier.';
 $string['privacy:path'] = 'Portfolio instances';
index 8a4d933..8b0fd76 100644 (file)
@@ -75,6 +75,10 @@ $string['privacy:metadata:middlename'] = 'The middle name of the user.';
 $string['privacy:metadata:mnethostid'] = 'An identifier for the mnet host if used.';
 $string['privacy:metadata:model'] = 'The device name, occam or iPhone etc..';
 $string['privacy:metadata:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:my_pages'] = 'User pages - dashboard and profile. This table does not contain personal data and only used to link dashboard blocks to users';
+$string['privacy:metadata:my_pages:name'] = 'Page name';
+$string['privacy:metadata:my_pages:private'] = 'Whether or not the page is private (dashboard) or public (profile)';
+$string['privacy:metadata:my_pages:userid'] = 'The user who owns this page or 0 for system defaults';
 $string['privacy:metadata:password'] = 'The password for this user to log into the system.';
 $string['privacy:metadata:passwordresettablesummary'] = 'A table tracking password reset confirmation tokens';
 $string['privacy:metadata:passwordtablesummary'] = 'A rotating log of hashes of previously used passwords for the user.';
@@ -87,6