Merge branch 'wip-nolink-bug' of https://gitlab.di.unito.it/rabellino/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 4 Jul 2018 06:37:14 +0000 (14:37 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 4 Jul 2018 06:37:14 +0000 (14:37 +0800)
183 files changed:
.stylelintignore
Gruntfile.js
admin/classes/form/purge_caches.php [new file with mode: 0644]
admin/cli/purge_caches.php
admin/purgecaches.php
admin/searchareas.php
admin/settings/development.php
admin/tests/behat/purge_caches.feature [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/request_filter.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/data_registry.js
admin/tool/dataprivacy/amd/src/expand_contract.js
admin/tool/dataprivacy/amd/src/request_filter.js [new file with mode: 0644]
admin/tool/dataprivacy/categories.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/expired_user_contexts.php
admin/tool/dataprivacy/classes/form/context_instance.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/data_deletion_page.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/request_filter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/classes/purpose.php
admin/tool/dataprivacy/datadeletion.php
admin/tool/dataprivacy/dataregistry.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/editcategory.php
admin/tool/dataprivacy/editpurpose.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/pluginregistry.php
admin/tool/dataprivacy/purposes.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_deletion.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/templates/data_request_email.mustache
admin/tool/dataprivacy/templates/data_request_modal.mustache
admin/tool/dataprivacy/templates/data_requests.mustache
admin/tool/dataprivacy/templates/request_filter.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/log/classes/local/privacy/helper.php
admin/tool/log/classes/privacy/provider.php
admin/tool/log/lang/en/tool_log.php
admin/tool/log/settings.php
admin/tool/log/version.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/tests/behat/consent.feature
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/actions/edit_xml_file/edit_xml_file.class.php
admin/tool/xmldb/actions/view_table_php/view_table_php.class.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/lang/en/auth_oauth2.php
blocks/recent_activity/lang/en/block_recent_activity.php
calendar/classes/local/event/forms/create.php
calendar/tests/behat/calendar.feature
course/classes/management_renderer.php
course/classes/search/section.php
course/externallib.php
course/format/lib.php
course/format/upgrade.txt
course/reset_form.php
course/tests/externallib_test.php
dataformat/json/lang/en/dataformat_json.php
dataformat/ods/lang/en/dataformat_ods.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/paypal/db/install.xml
enrol/paypal/db/upgrade.php
enrol/paypal/lang/en/enrol_paypal.php
enrol/paypal/version.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
filter/mathjaxloader/filter.php
lang/en/admin.php
lang/en/analytics.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/completion.php
lang/en/hub.php
lang/en/moodle.php
lang/en/notes.php
lang/en/role.php
lang/en/search.php
lang/en/user.php
lang/en/userkey.php
lang/en/webservice.php
lib/accesslib.php
lib/classes/hub/registration.php
lib/classes/hub/site_registration_form.php
lib/db/install.xml
lib/db/upgrade.php
lib/environmentlib.php
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/excellib.class.php
lib/filelib.php
lib/filterlib.php
lib/mathslib.php
lib/moodlelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_filters.php [new file with mode: 0644]
lib/tests/filterlib_test.php
lib/tests/mathslib_test.php
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_key.php
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/classes/privacy/provider.php
mod/assign/feedback/comments/lang/en/assignfeedback_comments.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/lang/en/assign.php
mod/assign/submission/onlinetext/lang/en/assignsubmission_onlinetext.php
mod/assign/tests/generator.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/tests/privacy_test.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/lang/en/data.php
mod/feedback/lang/en/feedback.php
mod/forum/lang/en/forum.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/tests/behat/categories.feature
mod/lesson/renderer.php
mod/lesson/tests/behat/lesson_complete_report.feature
mod/lesson/tests/behat/lesson_delete_answers.feature
mod/lesson/tests/behat/lesson_informations_at_end.feature
mod/lesson/tests/behat/lesson_number_of_student_attempts.feature
mod/lesson/tests/behat/lesson_outline_report.feature
mod/lesson/tests/behat/lesson_question_attempts.feature
mod/lesson/tests/behat/lesson_report.feature
mod/lesson/tests/behat/lesson_review.feature
mod/lesson/tests/behat/lesson_student_dashboard.feature
mod/lesson/tests/behat/lesson_student_resume.feature
mod/lesson/tests/behat/questions_images.feature
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/page/lang/en/page.php
mod/page/lib.php
mod/page/mod_form.php
mod/page/settings.php
mod/page/tests/behat/page_appearance.feature [new file with mode: 0644]
mod/page/tests/generator/lib.php
mod/page/version.php
mod/page/view.php
mod/quiz/lang/en/quiz.php
mod/quiz/report/grading/lang/en/quiz_grading.php
mod/quiz/report/overview/db/install.xml
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/overview/version.php
mod/scorm/lang/en/scorm.php
mod/workshop/lang/en/workshop.php
npm-shrinkwrap.json
package.json
question/type/shortanswer/renderer.php
report/stats/lang/en/report_stats.php
repository/wikimedia/lang/en/repository_wikimedia.php
repository/youtube/lang/en/repository_youtube.php
search/engine/simpledb/lang/en/search_simpledb.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/config.php
theme/boost/lib.php
theme/boost/readme_moodle.txt
theme/boost/scss/moodle/course.scss
theme/boost/style/moodle.css [new file with mode: 0644]
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
theme/styles.php
version.php

index 6368112..b0dc0dc 100644 (file)
@@ -2,6 +2,7 @@
 theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
+theme/boost/style/moodle.css
 node_modules/
 vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
index e0afa2d..4501fc6 100644 (file)
@@ -137,6 +137,16 @@ module.exports = function(grunt) {
                 }
            }
         },
+        sass: {
+            dist: {
+                files: {
+                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
+                }
+            },
+            options: {
+                includePaths: ["theme/boost/scss/"]
+            }
+        },
         watch: {
             options: {
                 nospawn: true // We need not to spawn so config can be changed dynamically.
@@ -214,7 +224,8 @@ module.exports = function(grunt) {
           '# Generated by "grunt ignorefiles"',
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
-          'theme/more/style/custom.css'
+          'theme/more/style/custom.css',
+          'theme/boost/style/moodle.css'
       ].concat(thirdPartyPaths);
       grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
     };
@@ -362,6 +373,7 @@ module.exports = function(grunt) {
     grunt.loadNpmTasks('grunt-contrib-uglify');
     grunt.loadNpmTasks('grunt-contrib-less');
     grunt.loadNpmTasks('grunt-contrib-watch');
+    grunt.loadNpmTasks('grunt-sass');
     grunt.loadNpmTasks('grunt-eslint');
     grunt.loadNpmTasks('grunt-stylelint');
 
@@ -374,7 +386,7 @@ module.exports = function(grunt) {
     grunt.registerTask('js', ['amd', 'yui']);
 
     // Register CSS taks.
-    grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
+    grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
 
     // Register the startup task.
     grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
diff --git a/admin/classes/form/purge_caches.php b/admin/classes/form/purge_caches.php
new file mode 100644 (file)
index 0000000..3151445
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * Form for selective purging of caches.
+ *
+ * @package    core
+ * @copyright  2018 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form for selecting which caches to purge on admin/purgecaches.php
+ *
+ * @package   core
+ * @copyright 2018 The Open University
+ * @author    Mark Johnson <mark.johnson@open.ac.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class purge_caches extends \moodleform {
+    /**
+     * Define a "Purge all caches" button, and a fieldset with checkboxes for selectively purging separate caches.
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
+        $mform->setType('returnurl', PARAM_LOCALURL);
+        $mform->addElement('submit', 'all', get_string('purgecaches', 'admin'));
+        $mform->addElement('header', 'purgecacheheader', get_string('purgeselectedcaches', 'admin'));
+        $checkboxes = [
+            $mform->createElement('advcheckbox', 'theme', '', get_string('purgethemecache', 'admin')),
+            $mform->createElement('advcheckbox', 'lang', '', get_string('purgelangcache', 'admin')),
+            $mform->createElement('advcheckbox', 'js', '', get_string('purgejscache', 'admin')),
+            $mform->createElement('advcheckbox', 'filter', '', get_string('purgefiltercache', 'admin')),
+            $mform->createElement('advcheckbox', 'muc', '', get_string('purgemuc', 'admin')),
+            $mform->createElement('advcheckbox', 'other', '', get_string('purgeothercaches', 'admin'))
+        ];
+        $mform->addGroup($checkboxes, 'purgeselectedoptions');
+        $mform->addElement('submit', 'purgeselectedcaches', get_string('purgeselectedcaches', 'admin'));
+    }
+
+    /**
+     * If the "Purge selected caches" button was pressed, ensure at least one cache was selected.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array Error messages
+     */
+    public function validation($data, $files) {
+        $errors = [];
+        if (isset($data['purgeselectedcaches']) && empty(array_filter($data['purgeselectedoptions']))) {
+            $errors['purgeselectedoptions'] = get_string('purgecachesnoneselected', 'admin');
+        }
+        return $errors;
+    }
+}
index 64f29bd..9846b20 100644 (file)
@@ -28,7 +28,16 @@ define('CLI_SCRIPT', true);
 require(__DIR__.'/../../config.php');
 require_once($CFG->libdir.'/clilib.php');
 
-list($options, $unrecognized) = cli_get_params(array('help' => false), array('h' => 'help'));
+$longoptions = [
+    'help' => false,
+    'muc' => false,
+    'theme' => false,
+    'lang' => false,
+    'js' => false,
+    'filter' => false,
+    'other' => false
+];
+list($options, $unrecognized) = cli_get_params($longoptions, ['h' => 'help']);
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -36,20 +45,32 @@ if ($unrecognized) {
 }
 
 if ($options['help']) {
-    $help =
-"Invalidates all Moodle internal caches
+    // The indentation of this string is "wrong" but this is to avoid a extra whitespace in console output.
+    $help = <<<EOF
+Invalidates Moodle internal caches
+
+Specific caches can be defined (alone or in combination) using arguments. If none are specified,
+all caches will be purged.
 
 Options:
 -h, --help            Print out this help
+    --muc             Purge all MUC caches (includes lang cache)
+    --theme           Purge theme cache
+    --lang            Purge language string cache
+    --js              Purge JavaScript cache
+    --filter          Purge text filter cache
+    --other           Purge all file caches and other miscellaneous caches (may include MUC
+                      if using cachestore_file).
 
 Example:
-\$sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
-";
+\$ sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
+
+EOF;
 
     echo $help;
     exit(0);
 }
 
-purge_all_caches();
+purge_caches(array_filter($options));
 
 exit(0);
index dd47087..b4401c4 100644 (file)
@@ -27,36 +27,43 @@ require_once('../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
 $confirm = optional_param('confirm', 0, PARAM_BOOL);
-$returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
+$returnurl = optional_param('returnurl', '/admin/purgecaches.php', PARAM_LOCALURL);
+$returnurl = new moodle_url($returnurl);
 
 admin_externalpage_setup('purgecaches');
 
+$form = new core_admin\form\purge_caches(null, ['returnurl' => $returnurl]);
+
 // If we have got here as a confirmed aciton, do it.
-if ($confirm && confirm_sesskey()) {
+if ($data = $form->get_data()) {
 
     // Valid request. Purge, and redirect the user back to where they came from.
-    purge_all_caches();
+    $selected = $data->purgeselectedoptions;
+    purge_caches($selected);
 
-    if ($returnurl) {
-        $returnurl = $CFG->wwwroot . $returnurl;
+    if (isset($data->all)) {
+        $message = get_string('purgecachesfinished', 'admin');
     } else {
-        $returnurl = new moodle_url('/admin/purgecaches.php');
+        $message = get_string('purgeselectedcachesfinished', 'admin');
     }
-    redirect($returnurl, get_string('purgecachesfinished', 'admin'));
+
+} else if ($confirm && confirm_sesskey()) {
+    purge_caches();
+    $message = get_string('purgecachesfinished', 'admin');
 }
 
-// Otherwise, show a button to actually purge the caches.
-$actionurl = new moodle_url('/admin/purgecaches.php', array('sesskey'=>sesskey(), 'confirm'=>1));
-if ($returnurl) {
-    $actionurl->param('returnurl', $returnurl);
+if (isset($message)) {
+    redirect($returnurl, $message);
 }
 
+// Otherwise, show a form to actually purge the caches.
+
 echo $OUTPUT->header();
-echo $OUTPUT->heading(get_string('purgecaches', 'admin'));
+echo $OUTPUT->heading(get_string('purgecachespage', 'admin'));
 
 echo $OUTPUT->box_start('generalbox', 'notice');
 echo html_writer::tag('p', get_string('purgecachesconfirm', 'admin'));
-echo $OUTPUT->single_button($actionurl, get_string('purgecaches', 'admin'), 'post');
+echo $form->render();
 echo $OUTPUT->box_end();
 
 echo $OUTPUT->footer();
index bf178b5..911a86b 100644 (file)
@@ -28,113 +28,139 @@ admin_externalpage_setup('searchareas');
 
 $areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
 $action = optional_param('action', null, PARAM_ALPHA);
+$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
 
+// Get a search manager instance, which we'll need for display and to handle some actions.
 try {
     $searchmanager = \core_search\manager::instance();
 } catch (core_search\engine_exception $searchmanagererror) {
-    // Continue, we return an error later depending on the requested action.
+    // In action cases, we'll throw this exception below. In non-action cases, we produce a lang string error.
 }
 
+// Handle all the actions.
 if ($action) {
-
+    // If dealing with an areaid, we need to check that the area exists.
     if ($areaid) {
-        // We need to check that the area exists.
         $area = \core_search\manager::get_search_area($areaid);
         if ($area === false) {
             throw new moodle_exception('invalidrequest');
         }
     }
 
-    if ($action !== 'enable' && $action !== 'disable') {
-        // All actions but enable/disable need the search engine to be ready.
-        if (!empty($searchmanagererror)) {
-            throw $searchmanagererror;
-        }
+    // All the indexing actions.
+    if (in_array($action, ['delete', 'indexall', 'reindexall', 'deleteall'])) {
 
-        // Show confirm prompt for all these actions as they may be inadvisable, or may cause
-        // an interruption in search functionality, on production systems.
-        if (!optional_param('confirm', 0, PARAM_INT)) {
-            // Display confirmation prompt.
-            $a = null;
-            if ($areaid) {
-                $a = html_writer::tag('strong', $area->get_visible_name());
-            }
+        // All of these actions require that indexing is enabled.
+        if ($indexingenabled) {
 
-            $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
-            if ($areaid) {
-                $actionparams['areaid'] = $areaid;
+            // For all of these actions, we strictly need a manager instance.
+            if (isset($searchmanagererror)) {
+                throw $searchmanagererror;
             }
-            $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
-            $cancelurl = new moodle_url('/admin/searchareas.php');
-            echo $OUTPUT->header();
-            echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
+
+            // Show confirm prompt for all these actions as they may be inadvisable, or may cause
+            // an interruption in search functionality, on production systems.
+            if (!optional_param('confirm', 0, PARAM_INT)) {
+                // Display confirmation prompt.
+                $a = null;
+                if ($areaid) {
+                    $a = html_writer::tag('strong', $area->get_visible_name());
+                }
+
+                $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
+                if ($areaid) {
+                    $actionparams['areaid'] = $areaid;
+                }
+                $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
+                $cancelurl = new moodle_url('/admin/searchareas.php');
+                echo $OUTPUT->header();
+                echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
                     new single_button($actionurl, get_string('continue'), 'post', true),
                     new single_button($cancelurl, get_string('cancel'), 'get'));
-            echo $OUTPUT->footer();
-            exit;
+                echo $OUTPUT->footer();
+                exit;
+            } else {
+                // Confirmed, so run the required action.
+                require_sesskey();
+
+                switch ($action) {
+                    case 'delete':
+                        $searchmanager->delete_index($areaid);
+                        \core\notification::success(get_string('searchindexdeleted', 'admin'));
+                        break;
+                    case 'indexall':
+                        $searchmanager->index();
+                        \core\notification::success(get_string('searchindexupdated', 'admin'));
+                        break;
+                    case 'reindexall':
+                        $searchmanager->index(true);
+                        \core\notification::success(get_string('searchreindexed', 'admin'));
+                        break;
+                    case 'deleteall':
+                        $searchmanager->delete_index();
+                        \core\notification::success(get_string('searchalldeleted', 'admin'));
+                        break;
+                    default:
+                        break;
+                }
+
+                // Redirect back to the main page after taking action.
+                redirect(new moodle_url('/admin/searchareas.php'));
+            }
         }
-    }
+    } else if (in_array($action, ['enable', 'disable'])) {
+        // Toggling search areas requires no confirmation.
+        require_sesskey();
 
-    // We are now taking an actual action, so require sesskey.
-    require_sesskey();
-
-    switch ($action) {
-        case 'enable':
-            $area->set_enabled(true);
-            \core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'disable':
-            $area->set_enabled(false);
-            \core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'delete':
-            $search = \core_search\manager::instance();
-            $search->delete_index($areaid);
-            \core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'indexall':
-            $searchmanager->index();
-            \core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'reindexall':
-            $searchmanager->index(true);
-            \core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'deleteall':
-            $searchmanager->delete_index();
-            \core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        default:
-            throw new moodle_exception('invalidaction');
-            break;
-    }
+        switch ($action) {
+            case 'enable':
+                $area->set_enabled(true);
+                \core\notification::success(get_string('searchareaenabled', 'admin'));
+                break;
+            case 'disable':
+                $area->set_enabled(false);
+                core\notification::success(get_string('searchareadisabled', 'admin'));
+                break;
+            default:
+                break;
+        }
 
-    // Redirect back to the main page after taking action.
-    redirect(new moodle_url('/admin/searchareas.php'));
+        redirect(new moodle_url('/admin/searchareas.php'));
+    } else {
+        // Invalid action.
+        throw new moodle_exception('invalidaction');
+    }
 }
 
-echo $OUTPUT->header();
 
-$searchareas = \core_search\manager::get_search_areas_list();
-if (empty($searchmanagererror)) {
-    $areasconfig = $searchmanager->get_areas_config($searchareas);
+// Display.
+if (isset($searchmanager) && $indexingenabled) {
+    \core\notification::info(get_string('indexinginfo', 'admin'));
+} else if (isset($searchmanager)) {
+    $params = (object) [
+        'url' => (new moodle_url("/admin/settings.php?section=manageglobalsearch#admin-searchindexwhendisabled"))->out(false)
+    ];
+    \core\notification::error(get_string('indexwhendisabledfullnotice', 'search', $params));
 } else {
-    $areasconfig = false;
-}
-
-if (!empty($searchmanagererror)) {
+    // In non-action cases, init errors are translated and displayed to the user as error notifications.
     $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
-    echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
-} else {
-    echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
+    \core\notification::error($errorstr);
 }
 
+echo $OUTPUT->header();
+
 $table = new html_table();
 $table->id = 'core-search-areas';
+$table->head = [
+    get_string('searcharea', 'search'),
+    get_string('enable'),
+    get_string('newestdocindexed', 'admin'),
+    get_string('searchlastrun', 'admin'),
+    get_string('searchindexactions', 'admin')
+];
 
-$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
-    get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
-
+$searchareas = \core_search\manager::get_search_areas_list();
+$areasconfig = isset($searchmanager) ? $searchmanager->get_areas_config($searchareas) : false;
 foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
@@ -144,7 +170,7 @@ foreach ($searchareas as $area) {
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
             null, array('title' => get_string('disable')));
 
-        if ($areasconfig) {
+        if ($areasconfig && $indexingenabled) {
             $columns[] = $areasconfig[$areaid]->lastindexrun;
 
             if ($areasconfig[$areaid]->indexingstart) {
@@ -173,7 +199,11 @@ foreach ($searchareas as $area) {
             $columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
 
         } else {
-            $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            if (!$areasconfig) {
+                $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            } else {
+                $blankrow = new html_table_cell(get_string('indexwhendisabledshortnotice', 'search'));
+            }
             $blankrow->colspan = 3;
             $columns[] = $blankrow;
         }
@@ -192,10 +222,7 @@ foreach ($searchareas as $area) {
 }
 
 // Cross-search area tasks.
-$options = array();
-if (!empty($searchmanagererror)) {
-    $options['disabled'] = true;
-}
+$options = (isset($searchmanager) && $indexingenabled) ? [] : ['disabled' => true];
 echo $OUTPUT->box_start('search-areas-actions');
 echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
 echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
@@ -204,7 +231,7 @@ echo $OUTPUT->box_end();
 
 echo html_writer::table($table);
 
-if (empty($searchmanagererror)) {
+if (isset($searchmanager)) {
     // Show information about queued index requests for specific contexts.
     $searchrenderer = $PAGE->get_renderer('core_search');
     echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
index f8dab27..cc2a620 100644 (file)
@@ -83,7 +83,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         $ADMIN->add('development', new admin_externalpage('mnettestclient', new lang_string('testclient', 'mnet'), "$CFG->wwwroot/$CFG->admin/mnet/testclient.php"));
     }
 
-    $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecaches','admin'), "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
+    $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecachespage', 'admin'),
+            "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
 
     $ADMIN->add('development', new admin_externalpage('thirdpartylibs', new lang_string('thirdpartylibs','admin'), "$CFG->wwwroot/$CFG->admin/thirdpartylibs.php"));
 } // end of speedup
diff --git a/admin/tests/behat/purge_caches.feature b/admin/tests/behat/purge_caches.feature
new file mode 100644 (file)
index 0000000..7f1e805
--- /dev/null
@@ -0,0 +1,34 @@
+@core @core_admin
+Feature: Purge caches
+  In order to see changes to cached data
+  As a Moodle administrator
+  I want manually purge different data and file caches
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Development > Purge caches" in site administration
+
+  Scenario: Purge all caches
+    Given I should not see "All caches were purged"
+    When I press "Purge all caches"
+    Then I should see "All caches were purged"
+
+  Scenario: Purge selected caches
+    Given I should not see "Selected caches were purged"
+    When I set the field "Themes" to "1"
+    And I press "Purge selected caches"
+    Then I should see "The selected caches were purged"
+
+  Scenario: Purge selected caches without selecting any caches
+    Given I should not see "Select one or more caches to purge"
+    When I press "Purge selected caches"
+    Then I should not see "The selected caches were purged"
+    And I should see "Select one or more caches to purge"
+
+  Scenario: Redirect back to the original page after following a Purge all caches link
+    Given I am on site homepage
+    And I should see "Available courses"
+    And I should not see "All caches were purged"
+    When I follow "Purge all caches"
+    Then I should see "All caches were purged"
+    And I should see "Available courses"
diff --git a/admin/tool/dataprivacy/amd/build/request_filter.min.js b/admin/tool/dataprivacy/amd/build/request_filter.min.js
new file mode 100644 (file)
index 0000000..61344eb
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/request_filter.min.js differ
index 76f00ee..07eb18d 100644 (file)
@@ -234,6 +234,7 @@ define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates'
                     },
                     fail: Notification.exception
                 }]);
+                return;
             }).catch(Notification.exception);
 
         };
index 41b7e50..cf509b5 100644 (file)
@@ -34,7 +34,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
          *
          * @param  {object} targetnode The node that we want to expand / collapse
          * @param  {object} thisnode The node that was clicked.
-         * @return {null}
          */
         expandCollapse: function(targetnode, thisnode) {
             if (targetnode.hasClass('hide')) {
@@ -58,7 +57,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
          * Expand or collapse all nodes on this page.
          *
          * @param  {string} nextstate The next state to change to.
-         * @return {null}
          */
         expandCollapseAll: function(nextstate) {
             var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
@@ -75,6 +73,7 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
 
             str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
                 $('.tool_dataprivacy-expand-all').html(langString);
+                return;
             }).catch(Notification.exception);
 
             $(':header i.fa').each(function() {
diff --git a/admin/tool/dataprivacy/amd/src/request_filter.js b/admin/tool/dataprivacy/amd/src/request_filter.js
new file mode 100644 (file)
index 0000000..6b915c8
--- /dev/null
@@ -0,0 +1,84 @@
+// 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/>.
+
+/**
+ * JS module for the data requests filter.
+ *
+ * @module     tool_dataprivacy/request_filter
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
+
+    /**
+     * Selectors.
+     *
+     * @access private
+     * @type {{REQUEST_FILTERS: string}}
+     */
+    var SELECTORS = {
+        REQUEST_FILTERS: '#request-filters'
+    };
+
+    /**
+     * Init function.
+     *
+     * @method init
+     * @private
+     */
+    var init = function() {
+        var stringkeys = [
+            {
+                key: 'filter',
+                component: 'moodle'
+            },
+            {
+                key: 'nofiltersapplied',
+                component: 'moodle'
+            }
+        ];
+
+        Str.get_strings(stringkeys).then(function(langstrings) {
+            var placeholder = langstrings[0];
+            var noSelectionString = langstrings[1];
+            return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
+        }).fail(Notification.exception);
+
+        var last = $(SELECTORS.REQUEST_FILTERS).val();
+        $(SELECTORS.REQUEST_FILTERS).on('change', function() {
+            var current = $(this).val();
+            // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
+            if (last.join(',') !== current.join(',')) {
+                // If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
+                if (current.length === 0) {
+                    $('#filters-cleared').val(1);
+                }
+                $(this.form).submit();
+            }
+        });
+    };
+
+    return /** @alias module:core/form-autocomplete */ {
+        /**
+         * Initialise the unified user filter.
+         *
+         * @method init
+         */
+        init: function() {
+            init();
+        }
+    };
+});
index b160ff3..f323278 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $url = new moodle_url("/admin/tool/dataprivacy/categories.php");
 $title = get_string('editcategories', 'tool_dataprivacy');
 
index 05efe9c..42acb42 100644 (file)
@@ -197,7 +197,7 @@ class api {
             } else {
                 // If not a DPO, only users with the capability to make data requests for the user should be allowed.
                 // (e.g. users with the Parent role, etc).
-                if (!api::can_create_data_request_for_user($foruser)) {
+                if (!self::can_create_data_request_for_user($foruser)) {
                     $forusercontext = \context_user::instance($foruser);
                     throw new required_capability_exception($forusercontext,
                             'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
@@ -232,16 +232,42 @@ class api {
      * (e.g. Users with the Data Protection Officer roles)
      *
      * @param int $userid The User ID.
+     * @param int[] $statuses The status filters.
+     * @param int[] $types The request type filters.
+     * @param string $sort The order by clause.
+     * @param int $offset Amount of records to skip.
+     * @param int $limit Amount of records to fetch.
      * @return data_request[]
+     * @throws coding_exception
      * @throws dml_exception
      */
-    public static function get_data_requests($userid = 0) {
+    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
         global $DB, $USER;
         $results = [];
-        $sort = 'status ASC, timemodified ASC';
+        $sqlparams = [];
+        $sqlconditions = [];
+
+        // Set default sort.
+        if (empty($sort)) {
+            $sort = 'status ASC, timemodified ASC';
+        }
+
+        // Set status filters.
+        if (!empty($statuses)) {
+            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "status $statusinsql";
+        }
+
+        // Set request type filter.
+        if (!empty($types)) {
+            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "type $typeinsql";
+            $sqlparams = array_merge($sqlparams, $typeparams);
+        }
+
         if ($userid) {
             // Get the data requests for the user or data requests made by the user.
-            $select = "(userid = :userid OR requestedby = :requestedby)";
+            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
             $params = [
                 'userid' => $userid,
                 'requestedby' => $userid
@@ -256,20 +282,87 @@ class api {
                 $alloweduserids = array_merge($alloweduserids, array_keys($children));
             }
             list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
-            $select .= " AND userid $insql";
-            $params = array_merge($params, $inparams);
+            $sqlconditions[] .= "userid $insql";
+            $select = implode(' AND ', $sqlconditions);
+            $params = array_merge($params, $inparams, $sqlparams);
 
-            $results = data_request::get_records_select($select, $params, $sort);
+            $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
         } else {
             // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
             if (self::is_site_dpo($USER->id)) {
-                $results = data_request::get_records(null, $sort, '');
+                if (!empty($sqlconditions)) {
+                    $select = implode(' AND ', $sqlconditions);
+                    $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
+                } else {
+                    $results = data_request::get_records(null, $sort, '', $offset, $limit);
+                }
             }
         }
 
         return $results;
     }
 
+    /**
+     * Fetches the count of data request records based on the given parameters.
+     *
+     * @param int $userid The User ID.
+     * @param int[] $statuses The status filters.
+     * @param int[] $types The request type filters.
+     * @return int
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
+        global $DB, $USER;
+        $count = 0;
+        $sqlparams = [];
+        $sqlconditions = [];
+        if (!empty($statuses)) {
+            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "status $statusinsql";
+        }
+        if (!empty($types)) {
+            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "type $typeinsql";
+            $sqlparams = array_merge($sqlparams, $typeparams);
+        }
+        if ($userid) {
+            // Get the data requests for the user or data requests made by the user.
+            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
+            $params = [
+                'userid' => $userid,
+                'requestedby' => $userid
+            ];
+
+            // Build a list of user IDs that the user is allowed to make data requests for.
+            // Of course, the user should be included in this list.
+            $alloweduserids = [$userid];
+            // Get any users that the user can make data requests for.
+            if ($children = helper::get_children_of_user($userid)) {
+                // Get the list of user IDs of the children and merge to the allowed user IDs.
+                $alloweduserids = array_merge($alloweduserids, array_keys($children));
+            }
+            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
+            $sqlconditions[] .= "userid $insql";
+            $select = implode(' AND ', $sqlconditions);
+            $params = array_merge($params, $inparams, $sqlparams);
+
+            $count = data_request::count_records_select($select, $params);
+        } else {
+            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
+            if (self::is_site_dpo($USER->id)) {
+                if (!empty($sqlconditions)) {
+                    $select = implode(' AND ', $sqlconditions);
+                    $count = data_request::count_records_select($select, $sqlparams);
+                } else {
+                    $count = data_request::count_records();
+                }
+            }
+        }
+
+        return $count;
+    }
+
     /**
      * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
      *
index 39e3e53..1435fcd 100644 (file)
@@ -289,7 +289,8 @@ class data_registry {
      * @param int $forcedcategoryvalue Use this value as if this was this context level category.
      * @return int[]
      */
-    public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false) {
+    public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
+                                                                                   $forcedcategoryvalue = false) {
 
         list($purposeid, $categoryid) = self::get_defaults($contextlevel);
 
index 3d20e5d..539fc28 100644 (file)
  */
 namespace tool_dataprivacy;
 
-use tool_dataprivacy\api;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\context_instance;
-use tool_dataprivacy\data_registry;
+use core_privacy\manager;
 use tool_dataprivacy\expired_context;
 
 defined('MOODLE_INTERNAL') || die();
@@ -90,7 +87,7 @@ abstract class expired_contexts_manager {
             return $numprocessed;
         }
 
-        $privacymanager = new \core_privacy\manager();
+        $privacymanager = new manager();
         $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
 
         foreach ($this->get_context_levels() as $level) {
@@ -118,11 +115,11 @@ abstract class expired_contexts_manager {
     /**
      * Deletes user data from the provided context.
      *
-     * @param \core_privacy\manager $privacymanager
-     * @param \tool_dataprivacy\expired_context $expiredctx
+     * @param manager $privacymanager
+     * @param expired_context $expiredctx
      * @return \context|false
      */
-    protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
+    protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
 
         $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
         if (!$context) {
index e4b40e8..924d565 100644 (file)
@@ -23,8 +23,7 @@
  */
 namespace tool_dataprivacy;
 
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\context_instance;
+use core_privacy\manager;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -110,11 +109,11 @@ class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
      *
      * Overwritten to delete the user.
      *
-     * @param \core_privacy\manager $privacymanager
-     * @param \tool_dataprivacy\expired_context $expiredctx
+     * @param manager $privacymanager
+     * @param expired_context $expiredctx
      * @return \context|false
      */
-    protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
+    protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
         $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
         if (!$context) {
             api::delete_expired_context($expiredctx->get('contextid'));
index 14790b5..bd1204f 100644 (file)
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
 
 use tool_dataprivacy\api;
 use tool_dataprivacy\data_registry;
+use tool_dataprivacy\purpose;
 
 /**
  * Context instance data form.
@@ -186,12 +187,12 @@ class context_instance extends \core\form\persistent {
     /**
      * Returns the purpose display text.
      *
-     * @param \tool_dataprivacy\purpose $effectivepurpose
+     * @param purpose $effectivepurpose
      * @param int $retentioncontextlevel
      * @param \context $context The context, just for displaying (filters) purposes.
      * @return string
      */
-    protected static function get_retention_display_text(\tool_dataprivacy\purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
+    protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
         global $PAGE;
 
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
index d7c3436..f98362d 100644 (file)
@@ -35,6 +35,17 @@ use tool_dataprivacy\api;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class helper {
+    /** The default number of results to be shown per page. */
+    const DEFAULT_PAGE_SIZE = 20;
+
+    /** Filter constant associated with the request type filter. */
+    const FILTER_TYPE = 1;
+
+    /** Filter constant associated with the request status filter. */
+    const FILTER_STATUS = 2;
+
+    /** The request filters preference key. */
+    const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
 
     /**
      * Retrieves the human-readable text value of a data request type.
@@ -45,16 +56,11 @@ class helper {
      * @throws moodle_exception
      */
     public static function get_request_type_string($requesttype) {
-        switch ($requesttype) {
-            case api::DATAREQUEST_TYPE_EXPORT:
-                return get_string('requesttypeexport', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_DELETE:
-                return get_string('requesttypedelete', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_OTHERS:
-                return get_string('requesttypeothers', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+        $types = self::get_request_types();
+        if (!isset($types[$requesttype])) {
+            throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
         }
+        return $types[$requesttype];
     }
 
     /**
@@ -66,16 +72,37 @@ class helper {
      * @throws moodle_exception
      */
     public static function get_shortened_request_type_string($requesttype) {
-        switch ($requesttype) {
-            case api::DATAREQUEST_TYPE_EXPORT:
-                return get_string('requesttypeexportshort', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_DELETE:
-                return get_string('requesttypedeleteshort', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_OTHERS:
-                return get_string('requesttypeothersshort', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+        $types = self::get_request_types_short();
+        if (!isset($types[$requesttype])) {
+            throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
         }
+        return $types[$requesttype];
+    }
+
+    /**
+     * Returns the key value-pairs of request type code and their string value.
+     *
+     * @return array
+     */
+    public static function get_request_types() {
+        return [
+            api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
+        ];
+    }
+
+    /**
+     * Returns the key value-pairs of request type code and their shortened string value.
+     *
+     * @return array
+     */
+    public static function get_request_types_short() {
+        return [
+            api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
+        ];
     }
 
     /**
@@ -83,30 +110,32 @@ class helper {
      *
      * @param int $status The request status.
      * @return string
-     * @throws coding_exception
      * @throws moodle_exception
      */
     public static function get_request_status_string($status) {
-        switch ($status) {
-            case api::DATAREQUEST_STATUS_PENDING:
-                return get_string('statuspending', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_PREPROCESSING:
-                return get_string('statuspreprocessing', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
-                return get_string('statusawaitingapproval', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_APPROVED:
-                return get_string('statusapproved', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_PROCESSING:
-                return get_string('statusprocessing', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_COMPLETE:
-                return get_string('statuscomplete', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_CANCELLED:
-                return get_string('statuscancelled', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_REJECTED:
-                return get_string('statusrejected', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
+        $statuses = self::get_request_statuses();
+        if (!isset($statuses[$status])) {
+            throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
         }
+        return $statuses[$status];
+    }
+
+    /**
+     * Returns the key value-pairs of request status code and string value.
+     *
+     * @return array
+     */
+    public static function get_request_statuses() {
+        return [
+            api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+        ];
     }
 
     /**
@@ -132,7 +161,7 @@ class helper {
             'contextlevel' => CONTEXT_USER
         ];
 
-        // The final list of users that we will return;
+        // The final list of users that we will return.
         $finalresults = [];
 
         // Our prospective list of users.
@@ -146,4 +175,34 @@ class helper {
         }
         return $finalresults;
     }
+
+    /**
+     * Get options for the data requests filter.
+     *
+     * @return array
+     * @throws coding_exception
+     */
+    public static function get_request_filter_options() {
+        $filters = [
+            self::FILTER_TYPE => (object)[
+                'name' => get_string('requesttype', 'tool_dataprivacy'),
+                'options' => self::get_request_types_short()
+            ],
+            self::FILTER_STATUS => (object)[
+                'name' => get_string('requeststatus', 'tool_dataprivacy'),
+                'options' => self::get_request_statuses()
+            ],
+        ];
+        $options = [];
+        foreach ($filters as $category => $filtercategory) {
+            foreach ($filtercategory->options as $key => $name) {
+                $option = (object)[
+                    'category' => $filtercategory->name,
+                    'name' => $name
+                ];
+                $options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
+            }
+        }
+        return $options;
+    }
 }
index c1a2202..444569e 100644 (file)
@@ -25,7 +25,6 @@ namespace tool_dataprivacy\output;
 defined('MOODLE_INTERNAL') || die();
 
 use coding_exception;
-use dml_exception;
 use moodle_exception;
 use moodle_url;
 use renderable;
@@ -34,7 +33,7 @@ use single_select;
 use stdClass;
 use templatable;
 use tool_dataprivacy\data_request;
-use tool_dataprivacy\output\expired_contexts_table;
+use tool_dataprivacy\local\helper;
 
 /**
  * Class containing data for a user's data requests.
@@ -44,9 +43,6 @@ use tool_dataprivacy\output\expired_contexts_table;
  */
 class data_deletion_page implements renderable, templatable {
 
-    /** The default number of results to be shown per page. */
-    const DEFAULT_PAGE_SIZE = 20;
-
     /** @var data_request[] $requests List of data requests. */
     protected $filter = null;
 
@@ -57,7 +53,7 @@ class data_deletion_page implements renderable, templatable {
      * Construct this renderable.
      *
      * @param \tool_dataprivacy\data_request[] $filter
-     * @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
+     * @param expired_contexts_table $expiredcontextstable
      */
     public function __construct($filter, expired_contexts_table $expiredcontextstable) {
         $this->filter = $filter;
@@ -70,7 +66,6 @@ class data_deletion_page implements renderable, templatable {
      * @param renderer_base $output
      * @return stdClass
      * @throws coding_exception
-     * @throws dml_exception
      * @throws moodle_exception
      */
     public function export_for_template(renderer_base $output) {
@@ -87,7 +82,7 @@ class data_deletion_page implements renderable, templatable {
         $data->filter = $filterselector->export_for_template($output);
 
         ob_start();
-        $this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
+        $this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
         $expiredcontexts = ob_get_contents();
         ob_end_clean();
         $data->expiredcontexts = $expiredcontexts;
index c1b2861..7ea4bf8 100644 (file)
 namespace tool_dataprivacy\output;
 defined('MOODLE_INTERNAL') || die();
 
-use action_menu;
-use action_menu_link_secondary;
 use coding_exception;
-use context_system;
 use dml_exception;
 use moodle_exception;
 use moodle_url;
 use renderable;
 use renderer_base;
+use single_select;
 use stdClass;
 use templatable;
 use tool_dataprivacy\api;
-use tool_dataprivacy\data_request;
-use tool_dataprivacy\external\data_request_exporter;
+use tool_dataprivacy\local\helper;
 
 /**
  * Class containing data for a user's data requests.
@@ -47,16 +44,21 @@ use tool_dataprivacy\external\data_request_exporter;
  */
 class data_requests_page implements renderable, templatable {
 
-    /** @var data_request[] $requests List of data requests. */
-    protected $requests = [];
+    /** @var data_requests_table $table The data requests table. */
+    protected $table;
+
+    /** @var int[] $filters The applied filters. */
+    protected $filters = [];
 
     /**
      * Construct this renderable.
      *
-     * @param data_request[] $requests
+     * @param data_requests_table $table The data requests table.
+     * @param int[] $filters The applied filters.
      */
-    public function __construct($requests) {
-        $this->requests = $requests;
+    public function __construct($table, $filters) {
+        $this->table = $table;
+        $this->filters = $filters;
     }
 
     /**
@@ -78,43 +80,17 @@ class data_requests_page implements renderable, templatable {
             $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
         }
 
-        $requests = [];
-        foreach ($this->requests as $request) {
-            $requestid = $request->get('id');
-            $status = $request->get('status');
-            $requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
-            $item = $requestexporter->export($output);
-
-            // Prepare actions.
-            $actions = [];
-
-            // View action.
-            $actionurl = new moodle_url('#');
-            $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
-            $actiontext = get_string('viewrequest', 'tool_dataprivacy');
-            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+        $filteroptions = helper::get_request_filter_options();
+        $filter = new request_filter($filteroptions, $this->filters, $url);
+        $data->filter = $filter->export_for_template($output);
 
-            if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
-                // Approve.
-                $actiondata['data-action'] = 'approve';
-                $actiontext = get_string('approverequest', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        ob_start();
+        $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+        $requests = ob_get_contents();
+        ob_end_clean();
 
-                // Deny.
-                $actiondata['data-action'] = 'deny';
-                $actiontext = get_string('denyrequest', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
-            }
-
-            $actionsmenu = new action_menu($actions);
-            $actionsmenu->set_menu_trigger(get_string('actions'));
-            $actionsmenu->set_owner_selector('request-actions-' . $requestid);
-            $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
-            $item->actions = $actionsmenu->export_for_template($output);
-
-            $requests[] = $item;
-        }
-        $data->requests = $requests;
+        $data->datarequests = $requests;
         return $data;
     }
 }
diff --git a/admin/tool/dataprivacy/classes/output/data_requests_table.php b/admin/tool/dataprivacy/classes/output/data_requests_table.php
new file mode 100644 (file)
index 0000000..97918d9
--- /dev/null
@@ -0,0 +1,262 @@
+<?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/>.
+
+/**
+ * Contains the class used for the displaying the data requests table.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta <jun@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+use action_menu;
+use action_menu_link_secondary;
+use coding_exception;
+use dml_exception;
+use html_writer;
+use moodle_url;
+use stdClass;
+use table_sql;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external\data_request_exporter;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * The class for displaying the data requests table.
+ *
+ * @copyright  2018 Jun Pataleta <jun@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_requests_table extends table_sql {
+
+    /** @var int The user ID. */
+    protected $userid = 0;
+
+    /** @var int[] The status filters. */
+    protected $statuses = [];
+
+    /** @var int[] The request type filters.  */
+    protected $types = [];
+
+    /** @var bool Whether this table is being rendered for managing data requests. */
+    protected $manage = false;
+
+    /** @var stdClass[] Array of data request persistents. */
+    protected $datarequests = [];
+
+    /**
+     * data_requests_table constructor.
+     *
+     * @param int $userid The user ID
+     * @param int[] $statuses
+     * @param int[] $types
+     * @param bool $manage
+     * @throws coding_exception
+     */
+    public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
+        parent::__construct('data-requests-table');
+
+        $this->userid = $userid;
+        $this->statuses = $statuses;
+        $this->types = $types;
+        $this->manage = $manage;
+
+        $columnheaders = [
+            'type' => get_string('requesttype', 'tool_dataprivacy'),
+            'userid' => get_string('user', 'tool_dataprivacy'),
+            'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
+            'requestedby' => get_string('requestby', 'tool_dataprivacy'),
+            'status' => get_string('requeststatus', 'tool_dataprivacy'),
+            'comments' => get_string('message', 'tool_dataprivacy'),
+            'actions' => '',
+        ];
+
+        $this->define_columns(array_keys($columnheaders));
+        $this->define_headers(array_values($columnheaders));
+        $this->no_sorting('actions');
+    }
+
+    /**
+     * The type column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_type($data) {
+        if ($this->manage) {
+            return $data->typenameshort;
+        }
+        return $data->typename;
+    }
+
+    /**
+     * The user column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_userid($data) {
+        $user = $data->foruser;
+        return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+    }
+
+    /**
+     * The context information column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_timecreated($data) {
+        return userdate($data->timecreated);
+    }
+
+    /**
+     * The requesting user's column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_requestedby($data) {
+        $user = $data->requestedbyuser;
+        return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+    }
+
+    /**
+     * The status column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_status($data) {
+        return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
+    }
+
+    /**
+     * The comments column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_comments($data) {
+        return shorten_text($data->comments, 60);
+    }
+
+    /**
+     * The actions column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_actions($data) {
+        global $OUTPUT;
+
+        $requestid = $data->id;
+        $status = $data->status;
+
+        // Prepare actions.
+        $actions = [];
+
+        // View action.
+        $actionurl = new moodle_url('#');
+        $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
+        $actiontext = get_string('viewrequest', 'tool_dataprivacy');
+        $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+        if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            // Approve.
+            $actiondata['data-action'] = 'approve';
+            $actiontext = get_string('approverequest', 'tool_dataprivacy');
+            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+            // Deny.
+            $actiondata['data-action'] = 'deny';
+            $actiontext = get_string('denyrequest', 'tool_dataprivacy');
+            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        }
+
+        $actionsmenu = new action_menu($actions);
+        $actionsmenu->set_menu_trigger(get_string('actions'));
+        $actionsmenu->set_owner_selector('request-actions-' . $requestid);
+        $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+
+        return $OUTPUT->render($actionsmenu);
+    }
+
+    /**
+     * Query the database for results to display in the table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     * @throws dml_exception
+     * @throws coding_exception
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        global $PAGE;
+
+        // Count data requests from the given conditions.
+        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+        $this->pagesize($pagesize, $total);
+
+        $sort = $this->get_sql_sort();
+
+        // Get data requests from the given conditions.
+        $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
+                $this->get_page_start(), $this->get_page_size());
+        $this->rawdata = [];
+        $context = \context_system::instance();
+        $renderer = $PAGE->get_renderer('tool_dataprivacy');
+        foreach ($datarequests as $persistent) {
+            $exporter = new data_request_exporter($persistent, ['context' => $context]);
+            $this->rawdata[] = $exporter->export($renderer);
+        }
+
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+
+    /**
+     * Override default implementation to display a more meaningful information to the user.
+     */
+    public function print_nothing_to_display() {
+        global $OUTPUT;
+        echo $this->render_reset_button();
+        $this->print_initials_bar();
+        if (!empty($this->statuses) || !empty($this->types)) {
+            $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
+        } else {
+            $message = get_string('nodatarequests', 'tool_dataprivacy');
+        }
+        echo $OUTPUT->notification($message, 'warning');
+    }
+
+    /**
+     * Override the table's show_hide_link method to prevent the show/hide links from rendering.
+     *
+     * @param string $column the column name, index into various names.
+     * @param int $index numerical index of the column.
+     * @return string HTML fragment.
+     */
+    protected function show_hide_link($column, $index) {
+        return '';
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/output/request_filter.php b/admin/tool/dataprivacy/classes/output/request_filter.php
new file mode 100644 (file)
index 0000000..ff3108d
--- /dev/null
@@ -0,0 +1,98 @@
+<?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 containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_filter implements renderable, templatable {
+
+    /** @var array $filteroptions The filter options. */
+    protected $filteroptions;
+
+    /** @var array $selectedoptions The list of selected filter option values. */
+    protected $selectedoptions;
+
+    /** @var moodle_url|string $baseurl The url with params needed to call up this page. */
+    protected $baseurl;
+
+    /**
+     * request_filter constructor.
+     *
+     * @param array $filteroptions The filter options.
+     * @param array $selectedoptions The list of selected filter option values.
+     * @param string|moodle_url $baseurl The url with params needed to call up this page.
+     */
+    public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
+        $this->filteroptions = $filteroptions;
+        $this->selectedoptions = $selectedoptions;
+        if (!empty($baseurl)) {
+            $this->baseurl = new moodle_url($baseurl);
+        }
+    }
+
+    /**
+     * Function to export the renderer data in a format that is suitable for a mustache template.
+     *
+     * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+     * @return stdClass|array
+     */
+    public function export_for_template(renderer_base $output) {
+        global $PAGE;
+        $data = new stdClass();
+        if (empty($this->baseurl)) {
+            $this->baseurl = $PAGE->url;
+        }
+        $data->action = $this->baseurl->out(false);
+
+        foreach ($this->selectedoptions as $option) {
+            if (!isset($this->filteroptions[$option])) {
+                $this->filteroptions[$option] = $option;
+            }
+        }
+
+        $data->filteroptions = [];
+        foreach ($this->filteroptions as $value => $label) {
+            $selected = in_array($value, $this->selectedoptions);
+            $filteroption = (object)[
+                'value' => $value,
+                'label' => $label
+            ];
+            $filteroption->selected = $selected;
+            $data->filteroptions[] = $filteroption;
+        }
+        return $data;
+    }
+}
index 59ca8c8..4c2d8c4 100644 (file)
@@ -51,7 +51,10 @@ class provider implements
         \core_privacy\local\metadata\provider,
 
         // This tool may provide access to and deletion of user data.
-        \core_privacy\local\request\plugin\provider {
+        \core_privacy\local\request\plugin\provider,
+
+        // This plugin has some sitewide user preferences to export.
+        \core_privacy\local\request\user_preference_provider {
     /**
      * Returns meta data about this system.
      *
@@ -70,6 +73,10 @@ class provider implements
             ],
             'privacy:metadata:request'
         );
+
+        $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
+            'privacy:metadata:preference:tool_dataprivacy_request-filters');
+
         return $collection;
     }
 
@@ -162,4 +169,36 @@ class provider implements
      */
     public static function delete_data_for_user(approved_contextlist $contextlist) {
     }
+
+    /**
+     * Export all user preferences for the plugin.
+     *
+     * @param   int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
+        if ($preffilter !== null) {
+            $filters = json_decode($preffilter);
+            $descriptions = [];
+            foreach ($filters as $filter) {
+                list($category, $value) = explode(':', $filter);
+                $option = new stdClass();
+                switch($category) {
+                    case tool_helper::FILTER_TYPE:
+                        $option->category = get_string('requesttype', 'tool_dataprivacy');
+                        $option->name = tool_helper::get_shortened_request_type_string($value);
+                        break;
+                    case tool_helper::FILTER_STATUS:
+                        $option->category = get_string('requeststatus', 'tool_dataprivacy');
+                        $option->name = tool_helper::get_request_status_string($value);
+                        break;
+                }
+                $descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
+            }
+            // Export the filter preference as comma-separated values and text descriptions.
+            $values = implode(', ', $filters);
+            $descriptionstext = implode(', ', $descriptions);
+            writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
+        }
+    }
 }
index c1631e4..ca4fb17 100644 (file)
@@ -64,7 +64,7 @@ class purpose extends \core\persistent {
                 // Replicate self::read.
                 $this->from_record($data);
 
-                // Using validate() as self::$validated is private.
+                // Validate the purpose record.
                 $this->validate();
 
                 // Now replicate the parent constructor.
index eaa5056..f622e5b 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT);
 
 $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php');
index 58da785..52eff0a 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
 
index d2d9123..0887134 100644 (file)
@@ -25,6 +25,8 @@
 require_once("../../../config.php");
 require_once('lib.php');
 
+require_login(null, false);
+
 $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
 
 $title = get_string('datarequests', 'tool_dataprivacy');
@@ -34,8 +36,38 @@ $title = get_string('datarequests', 'tool_dataprivacy');
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-$requests = tool_dataprivacy\api::get_data_requests();
-$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
+$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
+$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+if ($filtersapplied === [-1]) {
+    // If there are no filters submitted, check if there is a saved filters from the user preferences.
+    $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
+    if ($filterprefs && empty($filterscleared)) {
+        $filtersapplied = json_decode($filterprefs);
+    } else {
+        $filtersapplied = [];
+    }
+}
+// Save the current applied filters to the user preferences.
+set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
+
+$types = [];
+$statuses = [];
+foreach ($filtersapplied as $filter) {
+    list($category, $value) = explode(':', $filter);
+    switch($category) {
+        case \tool_dataprivacy\local\helper::FILTER_TYPE:
+            $types[] = $value;
+            break;
+        case \tool_dataprivacy\local\helper::FILTER_STATUS:
+            $statuses[] = $value;
+            break;
+    }
+}
+
+$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+$table->baseurl = $url;
+
+$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
 $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
 echo $requestlistoutput->render($requestlist);
 
index 0611908..d936ba2 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $url = new \moodle_url('/admin/tool/dataprivacy/defaults.php');
 $title = get_string('setdefaults', 'tool_dataprivacy');
 
index a2f59fb..0ee630f 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $id = optional_param('id', 0, PARAM_INT);
 
 $url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id));
index d7013db..20ee631 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $id = optional_param('id', 0, PARAM_INT);
 
 $url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id));
index b8622d8..aa64047 100644 (file)
@@ -48,8 +48,8 @@ $string['compliant'] = 'Compliant';
 $string['confirmapproval'] = 'Do you really want to approve this data request?';
 $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
 $string['confirmdenial'] = 'Do you really want deny this data request?';
-$string['contactdataprotectionofficer'] = 'Contact Data Protection Officer';
-$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the Data Protection Officer and make a data request via a link on their profile page.';
+$string['contactdataprotectionofficer'] = 'Contact the privacy officer';
+$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
 $string['contextlevelname10'] = 'Site';
 $string['contextlevelname30'] = 'Users';
 $string['contextlevelname40'] = 'Course categories';
@@ -57,7 +57,7 @@ $string['contextlevelname50'] = 'Courses';
 $string['contextlevelname70'] = 'Activity modules';
 $string['contextlevelname80'] = 'Blocks';
 $string['contextpurposecategorysaved'] = 'Purpose and category saved.';
-$string['contactdpoviaprivacypolicy'] = 'Please contact the Data Protection Officer as described in the privacy policy.';
+$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
 $string['createcategory'] = 'Create data category';
 $string['createpurpose'] = 'Create data purpose';
 $string['datadeletion'] = 'Data deletion';
@@ -82,8 +82,8 @@ $string['defaultssaved'] = 'Defaults saved';
 $string['deny'] = 'Deny';
 $string['denyrequest'] = 'Deny request';
 $string['download'] = 'Download';
-$string['dporolemapping'] = 'Data Protection Officer role mapping';
-$string['dporolemapping_desc'] = 'The Data Protection Officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a Data Protection Officer role mapping option.';
+$string['dporolemapping'] = 'Privacy officer role mapping';
+$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
 $string['editcategories'] = 'Edit categories';
 $string['editcategory'] = 'Edit category';
 $string['editcategories'] = 'Edit categories';
@@ -101,8 +101,10 @@ $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['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data';
+$string['exceptionnotificationbody'] = '<p>Exception occurred while calling <b>{$a->fullmethodname}</b>.<br>This means that plugin <b>{$a->component}</b> did not complete the processing of data. The following exception information may be passed on to the plugin developer:</p><pre>{$a->message}<br>
+
+{$a->backtrace}</pre>';
 $string['expiredretentionperiodtask'] = 'Expired retention period';
 $string['expiry'] = 'Expiry';
 $string['expandplugin'] = 'Expand and collapse plugin.';
@@ -110,6 +112,7 @@ $string['expandplugintype'] = 'Expand and collapse plugin type.';
 $string['explanationtitle'] = 'Icons used on this page and what they mean.';
 $string['external'] = 'Additional';
 $string['externalexplanation'] = 'An additional plugin installed on this site.';
+$string['filteroption'] = '{$a->category}: {$a->name}';
 $string['frontpagecourse'] = 'Front page course';
 $string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
 $string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
@@ -160,6 +163,7 @@ $string['nameemail'] = '{$a->name} ({$a->email})';
 $string['nchildren'] = '{$a} children';
 $string['newrequest'] = 'New request';
 $string['nodatarequests'] = 'There are no data requests';
+$string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
 $string['noactivitiestoload'] = 'No activities';
 $string['noassignedroles'] = 'No assigned roles in this context';
 $string['noblockstoload'] = 'No blocks';
@@ -174,11 +178,12 @@ $string['notset'] = 'Not set (use the default value)';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
+$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
 $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
+$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
@@ -199,16 +204,16 @@ $string['requestcomments_help'] = 'This box enables you to enter any further det
 $string['requestemailintro'] = 'You have received a data request:';
 $string['requestfor'] = 'Requesting for';
 $string['requeststatus'] = 'Status';
-$string['requestsubmitted'] = 'Your request has been submitted to the site\'s Data Protection Officer';
+$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
 $string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the site\'s Data Protection Officer';
+$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
 $string['requesttypedelete'] = 'Delete all of my personal data';
 $string['requesttypedeleteshort'] = 'Delete';
 $string['requesttypeexport'] = 'Export all of my personal data';
 $string['requesttypeexportshort'] = 'Export';
 $string['requesttypeothers'] = 'General inquiry';
-$string['requesttypeothersshort'] = 'Others';
+$string['requesttypeothersshort'] = 'Message';
 $string['requiresattention'] = 'Requires attention.';
 $string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
 $string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
index 060bd81..3b3f1c4 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
 
index 81d1517..5fad922 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $url = new moodle_url("/admin/tool/dataprivacy/purposes.php");
 $title = get_string('editpurposes', 'tool_dataprivacy');
 
index 79c9063..c62dc0b 100644 (file)
@@ -46,7 +46,7 @@
     }
 }}
 
-<div classs="container-fluid">
+<div class="container-fluid">
     <hr />
     <div class="row">
         {{#compliant}}
     </div>
 
     {{#compliant}}
-        <div class="hide" data-section="{{raw_component}}" aria-expanded="false">
+        <div class="hide" data-section="{{raw_component}}" aria-expanded="false" role="contentinfo">
             {{#metadata}}
                 <hr />
                 <div class="p-l-3">
                     <dl class="row">
                         <dt class="span3 col-xs-3">
                             {{#link}}
-                                <a href="#{{name}}"><h5 style="word-wrap:break-word">{{name}}</h5></a>
+                                <a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a>
                             {{/link}}
                             {{^link}}
-                                <h5 style="word-wrap:break-word">{{name}}</h5>
+                                <strong style="word-wrap:break-word">{{name}}</strong>
                             {{/link}}
                             <div class="small text-muted" style="word-wrap:break-word">{{type}}</div>
                         </dt>
index d87f231..cbf5ed3 100644 (file)
@@ -50,7 +50,7 @@
             "labelattributes": [],
             "helpicon": false
         },
-        "expiredcontexts": "<table class='table'><thead><tr><th class='header c0' scope='col'>Name<div class='commands'><a title='Hide Name' aria-expanded='true' aria-controls='expired-contexts-table_r0_c0 expired-contexts-table_r1_c0 expired-contexts-table_r2_c0 expired-contexts-table_r3_c0 expired-contexts-table_r4_c0 expired-contexts-table_r5_c0 expired-contexts-table_r6_c0 expired-contexts-table_r7_c0 expired-contexts-table_r8_c0 expired-contexts-table_r9_c0 expired-contexts-table_r10_c0 expired-contexts-table_r11_c0 expired-contexts-table_r12_c0 expired-contexts-table_r13_c0 expired-contexts-table_r14_c0 expired-contexts-table_r15_c0 expired-contexts-table_r16_c0 expired-contexts-table_r17_c0 expired-contexts-table_r18_c0 expired-contexts-table_r19_c0' href='#?thide=name'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c1' scope='col'>Information<div class='commands'><a title='Hide Information' aria-expanded='true' aria-controls='expired-contexts-table_r0_c1 expired-contexts-table_r1_c1 expired-contexts-table_r2_c1 expired-contexts-table_r3_c1 expired-contexts-table_r4_c1 expired-contexts-table_r5_c1 expired-contexts-table_r6_c1 expired-contexts-table_r7_c1 expired-contexts-table_r8_c1 expired-contexts-table_r9_c1 expired-contexts-table_r10_c1 expired-contexts-table_r11_c1 expired-contexts-table_r12_c1 expired-contexts-table_r13_c1 expired-contexts-table_r14_c1 expired-contexts-table_r15_c1 expired-contexts-table_r16_c1 expired-contexts-table_r17_c1 expired-contexts-table_r18_c1 expired-contexts-table_r19_c1'    href='#?thide=info'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c2' scope='col'>Purpose<div class='commands'><a title='Hide Purpose' aria-expanded='true' aria-controls='expired-contexts-table_r0_c2 expired-contexts-table_r1_c2 expired-contexts-table_r2_c2 expired-contexts-table_r3_c2 expired-contexts-table_r4_c2 expired-contexts-table_r5_c2 expired-contexts-table_r6_c2 expired-contexts-table_r7_c2 expired-contexts-table_r8_c2 expired-contexts-table_r9_c2 expired-contexts-table_r10_c2 expired-contexts-table_r11_c2 expired-contexts-table_r12_c2 expired-contexts-table_r13_c2 expired-contexts-table_r14_c2 expired-contexts-table_r15_c2 expired-contexts-table_r16_c2 expired-contexts-table_r17_c2 expired-contexts-table_r18_c2 expired-contexts-table_r19_c2'    href='#?thide=purpose'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c3' scope='col'>Category<div class='commands'><a title='Hide Category' aria-expanded='true' aria-controls='expired-contexts-table_r0_c3 expired-contexts-table_r1_c3 expired-contexts-table_r2_c3 expired-contexts-table_r3_c3 expired-contexts-table_r4_c3 expired-contexts-table_r5_c3 expired-contexts-table_r6_c3 expired-contexts-table_r7_c3 expired-contexts-table_r8_c3 expired-contexts-table_r9_c3 expired-contexts-table_r10_c3 expired-contexts-table_r11_c3 expired-contexts-table_r12_c3 expired-contexts-table_r13_c3 expired-contexts-table_r14_c3 expired-contexts-table_r15_c3 expired-contexts-table_r16_c3 expired-contexts-table_r17_c3 expired-contexts-table_r18_c3 expired-contexts-table_r19_c3'    href='#?thide=category'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c4' scope='col'>Retention period<div class='commands'><a title='Hide Retention period' aria-expanded='true' aria-controls='expired-contexts-table_r0_c4 expired-contexts-table_r1_c4 expired-contexts-table_r2_c4 expired-contexts-table_r3_c4 expired-contexts-table_r4_c4 expired-contexts-table_r5_c4 expired-contexts-table_r6_c4 expired-contexts-table_r7_c4 expired-contexts-table_r8_c4 expired-contexts-table_r9_c4 expired-contexts-table_r10_c4 expired-contexts-table_r11_c4 expired-contexts-table_r12_c4 expired-contexts-table_r13_c4 expired-contexts-table_r14_c4 expired-contexts-table_r15_c4 expired-contexts-table_r16_c4 expired-contexts-table_r17_c4 expired-contexts-table_r18_c4 expired-contexts-table_r19_c4'    href='#?thide=retentionperiod'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c5' scope='col'><a href='#?tsort=timecreated'>Expiry<span class='accesshide '>Sort by Expiry Ascending</span></a>  <i class='icon fa fa-sort-asc fa-fw ' aria-hidden='true' title='Ascending'aria-label='Ascending'></i><div class='commands'><a title='Hide Expiry' aria-expanded='true' aria-controls='expired-contexts-table_r0_c5 expired-contexts-table_r1_c5 expired-contexts-table_r2_c5 expired-contexts-table_r3_c5 expired-contexts-table_r4_c5 expired-contexts-table_r5_c5 expired-contexts-table_r6_c5 expired-contexts-table_r7_c5 expired-contexts-table_r8_c5 expired-contexts-table_r9_c5 expired-contexts-table_r10_c5 expired-contexts-table_r11_c5 expired-contexts-table_r12_c5 expired-contexts-table_r13_c5 expired-contexts-table_r14_c5 expired-contexts-table_r15_c5 expired-contexts-table_r16_c5 expired-contexts-table_r17_c5 expired-contexts-table_r18_c5 expired-contexts-table_r19_c5'    href='#?thide=timecreated'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c6' scope='col'><input title='Select all' type='checkbox' value='1' name='selectall' checked='checked'><div class='commands'></div>                    </th>                </tr>            </thead>            <tbody>                <tr class='' id='expired-contexts-table_r0'>                    <td class='cell c0' id='expired-contexts-table_r0_c0'><span class='m-r-1'>Miscellaneous / TC 1</span><i class='icon fa fa-info fa-fw ' aria-hidden='true' title='Miscellaneous / System' aria-label='Miscellaneous / System'></i></td>                    <td class='cell c1' id='expired-contexts-table_r0_c1'><span class='m-r-1'>7 children</span><i class='icon fa fa-info fa-fw ' aria-hidden='true' title='Test book, Glossary 1, Assignment 1, Page 1, Small files, Big file 0, Forum' aria-label='Test book, Glossary 1, Assignment 1, Page 1, Small files, Big file 0, Forum'></i></td>                    <td class='cell c2' id='expired-contexts-table_r0_c2'>Default purpose</td>                    <td class='cell c3' id='expired-contexts-table_r0_c3'>Default category</td>                    <td class='cell c4' id='expired-contexts-table_r0_c4'>1 days</td>                    <td class='cell c5' id='expired-contexts-table_r0_c5'>Thursday, 5 April 2018, 10:29 AM</td>                    <td class='cell c6' id='expired-contexts-table_r0_c6'><input type='checkbox' class='usercheckbox' name='expiredcontext_3' checked='true'></td></tr></tbody></table>"
+        "expiredcontexts": "<table class='table'><tbody><tr><td>This is the table that will contain the list of expired contexts</td></tr></tbody></table>"
     }
 }}
 <div class="container-fluid" data-region="data-deletion">
index 259c59c..caadac1 100644 (file)
@@ -55,7 +55,7 @@
             <h3 id="{{plugin_type_raw}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugintype, tool_dataprivacy{{/str}}{{/pix}}{{plugin_type}}</h3>
             </a>
         </div>
-        <div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false">
+        <div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo">
             {{#plugins}}
                 {{> tool_dataprivacy/component_status}}
             {{/plugins}}
index 8875b6c..1c993b2 100644 (file)
         "datarequestsurl": "#"
     }
 }}
-<style>
-    table, th, td {
-        border: 1px solid black;
-        padding: 0.5em;
-    }
-</style>
-<div>
-    <p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
-    <p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
-    <table>
-        <tr>
-            <th scope="row">
-                {{#str}}requesttype, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requesttype}}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">
-                {{#str}}requestfor, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestfor}}
-            </td>
-        </tr>
-        {{^forself}}
-        <tr>
-            <th scope="row">
-                {{#str}}requestby, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestedby}}
-            </td>
-        </tr>
-        {{/forself}}
-        <tr>
-            <th scope="row">
-                {{#str}}requestcomments, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{{requestcomments}}}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">
-                {{#str}}daterequested, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestdate}}
-            </td>
-        </tr>
-    </table>
-    <hr>
-    <a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
-</div>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <style>
+        table, th, td {
+            border: 1px solid black;
+            padding: 0.5em;
+        }
+    </style>
+    <title>{{#str}}datarequestemailsubject, tool_dataprivacy, {{requesttype}}{{/str}}</title>
+</head>
+<body>
+    <div>
+        <p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
+        <p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
+        <table>
+            <tr>
+                <th scope="row">
+                    {{#str}}requesttype, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requesttype}}
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    {{#str}}requestfor, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requestfor}}
+                </td>
+            </tr>
+            {{^forself}}
+                <tr>
+                    <th scope="row">
+                        {{#str}}requestby, tool_dataprivacy{{/str}}
+                    </th>
+                    <td>
+                        {{requestedby}}
+                    </td>
+                </tr>
+            {{/forself}}
+            <tr>
+                <th scope="row">
+                    {{#str}}requestcomments, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{{requestcomments}}}
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    {{#str}}daterequested, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requestdate}}
+                </td>
+            </tr>
+        </table>
+        <hr>
+        <a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
+    </div>
+</body>
+</html>
index b61bce1..cae5022 100644 (file)
@@ -30,6 +30,7 @@
 
     Example context (json):
     {
+        "title": "Data request modal title"
     }
 }}
 {{< core/modal }}
index 45cedc6..594aaab 100644 (file)
     * none
 
     Context variables required for this template:
-    * requests - Array of data requests.
+    * newdatarequesturl string The URL pointing to the data request creation page.
+    * datarequests string The HTML of the data requests table.
 
     Example context (json):
     {
-        "requests": [
-            {
-                "id": 1,
-                "foruser" : {
-                    "fullname": "Oscar Olsen",
-                    "profileurl": "#"
+        "newdatarequesturl": "#",
+        "datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>",
+        "filter": {
+            "action": "#",
+            "filteroptions": [
+                {
+                    "value": "1",
+                    "label": "Option 1"
                 },
-                "typenameshort" : "Export",
-                "comments": "I would like to download all of my daughter's personal data",
-                "statuslabelclass": "label-default",
-                "statuslabel": "Pending",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 2,
-                "foruser" : {
-                    "fullname": "Alexandre Denys",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Export",
-                "comments": "Please give me all of the information you have about me...",
-                "statuslabelclass": "label-warning",
-                "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 3,
-                "foruser" : {
-                    "fullname": "Hirondino Moura",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Delete",
-                "comments": "Please delete all of my son's personal data.",
-                "statuslabelclass": "label-success",
-                "statuslabel": "Complete",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 4,
-                "foruser" : {
-                    "fullname": "Florian Krause",
-                    "profileurl": "#"
+                {
+                    "value": "2",
+                    "label": "Option 2",
+                    "selected": true
                 },
-                "typenameshort" : "Delete",
-                "comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
-                "statuslabelclass": "label-danger",
-                "statuslabel": "Rejected",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
+                {
+                    "value": "3",
+                    "label": "Option 3",
+                    "selected": true
                 }
-            },
-            {
-                "id": 5,
-                "foruser" : {
-                    "fullname": "Nicklas Sørensen",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Export",
-                "comments": "Please let me download my data",
-                "statuslabelclass": "label-info",
-                "statuslabel": "Processing",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            }
-        ]
+            ]
+        }
     }
 }}
 
 
 <div data-region="datarequests">
     <div class="m-t-1 m-b-1">
-        <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
-            {{#str}}newrequest, tool_dataprivacy{{/str}}
-        </a>
+        <div class="pull-right">
+            <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
+                {{#str}}newrequest, tool_dataprivacy{{/str}}
+            </a>
+        </div>
+        {{#filter}}
+            {{>tool_dataprivacy/request_filter}}
+        {{/filter}}
+    </div>
+
+    <div class="m-t-1 m-b-1" data-region="data-requests-table">
+        {{{datarequests}}}
     </div>
-    <table class="generaltable fullwidth">
-        <thead>
-            <tr>
-                <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
-                <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
-            </tr>
-        </thead>
-        <tbody>
-            {{#requests}}
-            <tr {{!
-              }} data-region="request-node"{{!
-              }} data-id="{{id}}"{{!
-              }} data-type="{{type}}"{{!
-              }} data-status="{{status}}"{{!
-              }}>
-                <td>{{typenameshort}}</td>
-                <td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
-                <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
-                <td>
-                    <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
-                </td>
-                <td>{{#shortentext}}60, {{comments}}{{/shortentext}}</td>
-                <td>
-                    {{#actions}}
-                        {{> core/action_menu}}
-                    {{/actions}}
-                </td>
-            </tr>
-            {{/requests}}
-            {{^requests}}
-            <tr>
-                <td class="text-muted" colspan="6">
-                    {{#str}}nodatarequests, tool_dataprivacy{{/str}}
-                </td>
-            </tr>
-            {{/requests}}
-        </tbody>
-    </table>
 </div>
 
 {{#js}}
diff --git a/admin/tool/dataprivacy/templates/request_filter.mustache b/admin/tool/dataprivacy/templates/request_filter.mustache
new file mode 100644 (file)
index 0000000..ea4f15d
--- /dev/null
@@ -0,0 +1,67 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/request_filter
+
+    Template for the request filter element.
+
+    Context variables required for this template:
+    * action string - The action URL for the form.
+    * filteroptions - Array of filter options.
+      * value string - The option value.
+      * label string - The option label.
+      * selected boolean - Whether the option is selected
+
+    Example context (json):
+    {
+        "action": "#",
+        "filteroptions": [
+            {
+                "value": "1",
+                "label": "Option 1"
+            },
+            {
+                "value": "2",
+                "label": "Option 2",
+                "selected": true
+            },
+            {
+                "value": "3",
+                "label": "Option 3",
+                "selected": true
+            },
+            {
+                "value": "4",
+                "label": "Option 4"
+            }
+        ]
+    }
+}}
+<form method="post" action="{{action}}" class="m-b-1" role="search" id="request_filter_form">
+    <label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label>
+    <select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select">
+        {{#filteroptions}}
+            <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option>
+        {{/filteroptions}}
+    </select>
+    <input type="hidden" id="filters-cleared" name="filters-cleared" value="0" />
+</form>
+{{#js}}
+require(['tool_dataprivacy/request_filter'], function(Filter) {
+    Filter.init();
+});
+{{/js}}
index 6e06478..f4a7a66 100644 (file)
@@ -29,6 +29,7 @@ use tool_dataprivacy\api;
 use tool_dataprivacy\data_registry;
 use tool_dataprivacy\expired_context;
 use tool_dataprivacy\data_request;
+use tool_dataprivacy\local\helper;
 use tool_dataprivacy\task\initiate_data_request_task;
 use tool_dataprivacy\task\process_data_request_task;
 
@@ -411,42 +412,128 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
     }
 
     /**
-     * Test for api::get_data_requests()
+     * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
+     *
+     * @return array
      */
-    public function test_get_data_requests() {
+    public function get_data_requests_provider() {
         $generator = new testing_data_generator();
         $user1 = $generator->create_user();
         $user2 = $generator->create_user();
-        $comment = 'sample comment';
+        $user3 = $generator->create_user();
+        $user4 = $generator->create_user();
+        $user5 = $generator->create_user();
+        $users = [$user1, $user2, $user3, $user4, $user5];
+        $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
+        $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
 
-        // Make a data request as user 1.
-        $this->setUser($user1);
-        $d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-        // Make a data request as user 2.
-        $this->setUser($user2);
-        $d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-
-        // Fetching data requests of specific users.
-        $requests = api::get_data_requests($user1->id);
-        $this->assertCount(1, $requests);
-        $datarequest = reset($requests);
-        $this->assertEquals($d1->to_record(), $datarequest->to_record());
-
-        $requests = api::get_data_requests($user2->id);
-        $this->assertCount(1, $requests);
-        $datarequest = reset($requests);
-        $this->assertEquals($d2->to_record(), $datarequest->to_record());
-
-        // Fetching data requests of all users.
-        // As guest.
-        $this->setGuestUser();
-        $requests = api::get_data_requests();
-        $this->assertEmpty($requests);
-
-        // As DPO (admin in this case, which is default if no site DPOs are set).
-        $this->setAdminUser();
-        $requests = api::get_data_requests();
-        $this->assertCount(2, $requests);
+        return [
+            // Own data requests.
+            [$users, $user1, false, $completeonly],
+            // Non-DPO fetching all requets.
+            [$users, $user2, true, $completeonly],
+            // Admin fetching all completed and cancelled requests.
+            [$users, get_admin(), true, $completeandcancelled],
+            // Admin fetching all completed requests.
+            [$users, get_admin(), true, $completeonly],
+            // Guest fetching all requests.
+            [$users, guest_user(), true, $completeonly],
+        ];
+    }
+
+    /**
+     * Test for api::get_data_requests()
+     *
+     * @dataProvider get_data_requests_provider
+     * @param stdClass[] $users Array of users to create data requests for.
+     * @param stdClass $loggeduser The user logging in.
+     * @param boolean $fetchall Whether to fetch all records.
+     * @param int[] $statuses Status filters.
+     */
+    public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
+        $comment = 'Data %s request comment by user %d';
+        $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
+        $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
+        // Make a data requests for the users.
+        foreach ($users as $user) {
+            $this->setUser($user);
+            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
+            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
+        }
+
+        // Log in as the target user.
+        $this->setUser($loggeduser);
+        // Get records count based on the filters.
+        $userid = $loggeduser->id;
+        if ($fetchall) {
+            $userid = 0;
+        }
+        $count = api::get_data_requests_count($userid);
+        if (api::is_site_dpo($loggeduser->id)) {
+            // DPOs should see all the requests.
+            $this->assertEquals(count($users) * 2, $count);
+        } else {
+            if (empty($userid)) {
+                // There should be no data requests for this user available.
+                $this->assertEquals(0, $count);
+            } else {
+                // There should be only one (request with pending status).
+                $this->assertEquals(2, $count);
+            }
+        }
+        // Get data requests.
+        $requests = api::get_data_requests($userid);
+        // The number of requests should match the count.
+        $this->assertCount($count, $requests);
+
+        // Test filtering by status.
+        if ($count && !empty($statuses)) {
+            $filteredcount = api::get_data_requests_count($userid, $statuses);
+            // There should be none as they are all pending.
+            $this->assertEquals(0, $filteredcount);
+            $filteredrequests = api::get_data_requests($userid, $statuses);
+            $this->assertCount($filteredcount, $filteredrequests);
+
+            $statuscounts = [];
+            foreach ($statuses as $stat) {
+                $statuscounts[$stat] = 0;
+            }
+            $numstatus = count($statuses);
+            // Get all requests with status filter and update statuses, randomly.
+            foreach ($requests as $request) {
+                if (rand(0, 1)) {
+                    continue;
+                }
+
+                if ($numstatus > 1) {
+                    $index = rand(0, $numstatus - 1);
+                    $status = $statuses[$index];
+                } else {
+                    $status = reset($statuses);
+                }
+                $statuscounts[$status]++;
+                api::update_request_status($request->get('id'), $status);
+            }
+            $total = array_sum($statuscounts);
+            $filteredcount = api::get_data_requests_count($userid, $statuses);
+            $this->assertEquals($total, $filteredcount);
+            $filteredrequests = api::get_data_requests($userid, $statuses);
+            $this->assertCount($filteredcount, $filteredrequests);
+            // Confirm the filtered requests match the status filter(s).
+            foreach ($filteredrequests as $request) {
+                $this->assertContains($request->get('status'), $statuses);
+            }
+
+            if ($numstatus > 1) {
+                // Fetch by individual status to check the numbers match.
+                foreach ($statuses as $status) {
+                    $filteredcount = api::get_data_requests_count($userid, [$status]);
+                    $this->assertEquals($statuscounts[$status], $filteredcount);
+                    $filteredrequests = api::get_data_requests($userid, [$status]);
+                    $this->assertCount($filteredcount, $filteredrequests);
+                }
+            }
+        }
     }
 
     /**
index 1e434a7..d480e57 100644 (file)
@@ -142,7 +142,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         global $DB;
 
         $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
-        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1000Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
+        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
         $cat = api::create_category((object)['name' => 'a']);
 
         $record = (object)[
@@ -165,8 +165,12 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
 
         $course1 = $this->getDataGenerator()->create_course();
 
-        // Old course.
-        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']);
+        // Course finished last week (so purpose1 retention period does delete stuff but purpose2 retention period does not).
+        $dt = new \DateTime();
+        $di = new \DateInterval('P7D');
+        $dt->sub($di);
+
+        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => $dt->getTimestamp()]);
         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 
index 4aa17d1..8a057cf 100644 (file)
@@ -99,8 +99,8 @@ class helper {
             'name' => $name,
             'description' => $description,
             'timecreated' => transform::datetime($record->timecreated),
-            'ip' => $record->ip,
             'origin' => static::transform_origin($record->origin),
+            'ip' => $isauthor ? $record->ip : '',
             'other' => $other ? $other : []
         ];
 
index af91b8b..252ba50 100644 (file)
@@ -74,7 +74,9 @@ class provider implements
      * @param approved_contextlist $contextlist The approved contexts to export information for.
      */
     public static function export_user_data(approved_contextlist $contextlist) {
-        static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+        if (get_config('tool_log', 'exportlog')) {
+            static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+        }
     }
 
     /**
index bfda300..571b91f 100644 (file)
@@ -24,6 +24,8 @@
 
 $string['actlogshdr'] = 'Available log stores';
 $string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
+$string['exportlog'] = 'Include logs when exporting.';
+$string['exportlogdetail'] = 'Include logs that relate to the user when exporting.';
 $string['logging'] = 'Logging';
 $string['managelogging'] = 'Manage log stores';
 $string['pluginname'] = 'Log store manager';
index 3fee812..cbc3f31 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+
+    $privacysettings = $ADMIN->locate('privacysettings');
+
+    if ($ADMIN->fulltree) {
+        $privacysettings->add(new admin_setting_configcheckbox('tool_log/exportlog',
+                new lang_string('exportlog', 'tool_log'),
+                new lang_string('exportlogdetail', 'tool_log'), 1)
+        );
+    }
+
     $ADMIN->add('modules', new admin_category('logging', new lang_string('logging', 'tool_log')));
 
     $temp = new admin_settingpage('managelogging', new lang_string('managelogging', 'tool_log'));
index b5a7d90..1af8d1c 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2018051401; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2018050800; // Requires this Moodle version.
 $plugin->component = 'tool_log'; // Full name of the plugin (used for diagnostics).
index c451c1a..c556825 100644 (file)
@@ -54,7 +54,7 @@ $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
 $string['consentdetails'] = 'Give consent on behalf of user';
 $string['consentpagetitle'] = 'Consent';
-$string['contactdpo'] = 'For questions regarding the policies please contact the Data Protection Officer.';
+$string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
 $string['dataproc'] = 'Personal data processing';
 $string['deleting'] = 'Deleting a version';
 $string['deleteconfirm'] = '<p>Are you sure you want to delete policy <em>\'{$a->name}\'</em>?</p><p>This operation can not be undone.</p>';
index 3073212..1fbe958 100644 (file)
@@ -83,7 +83,7 @@ function tool_policy_before_standard_html_head() {
     if (!empty($CFG->sitepolicyhandler)
             && $CFG->sitepolicyhandler == 'tool_policy'
             && empty($USER->policyagreed)
-            && isguestuser()) {
+            && (isguestuser() || !isloggedin())) {
         $output = $PAGE->get_renderer('tool_policy');
         $page = new \tool_policy\output\guestconsent();
 
index fde43cc..6c504eb 100644 (file)
@@ -81,12 +81,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     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 "This site policy" in the "region-main" "region"
     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"
@@ -129,22 +129,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     And I press "Next"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     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 "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
@@ -495,20 +495,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I set the field "I agree to the This site policy" to "1"
     And I set the field "I agree to the This privacy policy" to "1"
@@ -528,22 +528,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I follow "Log in"
     When I press "Create new account"
     # Confirm that the user can view and accept policies when attempting to create another account.
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
index c23c25f..7656ae8 100644 (file)
@@ -693,7 +693,15 @@ class tool_uploadcourse_course {
             return false;
         }
 
-        // TODO MDL-59259 allow to set course format options for the current course format.
+        // Add data for course format options.
+        if (isset($coursedata['format']) || $exists) {
+            if (isset($coursedata['format'])) {
+                $courseformat = course_get_format((object)['format' => $coursedata['format']]);
+            } else {
+                $courseformat = course_get_format($existingdata);
+            }
+            $coursedata += $courseformat->validate_course_format_options($this->rawdata);
+        }
 
         // Special case, 'numsections' is not a course format option any more but still should apply from defaults.
         if (!$exists || !array_key_exists('numsections', $coursedata)) {
index f52ec31..4896658 100644 (file)
@@ -99,7 +99,8 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
         $co->proceed();
-        $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+        $course = $DB->get_record('course', array('shortname' => 'newcourse'), '*', MUST_EXIST);
+        $this->assertEquals(0, course_get_format($course)->get_course()->coursedisplay);
 
         // Try to add a new course, that already exists.
         $coursecount = $DB->count_records('course', array());
@@ -118,6 +119,16 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $co->proceed();
         $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+
+        // Add a new course with non-default course format option.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'c3', 'fullname' => 'C3', 'summary' => 'New c3', 'category' => 1,
+            'format' => 'weeks', 'coursedisplay' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c3'), '*', MUST_EXIST);
+        $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
     }
 
     public function test_create_with_sections() {
@@ -260,6 +271,16 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $co->proceed();
         $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Update course format option.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'coursedisplay' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c1'), '*', MUST_EXIST);
+        $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
     }
 
     public function test_data_saved() {
index 5c2e7b9..32f6fea 100644 (file)
@@ -124,7 +124,7 @@ class edit_table extends XMLDBAction {
         $o.= '    <input type="hidden" name ="action" value="edit_table_save" />';
         $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
         $o.= '    <input type="hidden" name ="postaction" value="edit_table" />';
-        $o.= '    <table id="formelements" class="boxaligncenter">';
+        $o .= '    <table id="formelements">';
         // If the table is being used, we cannot rename it
         if ($structure->getTableUses($table->getName())) {
             $o.= '      <tr valign="top"><td>Name:</td><td><input type="hidden" name ="name" value="' . s($table->getName()) . '" />' . s($table->getName()) .'</td></tr>';
@@ -243,7 +243,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $field->readableInfo() . '</td>';
                 // Print table row
-                $o .= '<tr class="r' . $row . '"><td class="table cell">' . $f . $b . $r . '</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $f . $b . $r . '</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -296,7 +296,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $key->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $k . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $k . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -337,7 +337,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $index->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $i . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $i . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
index 1fdd4cb..f053c08 100644 (file)
@@ -115,7 +115,7 @@ class edit_xml_file extends XMLDBAction {
                 $o.= '    <input type="hidden" name ="path" value="' . s($structure->getPath()) .'" />';
                 $o.= '    <input type="hidden" name ="version" value="' . s($structure->getVersion()) .'" />';
                 $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
-                $o.= '    <table id="formelements" class="boxaligncenter">';
+                $o .= '    <table id="formelements">';
                 $o.= '      <tr valign="top"><td>Path:</td><td>' . s($structure->getPath()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td>Version:</td><td>' . s($structure->getVersion()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td><textarea name="comment" rows="3" cols="80" id="comment">' . $structure->getComment() . '</textarea></td></tr>';
@@ -216,7 +216,7 @@ class edit_xml_file extends XMLDBAction {
                          }
                         $b .= '</td>';
                         // Print table row
-                        $o .= '<tr class="r' . $row . '"><td class="table cell">' . $t . $b . '</tr>';
+                        $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $t . $b . '</tr>';
                         $row = ($row + 1) % 2;
                     }
                     $o .= '</table>';
index e975201..d337572 100644 (file)
@@ -162,7 +162,7 @@ class view_table_php extends XMLDBAction {
         $o.= '    <input type="hidden" name ="dir" value="' . str_replace($CFG->dirroot, '', $dirpath) . '" />';
         $o.= '    <input type="hidden" name ="table" value="' . s($tableparam) . '" />';
         $o.= '    <input type="hidden" name ="action" value="view_table_php" />';
-        $o.= '    <table id="formelements" class="boxaligncenter" cellpadding="5">';
+        $o .= '    <table id="formelements" cellpadding="5">';
         $o.= '      <tr><td><label for="menucommand" accesskey="c">' . $this->str['selectaction'] .' </label>' . html_writer::select($popcommands, 'command', $commandparam, false) . '&nbsp;<label for="menufieldkeyindex" accesskey="f">' . $this->str['selectfieldkeyindex'] . ' </label>' .html_writer::select($popfields, 'fieldkeyindex', $origfieldkeyindexparam, false) . '</td></tr>';
         $o.= '      <tr><td colspan="2" align="center"><input type="submit" value="' .$this->str['view'] . '" /></td></tr>';
         $o.= '    </table>';
index 024fe2f..4c0ca9e 100644 (file)
@@ -1187,9 +1187,18 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding);
                     empty($nuvalue) ? $nuvalue = array() : $nuvalue;
                     $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
-
                     foreach ($ldapkeys as $ldapkey) {
-                        $ldapkey   = $ldapkey;
+                        // Skip update if $ldapkey does not exist in LDAP.
+                        if (!isset($user_entry[$ldapkey][0])) {
+                            $success = false;
+                            error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
+                                                                     array('ldapkey' => $ldapkey,
+                                                                            'key' => $key,
+                                                                            'ouvalue' => $ouvalue,
+                                                                            'nuvalue' => $nuvalue)));
+                            continue;
+                        }
+
                         $ldapvalue = $user_entry[$ldapkey][0];
                         if (!$ambiguous) {
                             // Skip update if the values already match
index 237423e..5040996 100644 (file)
@@ -149,6 +149,7 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 412d35b..6074f71 100644 (file)
@@ -38,7 +38,7 @@ $string['pluginname'] = 'MNet authentication';
 $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
 $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
 $string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
 $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
@@ -53,8 +53,8 @@ $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user
 $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
 $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
 $string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:mnet_external:id'] = 'The identifier for the user.';
-$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:mnet_external:id'] = 'The user ID';
+$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
 $string['privacy:metadata:mnet_external:institution'] = 'The institution that this user is a member of.';
 $string['privacy:metadata:mnet_external:lang'] = 'A user preference for the language shown.';
@@ -64,20 +64,20 @@ $string['privacy:metadata:mnet_external:lastname'] = 'The surname of the user.';
 $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
 $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user.';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
 $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
 $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
-$string['privacy:metadata:mnet_external:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
 $string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
 $string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
 $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
 $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
@@ -94,7 +94,7 @@ $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried
 $string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
 $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
 $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
-$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier.';
+$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
 $string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
index d91aaca..ed38e2d 100644 (file)
@@ -93,7 +93,7 @@ $string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connecte
 $string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.';
 $string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.';
 $string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.';
-$string['privacy:metadata:auth_oauth2:issuerid'] = 'The identifier of the OAuth 2 issuer for this OAuth 2 login.';
+$string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login';
 $string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.';
 $string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.';
 $string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.';
index a569a7a..defbc88 100644 (file)
@@ -27,8 +27,8 @@ $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:cmid'] = 'Course activity 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';
index 1e36407..50e6d81 100644 (file)
@@ -119,10 +119,9 @@ class create extends \moodleform {
         global $DB, $CFG;
 
         $errors = parent::validation($data, $files);
-        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
         $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
-
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
         if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
         }
index e0e7850..18e41e8 100644 (file)
@@ -154,3 +154,22 @@ Feature: Perform basic calendar functionality
     When I click on "Go to activity" "link"
     And I wait to be redirected
     Then I should see "Test choice"
+
+  @javascript
+  Scenario: Attempt to create event without fill required fields should display validation errors
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "New event" "button"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I am on site homepage
+    And I follow "Calendar"
+    And I click on "New event" "button"
+    And I set the field "Type of event" to "Course"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I should see "Select a course"
+    And I set the field "Event title" to "Really awesome event!"
+    When I click on "Save" "button"
+    Then I should see "Select a course"
index 4390d5a..eca4de1 100644 (file)
@@ -860,7 +860,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     protected function detail_pair($key, $value, $class ='') {
         $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
         $html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
-        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4 form-inline');
         $html .= html_writer::end_div();
         return $html;
     }
index 11f6866..ce28664 100644 (file)
@@ -85,6 +85,9 @@ class section extends \core_search\base {
      * @return \core_search\document
      */
     public function get_document($record, $options = array()) {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
+
         // Get the context, modinfo, and section.
         try {
             $context = \context_course::instance($record->course);
index 1f1cb78..07f2d9f 100644 (file)
@@ -1600,9 +1600,7 @@ class core_course_external extends external_api {
                             break;
 
                         case 'visible':
-                            if (has_capability('moodle/category:manage', $context)
-                                or has_capability('moodle/category:viewhiddencategories',
-                                        context_system::instance())) {
+                            if (has_capability('moodle/category:viewhiddencategories', $context)) {
                                 $value = clean_param($crit['value'], PARAM_INT);
                                 $conditions[$key] = $value;
                                 $wheres[] = $key . " = :" . $key;
@@ -1712,9 +1710,7 @@ class core_course_external extends external_api {
             if (!isset($excludedcats[$category->id])) {
 
                 // Final check to see if the category is visible to the user.
-                if ($category->visible
-                        or has_capability('moodle/category:viewhiddencategories', context_system::instance())
-                        or has_capability('moodle/category:manage', $context)) {
+                if ($category->visible or has_capability('moodle/category:viewhiddencategories', $context)) {
 
                     $categoryinfo = array();
                     $categoryinfo['id'] = $category->id;
index 045d049..f70371c 100644 (file)
@@ -735,6 +735,43 @@ abstract class format_base {
         return array();
     }
 
+    /**
+     * Prepares values of course or section format options before storing them in DB
+     *
+     * If an option has invalid value it is not returned
+     *
+     * @param array $rawdata associative array of the proposed course/section format options
+     * @param int|null $sectionid null if it is course format option
+     * @return array array of options that have valid values
+     */
+    protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
+        if (!$sectionid) {
+            $allformatoptions = $this->course_format_options(true);
+        } else {
+            $allformatoptions = $this->section_format_options(true);
+        }
+        $data = array_intersect_key($rawdata, $allformatoptions);
+        foreach ($data as $key => $value) {
+            $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
+            $data[$key] = clean_param($value, $option['type']);
+            if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
+                // Value invalid for select element, skip.
+                unset($data[$key]);
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Validates format options for the course
+     *
+     * @param array $data data to insert/update
+     * @return array array of options that have valid values
+     */
+    public function validate_course_format_options(array $data) : array {
+        return $this->validate_format_options($data);
+    }
+
     /**
      * Updates format options for a course or section
      *
@@ -747,6 +784,7 @@ abstract class format_base {
      */
     protected function update_format_options($data, $sectionid = null) {
         global $DB;
+        $data = $this->validate_format_options((array)$data, $sectionid);
         if (!$sectionid) {
             $allformatoptions = $this->course_format_options();
             $sectionid = 0;
@@ -772,7 +810,6 @@ abstract class format_base {
                       'sectionid' => $sectionid
                     ), '', 'name,id,value');
         $changed = $needrebuild = false;
-        $data = (array)$data;
         foreach ($defaultoptions as $key => $value) {
             if (isset($records[$key])) {
                 if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) {
index 394bf21..16a2c66 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.6 ===
+* New method validate_format_options() cleans the values of the course/section format options before inserting them
+  in the database. Course format options can now be set in tool_uploadcourse and validation of user-submitted data is important.
+  Note that validate_format_options() is now always called when somebody creates or edits course or section and also
+  during restore and course upload. Default implementation validates against the definition of the form elements for
+  format options.
+
 === 3.5 ===
 * Course formats should overwrite get_config_for_external function to return the course format settings viewable by the
   current user.
index 1accadd..23fe7f8 100644 (file)
@@ -77,15 +77,11 @@ class course_reset_form extends moodleform {
         $mform->addElement('header', 'groupheader', get_string('groups'));
 
         $mform->addElement('checkbox', 'reset_groups_remove', get_string('deleteallgroups', 'group'));
-        $mform->setAdvanced('reset_groups_remove');
         $mform->addElement('checkbox', 'reset_groups_members', get_string('removegroupsmembers', 'group'));
-        $mform->setAdvanced('reset_groups_members');
         $mform->disabledIf('reset_groups_members', 'reset_groups_remove', 'checked');
 
         $mform->addElement('checkbox', 'reset_groupings_remove', get_string('deleteallgroupings', 'group'));
-        $mform->setAdvanced('reset_groupings_remove');
         $mform->addElement('checkbox', 'reset_groupings_members', get_string('removegroupingsmembers', 'group'));
-        $mform->setAdvanced('reset_groupings_members');
         $mform->disabledIf('reset_groupings_members', 'reset_groupings_remove', 'checked');
 
         $unsupported_mods = array();
index fdd0380..19cb9b2 100644 (file)
@@ -201,6 +201,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // Set the required capabilities by the external function.
         $context = context_system::instance();
         $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
+        $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 
         // Retrieve category1 + sub-categories except not visible ones
         $categories = core_course_external::get_categories(array(
@@ -278,10 +279,10 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals($DB->count_records('course_categories'), count($categories));
 
-        $this->unassignUserCapability('moodle/category:manage', $context->id, $roleid);
+        $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 
-        // Ensure maxdepthcategory is 2 and retrieve all categories without category:manage capability. It should retrieve all
-        // visible categories as well.
+        // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
+        // It should retrieve all visible categories as well.
         set_config('maxcategorydepth', 2);
         $categories = core_course_external::get_categories();
 
index a8028a4..da840ff 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Javascript Object Notation (.json)';
-$string['privacy:metadata'] = 'The JavaScript Object Notation data format plugin does not store any personal data.';
-$string['shortname'] = 'JSON';
+$string['privacy:metadata'] = 'The JavaScript Object Notation (JSON) data format plugin does not store any personal data.';
+$string['shortname'] = 'JavaScript Object Notation (JSON)';
 
index 1d2773c..5eeaf6d 100644 (file)
@@ -24,5 +24,5 @@
 
 $string['dataformat'] = 'OpenDocument (.ods)';
 $string['privacy:metadata'] = 'The OpenDocument data format plugin does not store any personal data.';
-$string['shortname'] = 'OpenDoc';
+$string['shortname'] = 'OpenDocument';
 
index e815bf9..508fa9a 100644 (file)
@@ -64,9 +64,9 @@ It could look something like this:
 </pre>';
 $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:courseid'] = 'The course ID to which the enrolment relates';
+$string['privacy:metadata:enrol_flatfile:roleid'] = 'The ID of the role to be assigned or unassigned';
 $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.';
+$string['privacy:metadata:enrol_flatfile:userid'] = 'The ID of the user to which the role assignment relates';
index 99d121a..cf4f923 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="enrol/paypal/db" VERSION="20120122" COMMENT="XMLDB file for Moodle enrol/paypal"
+<XMLDB PATH="enrol/paypal/db" VERSION="20180625" COMMENT="XMLDB file for Moodle enrol/paypal"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="courseid" TYPE="foreign" FIELDS="courseid" REFTABLE="course" REFFIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="instanceid" TYPE="foreign" FIELDS="instanceid" REFTABLE="enrol" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="business" UNIQUE="false" FIELDS="business"/>
+        <INDEX NAME="receiver_email" UNIQUE="false" FIELDS="receiver_email"/>
+      </INDEXES>
     </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index 0cbbe98..c9ecd60 100644 (file)
@@ -75,5 +75,68 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018053000, 'enrol', 'paypal');
     }
 
+    if ($oldversion < 2018062500) {
+
+        // Define key courseid (foreign) to be added to enrol_paypal.
+        $table = new xmldb_table('enrol_paypal');
+        $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, array('courseid'), 'course', array('id'));
+
+        // Launch add key courseid.
+        $dbman->add_key($table, $key);
+
+        // Paypal savepoint reached.
+        upgrade_plugin_savepoint(true, 2018062500, 'enrol', 'paypal');
+    }
+
+    if ($oldversion < 2018062501) {
+
+        // Define key userid (foreign) to be added to enrol_paypal.
+        $table = new xmldb_table('enrol_paypal');
+        $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+
+        // Launch add key userid.
+        $dbman->add_key($table, $key);
+
+        // Paypal savepoint reached.
+        upgrade_plugin_savepoint(true, 2018062501, 'enrol', 'paypal');
+    }
+
+    if ($oldversion < 2018062502) {
+
+        // Define key instanceid (foreign) to be added to enrol_paypal.
+        $table = new xmldb_table('enrol_paypal');
+        $key = new xmldb_key('instanceid', XMLDB_KEY_FOREIGN, array('instanceid'), 'enrol', array('id'));
+
+        // Launch add key instanceid.
+        $dbman->add_key($table, $key);
+
+        // Paypal savepoint reached.
+        upgrade_plugin_savepoint(true, 2018062502, 'enrol', 'paypal');
+    }
+
+    if ($oldversion < 2018062503) {
+
+        $table = new xmldb_table('enrol_paypal');
+
+        // Define index business (not unique) to be added to enrol_paypal.
+        $index = new xmldb_index('business', XMLDB_INDEX_NOTUNIQUE, array('business'));
+
+        // Conditionally launch add index business.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Define index receiver_email (not unique) to be added to enrol_paypal.
+        $index = new xmldb_index('receiver_email', XMLDB_INDEX_NOTUNIQUE, array('receiver_email'));
+
+        // Conditionally launch add index receiver_email.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Paypal savepoint reached.
+        upgrade_plugin_savepoint(true, 2018062503, 'enrol', 'paypal');
+    }
+
     return true;
 }
index 6123a3c..6a92037 100644 (file)
@@ -72,7 +72,7 @@ $string['privacy:metadata:enrol_paypal:enrol_paypal:receiver_email'] = 'Primary
 $string['privacy:metadata:enrol_paypal:enrol_paypal:receiver_id'] = 'Unique PayPal account ID of the payment recipient (i.e., the merchant).';
 $string['privacy:metadata:enrol_paypal:enrol_paypal:tax'] = 'Amount of tax charged on payment.';
 $string['privacy:metadata:enrol_paypal:enrol_paypal:timeupdated'] = 'The time of Moodle being notified by PayPal about the payment.';
-$string['privacy:metadata:enrol_paypal:enrol_paypal:txn_id'] = 'The merchant\'s original transaction identification number for the payment from the buyer, against which the case was registered.';
+$string['privacy:metadata:enrol_paypal:enrol_paypal:txn_id'] = 'The merchant\'s original transaction identification number for the payment from the buyer, against which the case was registered';
 $string['privacy:metadata:enrol_paypal:enrol_paypal:userid'] = 'The ID of the user who bought the course enrolment.';
 $string['privacy:metadata:enrol_paypal:paypal_com'] = 'The PayPal enrolment plugin transmits user data from Moodle to the PayPal website.';
 $string['privacy:metadata:enrol_paypal:paypal_com:address'] = 'Address of the user who is buying the course.';
index 61566dd..6ca2f17 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018053000;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018062503;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;        // Requires this Moodle version
 $plugin->component = 'enrol_paypal';    // Full name of the plugin (used for diagnostics)
index d579bf6..5f5b3ff 100644 (file)
@@ -32,6 +32,22 @@ defined('MOODLE_INTERNAL') || die();
 
 class filter_emoticon extends moodle_text_filter {
 
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticontexts = array();
+
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticonimgs = array();
+
     /**
      * Apply the filter to the text
      *
@@ -49,7 +65,7 @@ class filter_emoticon extends moodle_text_filter {
             return $text;
         }
         if (in_array($options['originalformat'], explode(',', get_config('filter_emoticon', 'formats')))) {
-            $this->replace_emoticons($text);
+            return $this->replace_emoticons($text);
         }
         return $text;
     }
@@ -62,51 +78,73 @@ class filter_emoticon extends moodle_text_filter {
      * Replace emoticons found in the text with their images
      *
      * @param string $text to modify
-     * @return void
+     * @return string the modified result
      */
-    protected function replace_emoticons(&$text) {
+    protected function replace_emoticons($text) {
         global $CFG, $OUTPUT, $PAGE;
-        static $emoticontexts = array();    // internal cache used for replacing
-        static $emoticonimgs = array();     // internal cache used for replacing
 
         $lang = current_language();
         $theme = $PAGE->theme->name;
 
-        if (!isset($emoticontexts[$lang][$theme]) or !isset($emoticonimgs[$lang][$theme])) {
+        if (!isset(self::$emoticontexts[$lang][$theme]) or !isset(self::$emoticonimgs[$lang][$theme])) {
             // prepare internal caches
             $manager = get_emoticon_manager();
             $emoticons = $manager->get_emoticons();
-            $emoticontexts[$lang][$theme] = array();
-            $emoticonimgs[$lang][$theme] = array();
+            self::$emoticontexts[$lang][$theme] = array();
+            self::$emoticonimgs[$lang][$theme] = array();
             foreach ($emoticons as $emoticon) {
-                $emoticontexts[$lang][$theme][] = $emoticon->text;
-                $emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+                self::$emoticontexts[$lang][$theme][] = $emoticon->text;
+                self::$emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
             }
             unset($emoticons);
         }
 
-        if (empty($emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here
-            return;
+        if (empty(self::$emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here.
+            return $text;
         }
 
-        // detect all the <script> zones to take out
-        $excludes = array();
-        preg_match_all('/<script language(.+?)<\/script>/is', $text, $listofexcludes);
+        // Detect all zones that we should not handle (including the nested tags).
+        $processing = preg_split('/(<\/?(?:span|script)[^>]*>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
 
-        // take out all the <script> zones from text
-        foreach (array_unique($listofexcludes[0]) as $key => $value) {
-            $excludes['<+'.$key.'+>'] = $value;
-        }
-        if ($excludes) {
-            $text = str_replace($excludes, array_keys($excludes), $text);
-        }
+        // Initialize the results.
+        $resulthtml = "";
+        $exclude = 0;
 
-        // this is the meat of the code - this is run every time
-        $text = str_replace($emoticontexts[$lang][$theme], $emoticonimgs[$lang][$theme], $text);
+        // Define the patterns that mark the start of the forbidden zones.
+        $excludepattern = array('/^<script/is', '/^<span[^>]+class="nolink[^"]*"/is');
 
-        // Recover all the <script> zones to text
-        if ($excludes) {
-            $text = str_replace(array_keys($excludes), $excludes, $text);
+        // Loop through the fragments.
+        foreach ($processing as $fragment) {
+            // If we are not ignoring, we MUST test if we should.
+            if ($exclude == 0) {
+                foreach ($excludepattern as $exp) {
+                    if (preg_match($exp, $fragment)) {
+                        $exclude = $exclude + 1;
+                        break;
+                    }
+                }
+            }
+            if ($exclude > 0) {
+                // If we are ignoring the fragment, then we must check if we may have reached the end of the zone.
+                if (strpos($fragment, '</span') !== false || strpos($fragment, '</script') !== false) {
+                    $exclude -= 1;
+                    // This is needed because of a double increment at the first element.
+                    if ($exclude == 1) {
+                        $exclude -= 1;
+                    }
+                } else if (strpos($fragment, '<span') !== false || strpos($fragment, '<script') !== false) {
+                    // If we find a nested tag we increase the exclusion level.
+                    $exclude = $exclude + 1;
+                }
+            } else if (strpos($fragment, '<span') === false ||
+                       strpos($fragment, '</span') === false) {
+                // This is the meat of the code - this is run every time.
+                // This code only runs for fragments that are not ignored (including the tags themselves).
+                $fragment = str_replace(self::$emoticontexts[$lang][$theme], self::$emoticonimgs[$lang][$theme], $fragment);
+            }
+            $resulthtml .= $fragment;
         }
+
+        return $resulthtml;
     }
 }
index da3576a..10ee76e 100644 (file)
@@ -34,29 +34,124 @@ require_once($CFG->dirroot . '/filter/emoticon/filter.php'); // Include the code
 class filter_emoticon_testcase extends advanced_testcase {
 
     /**
-     * Verify configured target formats are observed. Just that.
+     * Tests the filter doesn't affect nolink classes.
+     *
+     * @dataProvider filter_emoticon_provider
      */
-    public function test_filter_emoticon_formats() {
-
-        $this->resetAfterTest(true); // We are modifying the config.
+    public function test_filter_emoticon($input, $format, $expected) {
+        $this->resetAfterTest();
 
         $filter = new testable_filter_emoticon();
+        $this->assertEquals($expected, $filter->filter($input, [
+                'originalformat' => $format,
+            ]));
+    }
+
+    /**
+     * The data provider for filter emoticon tests.
+     *
+     * @return  array
+     */
+    public function filter_emoticon_provider() {
+        $grr = '(grr)';
+        return [
+            'FORMAT_MOODLE is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MOODLE,
+                'expected' => $grr,
+            ],
+            'FORMAT_MARKDOWN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MARKDOWN,
+                'expected' => $grr,
+            ],
+            'FORMAT_PLAIN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_PLAIN,
+                'expected' => $grr,
+            ],
+            'FORMAT_HTML is filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_HTML,
+                'expected' => $this->get_converted_content_for_emoticon($grr),
+            ],
+            'Script tag should not be processed' => [
+                'input' => "<script language='javascript'>alert('{$grr}');</script>",
+                'format' => FORMAT_HTML,
+                'expected' => "<script language='javascript'>alert('{$grr}');</script>",
+            ],
+            'Basic nolink should not be processed' => [
+                'input' => '<span class="nolink">(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink">(n)</span>',
+            ],
+            'Nested nolink should not be processed' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>',
+            ],
+            'Nested nolink should not be processed but following emoticon' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>(n)',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>' . $this->get_converted_content_for_emoticon('(n)'),
+            ],
+        ];
+    }
+
+    /**
+     * Translate the text for a single emoticon into the rendered value.
+     *
+     * @param   string  $text The text to translate.
+     * @return  string
+     */
+    public function get_converted_content_for_emoticon($text) {
+        global $OUTPUT;
+        $manager = get_emoticon_manager();
+        $emoticons = $manager->get_emoticons();
+        foreach ($emoticons as $emoticon) {
+            if ($emoticon->text == $text) {
+                return $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+            }
+        }
+
+        return $text;
+    }
 
-        // Verify texts not matching target formats aren't filtered.
+    /**
+     * Tests the filter doesn't break anything if activated but invalid format passed.
+     *
+     */
+    public function test_filter_invalidformat() {
+        global $PAGE;
+        $this->resetAfterTest();
+
+        $filter = new testable_filter_emoticon();
+        $input = '(grr)';
         $expected = '(grr)';
-        $options = array('originalformat' => FORMAT_MOODLE); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
 
-        $options = array('originalformat' => FORMAT_MARKDOWN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => 'ILLEGALFORMAT',
+        ]));
+    }
+
+    /**
+     * Tests the filter doesn't break anything if activated but no emoticons available.
+     *
+     */
+    public function test_filter_emptyemoticons() {
+        global $CFG;
+        $this->resetAfterTest();
+        // Empty the emoticons array.
+        $CFG->emoticons = null;
 
-        $options = array('originalformat' => FORMAT_PLAIN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $filter = new filter_emoticon(context_system::instance(), array('originalformat' => FORMAT_HTML));
+
+        $input = '(grr)';
+        $expected = '(grr)';
 
-        // And texts matching target formats are filtered.
-        $expected = '<img class="icon emoticon" alt="angry" title="angry" src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/angry" />';
-        $options = array('originalformat' => FORMAT_HTML); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => FORMAT_HTML,
+        ]));
     }
 }
 
@@ -65,6 +160,9 @@ class filter_emoticon_testcase extends advanced_testcase {
  */
 class testable_filter_emoticon extends filter_emoticon {
     public function __construct() {
+        // Reset static emoticon caches.
+        parent::$emoticontexts = array();
+        parent::$emoticonimgs = array();
         // Use this context for filtering.
         $this->context = context_system::instance();
         // Define FORMAT_HTML as only one filtering in DB.
index a6010bf..50e568e 100644 (file)
@@ -79,10 +79,8 @@ class filter_mathjaxloader extends moodle_text_filter {
      * @param context $context The current context.
      */
     public function setup($page, $context) {
-        // This only requires execution once per request.
-        static $jsinitialised = false;
 
-        if (empty($jsinitialised)) {
+        if ($page->requires->should_create_one_time_item_now('filter_mathjaxloader-scripts')) {
             $url = get_config('filter_mathjaxloader', 'httpsurl');
             $lang = $this->map_language_code(current_language());
             $url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
@@ -102,8 +100,6 @@ class filter_mathjaxloader extends moodle_text_filter {
             $params = array('mathjaxconfig' => $config, 'lang' => $lang);
 
             $page->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.configure', array($params));
-
-            $jsinitialised = true;
         }
     }
 
index 0103646..cf656d3 100644 (file)
@@ -529,6 +529,7 @@ $string['environmentrequireversion'] = 'version {$a->needed} is required and you
 $string['environmentsettingok'] = 'recommended setting detected';
 $string['environmentshouldfixsetting'] = 'PHP setting should be changed.';
 $string['environmentxmlerror'] = 'Error reading environment data ({$a->error_code})';
+$string['environmentmariadbwrongdbtype'] = 'Wrong <code>$CFG->dbtype</code>: you need to change it in your <code>config.php</code> file, from \'<code>mysql</code>\' to \'<code>mariadb</code>\'.';
 $string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
 $string['errorsetting'] = 'Could not save setting:';
 $string['errorwithsettings'] = 'Some settings were not changed due to an error.';
@@ -978,6 +979,16 @@ $string['requires'] = 'Requires';
 $string['purgecaches'] = 'Purge all caches';
 $string['purgecachesconfirm'] = 'Moodle can cache themes, javascript, language strings, filtered text, rss feeds and many other pieces of calculated data.  Purging these caches will delete that data from the server and force browsers to refetch data, so that you can be sure you are seeing the most up-to-date values produced by the current code.  There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
 $string['purgecachesfinished'] = 'All caches were purged.';
+$string['purgecachesnoneselected'] = 'Select one or more caches to purge';
+$string['purgecachespage'] = 'Purge caches';
+$string['purgefiltercache'] = 'Text filters';
+$string['purgejscache'] = 'JavaScript';
+$string['purgelangcache'] = 'Language strings';
+$string['purgemuc'] = 'All MUC caches';
+$string['purgeothercaches'] = 'All file and miscellaneous caches';
+$string['purgeselectedcaches'] = 'Purge selected caches';
+$string['purgeselectedcachesfinished'] = 'The selected caches were purged.';
+$string['purgethemecache'] = 'Themes';
 $string['requestcategoryselection'] = 'Enable category selection';
 $string['restorecourse'] = 'Restore course';
 $string['restorernewroleid'] = 'Restorers\' role in courses';
index efd2552..9d4715e 100644 (file)
@@ -88,14 +88,14 @@ $string['privacy:metadata:analytics:indicatorcalc:starttime'] = 'Calculation sta
 $string['privacy:metadata:analytics:indicatorcalc:endtime'] = 'Calculation end time';
 $string['privacy:metadata:analytics:indicatorcalc:contextid'] = 'The context';
 $string['privacy:metadata:analytics:indicatorcalc:sampleorigin'] = 'The origin table of the sample';
-$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample ID';
 $string['privacy:metadata:analytics:indicatorcalc:indicator'] = 'The indicator calculator class';
 $string['privacy:metadata:analytics:indicatorcalc:value'] = 'The calculated value';
 $string['privacy:metadata:analytics:indicatorcalc:timecreated'] = 'When the prediction was made';
 $string['privacy:metadata:analytics:predictions'] = 'Predictions';
-$string['privacy:metadata:analytics:predictions:modelid'] = 'The model id';
+$string['privacy:metadata:analytics:predictions:modelid'] = 'The model ID';
 $string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
-$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample ID';
 $string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time splitting method';
 $string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
 $string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
@@ -104,7 +104,7 @@ $string['privacy:metadata:analytics:predictions:timecreated'] = 'When the predic
 $string['privacy:metadata:analytics:predictions:timestart'] = 'Calculations time start';
 $string['privacy:metadata:analytics:predictions:timeend'] = 'Calculations time end';
 $string['privacy:metadata:analytics:predictionactions'] = 'Prediction actions';
-$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction id';
+$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction ID';
 $string['privacy:metadata:analytics:predictionactions:userid'] = 'The user that made the action';
 $string['privacy:metadata:analytics:predictionactions:actionname'] = 'The action name';
 $string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the prediction action was performed';
index 0ca7302..2be7f58 100644 (file)
@@ -144,7 +144,7 @@ $string['privacy:metadata:post:userid'] = 'The ID of the user who added the blog
 $string['privacy:metadata:post:subject'] = 'Blog entry title.';
 $string['privacy:metadata:post:summary'] = 'Blog entry.';
 $string['privacy:metadata:post:content'] = 'The content of an external blog entry.';
-$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL.';
+$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL';
 $string['privacy:metadata:post:publishstate'] = 'Whether the entry is visible to others or not';
 $string['privacy:metadata:post:created'] = 'Date when the entry was created.';
 $string['privacy:metadata:post:lastmodified'] = 'Date when the entry was last modified.';
index cfda2a1..5f0ea26 100644 (file)
@@ -62,7 +62,7 @@ $string['invalidtheme'] = 'Cohort theme does not exist';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
-$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes column names.';
+$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes the correct column names. Note that Upload cohorts only allows you to add new users to an existing cohort and does not allow removal from an existing cohort.';
 $string['namefieldempty'] = 'Field name can not be empty';
 $string['newnamefor'] = 'New name for cohort {$a}';
 $string['newidnumberfor'] = 'New ID number for cohort {$a}';
index a3f66c3..606cac2 100644 (file)
@@ -139,7 +139,7 @@ $string['privacy:metadata:evidence:actionuserid'] = 'The user performing the act
 $string['privacy:metadata:evidence:desca'] = 'The optional parameters of the translatable evidence description';
 $string['privacy:metadata:evidence:desccomponent'] = 'The component of the translatable evidence description';
 $string['privacy:metadata:evidence:descidentifier'] = 'An identifier of the translatable evidence description';
-$string['privacy:metadata:evidence:grade'] = 'The grade associted with the evidence';
+$string['privacy:metadata:evidence:grade'] = 'The grade associated with the evidence';
 $string['privacy:metadata:evidence:note'] = 'A non-localised note attached to the evidence';
 $string['privacy:metadata:evidence:url'] = 'A URL associated with the evidence';
 $string['privacy:metadata:plan:description'] = 'The description of the learning plan';
index 56efe38..dc1a66f 100644 (file)
@@ -182,7 +182,7 @@ $string['overallaggregation_any'] = 'Course is complete when ANY of the conditio
 $string['pending'] = 'Pending';
 $string['periodpostenrolment'] = 'Period post enrolment';
 $string['privacy:metadata:completionstate'] = 'If the activity has been completed';
-$string['privacy:metadata:course'] = 'A course identifier.';
+$string['privacy:metadata:course'] = 'A course identifier';
 $string['privacy:metadata:coursecompletedsummary'] = 'Stores information about users who have completed criteria in a course';
 $string['privacy:metadata:coursemoduleid'] = 'The activity ID';
 $string['privacy:metadata:coursemodulesummary'] = 'Stores activity completion data for a user';
index fcd85b0..4a9937d 100644 (file)
@@ -111,6 +111,8 @@ $string['nosearch'] = 'Don\'t publish hub or courses';
 $string['notregisteredonhub'] = 'Your administrator needs to register this site with Moodle.net before you can share a course.';
 $string['operation'] = 'Actions';
 $string['participantnumberaverage'] = 'Average number of participants ({$a})';
+$string['policyagreed'] = 'Privacy notice and data processing agreement';
+$string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Privacy notice and data processing agreement</a> for Moodle.net';
 $string['postaladdress'] = 'Postal address';
 $string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
 $string['postsnumber'] = 'Number of posts ({$a})';
index efd628b..88883be 100644 (file)
@@ -1020,9 +1020,9 @@ $string['idnumbergroup_help'] = 'The ID number of a group is only used when matc
 $string['idnumbergrouping'] = 'Grouping ID number';
 $string['idnumbergrouping_help'] = 'The ID number of a grouping is only used when matching the grouping against external systems and is not displayed anywhere on the site. If the grouping has an official code name it may be entered, otherwise the field can be left blank.';
 $string['idnumbermod'] = 'ID number';
-$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity for grade calculation purposes. If the activity is not included in any grade calculation then the ID number field may be left blank.
+$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity or resource for purposes such as grade calculation or custom reporting. Otherwise the field may be left blank.
 
-The ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
+For gradable activities, the ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
 $string['idnumbertaken'] = 'This ID number is already taken';
 $string['imagealt'] = 'Picture description';
 $string['import'] = 'Import';
@@ -1565,7 +1565,7 @@ $string['privacy:metadata:events_queue'] = 'The queue of user events waiting to
 $string['privacy:metadata:events_queue:eventdata'] = 'The data stored in the event.';
 $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:events_queue:userid'] = 'The user ID 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';
index 5186147..3010079 100644 (file)
@@ -60,11 +60,11 @@ $string['personal'] = 'personal';
 $string['personalnotes'] = 'Personal notes';
 $string['privacy:metadata:core_notes'] = 'The Notes component stores user notes within the core subsystem.';
 $string['privacy:metadata:core_notes:content'] = 'The content of the note.';
-$string['privacy:metadata:core_notes:courseid'] = 'The Id of the course associated with the note.';
+$string['privacy:metadata:core_notes:courseid'] = 'The ID of the course associated with the note';
 $string['privacy:metadata:core_notes:created'] = 'The creation date/time for the note.';
 $string['privacy:metadata:core_notes:lastmodified'] = 'The last modified date/time for the note.';
 $string['privacy:metadata:core_notes:publishstate'] = 'The publish state of the note.';
-$string['privacy:metadata:core_notes:userid'] = 'The Id of the user associated with the note.';
+$string['privacy:metadata:core_notes:userid'] = 'The ID of the user associated with the note';
 $string['publishstate'] = 'Context';
 $string['publishstate_help'] = 'A note\'s context determines who can see the note in everyday use. Users should be aware that all notes, including personal ones, may be disclosed under the laws of their jurisdictions.
 
index a5fae08..1255ad8 100644 (file)
@@ -473,17 +473,17 @@ $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
 $string['privacy:metadata:preference:showadvanced'] = 'Handle the toggle advanced mode button.';
 $string['privacy:metadata:role_assignments'] = 'Role assignments';
 $string['privacy:metadata:role_assignments:component'] = 'Plugin responsible for role assignment, empty when manually assigned.';
-$string['privacy:metadata:role_assignments:itemid'] = 'The Id of enrolment/auth instance responsible for this role assignment.';
-$string['privacy:metadata:role_assignments:modifierid'] = 'The Id of the user who created or modified the role assignment.';
-$string['privacy:metadata:role_assignments:roleid'] = 'The Id of the role.';
+$string['privacy:metadata:role_assignments:itemid'] = 'The ID of enrolment/auth instance responsible for this role assignment';
+$string['privacy:metadata:role_assignments:modifierid'] = 'The ID of the user who created or modified the role assignment';
+$string['privacy:metadata:role_assignments:roleid'] = 'The ID of the role';
 $string['privacy:metadata:role_assignments:tableexplanation'] = 'This table stores the assigned roles in each context.';
 $string['privacy:metadata:role_assignments:timemodified'] = 'The date when the role assignment was created or modified.';
-$string['privacy:metadata:role_assignments:userid'] = 'The Id of the user.';
+$string['privacy:metadata:role_assignments:userid'] = 'The ID of the user';
 $string['privacy:metadata:role_capabilities'] = 'Role capabilities';
 $string['privacy:metadata:role_capabilities:capability'] = 'The name of the capability.';
-$string['privacy:metadata:role_capabilities:modifierid'] = 'The Id of the user who created or modified the capability.';
+$string['privacy:metadata:role_capabilities:modifierid'] = 'The ID of the user who created or modified the capability';
 $string['privacy:metadata:role_capabilities:permission'] = 'The permission for a capability: inherit, allow, prevent or prohibit.';
-$string['privacy:metadata:role_capabilities:roleid'] = 'The Id of the role.';
-$string['privacy:metadata:role_capabilities:tableexplanation'] = 'This table stores the capabilities and the override capabilities for a particular role in a particular context.';
+$string['privacy:metadata:role_capabilities:roleid'] = 'The ID of the role';
+$string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabilities and override capabilities for a particular role in a particular context';
 $string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
 $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
index db7c5d6..e956098 100644 (file)
@@ -68,13 +68,15 @@ $string['globalsearch'] = 'Global search';
 $string['globalsearchdisabled'] = 'Global searching is not enabled.';
 $string['gradualreindex'] = 'Gradual reindex {$a}';
 $string['gradualreindex_confirm'] = 'Are you sure you want to reindex {$a}? This may take some time, although existing data will remain available during the reindex.';
-$string['gradualreindex_queued'] = 'Reindexing has been requested for {$a->name} ({$a->count} contexts). This indexing will be carried out by the &lsquo;Global search indexing&rsquo; scheduled task.';
+$string['gradualreindex_queued'] = 'Reindexing has been requested for {$a->name} ({$a->count} contexts). This indexing will be carried out by the "Global search indexing" scheduled task.';
 $string['checkdb'] = 'Check database';
 $string['checkdbadvice'] = 'Check your database for any problems.';
 $string['checkdir'] = 'Check dir';
 $string['checkdiradvice'] = 'Ensure the data directory exists and is writable.';
 $string['incourse'] = 'in course {$a}';
 $string['index'] = 'Index';
+$string['indexwhendisabledfullnotice'] = 'Indexing is currently not permitted when search is disabled. To enable this, please see the <a href="{$a->url}">searchindexwhendisabled</a> setting.';
+$string['indexwhendisabledshortnotice'] = 'Indexing is not available.';
 $string['invalidindexerror'] = 'Index directory either contains an invalid index, or nothing at all.';
 $string['ittook'] = 'It took';
 $string['matchingfile'] = 'Matched from file <span class="filename">{$a}</span>';
index 8b0fd76..e72e036 100644 (file)
@@ -28,17 +28,17 @@ $string['privacy:devicespath'] = 'User devices';
 $string['privacy:draftfilespath'] = 'Draft files';
 $string['privacy:lastaccesspath'] = 'Last access to courses';
 $string['privacy:metadata:address'] = 'The address of the user.';
-$string['privacy:metadata:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:alternatename'] = 'An alternative name for the user.';
-$string['privacy:metadata:appid'] = 'The app id, usually something like com.moodle.moodlemobile';
+$string['privacy:metadata:appid'] = 'The app ID, usually something like com.moodle.moodlemobile';
 $string['privacy:metadata:auth'] = 'The authentication plugin used for this user record.';
 $string['privacy:metadata:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:calendartype'] = 'A user preference for the type of calendar to use.';
-$string['privacy:metadata:category'] = 'The category identifier.';
+$string['privacy:metadata:category'] = 'The category identifier';
 $string['privacy:metadata:city'] = 'The city of the user.';
 $string['privacy:metadata:confirmed'] = 'If this is an active user or not.';
 $string['privacy:metadata:country'] = 'The country that the user is in.';
-$string['privacy:metadata:courseid'] = 'An identifier for a course.';
+$string['privacy:metadata:courseid'] = 'Course ID';
 $string['privacy:metadata:currentlogin'] = 'The current login for this user.';
 $string['privacy:metadata:data'] = 'Data relating to the custom user field from the user.';
 $string['privacy:metadata:deleted'] = 'A flag to show if the user has been deleted or not.';
@@ -57,8 +57,8 @@ $string['privacy:metadata:firstnamephonetic'] = 'The phonetic details about the
 $string['privacy:metadata:fullname'] = 'The fullname for this course.';
 $string['privacy:metadata:hash'] = 'A hash of a previous password.';
 $string['privacy:metadata:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:id'] = 'The identifier for the user.';
-$string['privacy:metadata:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:id'] = 'The user ID';
+$string['privacy:metadata:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:imagealt'] = 'Alternative text for the user\'s image.';
 $string['privacy:metadata:infotablesummary'] = 'Stores custom user information.';
 $string['privacy:metadata:institution'] = 'The institution that this user is a member of.';
@@ -71,10 +71,10 @@ $string['privacy:metadata:lastname'] = 'The surname of the user.';
 $string['privacy:metadata:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
 $string['privacy:metadata:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$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: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: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)';
@@ -86,14 +86,14 @@ $string['privacy:metadata:phone'] = 'A phone number for the user.';
 $string['privacy:metadata:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:platform'] = 'The device platform, Android or iOS etc';
 $string['privacy:metadata:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:pushid'] = 'The device PUSH token/key/identifier/registration id';
+$string['privacy:metadata:pushid'] = 'The device PUSH token/key/identifier/registration ID';
 $string['privacy:metadata:reason'] = 'The reason for requesting this course.';
-$string['privacy:metadata:requester'] = 'An identifier to a user that requested this course.';
+$string['privacy:metadata:requester'] = 'The ID of the user who requested the course';
 $string['privacy:metadata:requestsummary'] = 'Stores information about requests for courses that users make.';
 $string['privacy:metadata:suspended'] = 'A flag to show if the user has been suspended on this system.';
 $string['privacy:metadata:user_preferences'] = 'Preferences associated with the given user';
 $string['privacy:metadata:user_preferences:name'] = 'Preference name';
-$string['privacy:metadata:user_preferences:userid'] = 'User id';
+$string['privacy:metadata:user_preferences:userid'] = 'The user ID';
 $string['privacy:metadata:user_preferences:value'] = 'Preference value';
 $string['privacy:metadata:username'] = 'The username for this user.';
 $string['privacy:metadata:secret'] = 'Secret.. not sure.';
@@ -101,7 +101,7 @@ $string['privacy:metadata:sessdata'] = 'Session content';
 $string['privacy:metadata:sessiontablesummary'] = 'Database based session storage';
 $string['privacy:metadata:shortname'] = 'A short name for the course.';
 $string['privacy:metadata:sid'] = 'The session ID';
-$string['privacy:metadata:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:state'] = '0 means a normal session';
 $string['privacy:metadata:summary'] = 'A description of the course.';
 $string['privacy:metadata:theme'] = 'A user preference for the theme to display.';
@@ -110,11 +110,11 @@ $string['privacy:metadata:timecreated'] = 'The time this record was created.';
 $string['privacy:metadata:timemodified'] = 'The time this records was modified.';
 $string['privacy:metadata:timererequested'] = 'The time the user re-requested the password reset.';
 $string['privacy:metadata:timerequested'] = 'The time that the user first requested this password reset';
-$string['privacy:metadata:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:token'] = 'secret set and emailed to user';
 $string['privacy:metadata:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:trustbitmask'] = 'The trust bit mask';
-$string['privacy:metadata:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:url'] = 'A URL related to this user.';
 $string['privacy:metadata:userid'] = 'The user ID linked to this table.';
 $string['privacy:metadata:usertablesummary'] = 'This table stores the main personal data about the user.';
index 69614d2..65330d6 100644 (file)
@@ -38,7 +38,7 @@ $string['privacy:metadata:user_private_key:value'] = 'The value of the key.';
 $string['privacy:metadata:user_private_key:userid'] = 'The user associated with the key.';
 $string['privacy:metadata:user_private_key:instance'] = 'The instance of the script.';
 $string['privacy:metadata:user_private_key:iprestriction'] = 'The IP address range that this key can be used from.';
-$string['privacy:metadata:user_private_key:validuntil'] = 'The date and time that the private key is valid until.';
+$string['privacy:metadata:user_private_key:validuntil'] = 'The date that the private key is valid until';
 $string['privacy:metadata:user_private_key:timecreated'] = 'The date and time that the key was created.';
 $string['privacy:metadata:user_private_key'] = 'Private keys for the user.';
 $string['userkey'] = 'User key';
index 9c1f8d8..b6cf1de 100644 (file)
@@ -150,12 +150,12 @@ $string['privacy:metadata:tokens'] = 'A record of tokens for interacting with Mo
 $string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
 $string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
 $string['privacy:metadata:tokens:lastaccess'] = 'The date at which the token was last used';
-$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO.';
+$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO';
 $string['privacy:metadata:tokens:timecreated'] = 'The date at which the token was created';
 $string['privacy:metadata:tokens:token'] = 'The user\'s token';
 $string['privacy:metadata:tokens:tokentype'] = 'The type of token';
 $string['privacy:metadata:tokens:userid'] = 'The ID of the user whose token it is';
-$string['privacy:metadata:tokens:validuntil'] = 'The date at which the token becomes invalid';
+$string['privacy:metadata:tokens:validuntil'] = 'The date that the token is valid until';
 $string['privacy:request:notexportedsecurity'] = 'Not exported for security reasons';
 $string['protocol'] = 'Protocol';
 $string['removefunction'] = 'Remove';
index fc93ae5..536504c 100644 (file)
@@ -286,7 +286,11 @@ function get_role_definitions(array $roleids) {
     // Grab all keys we have not yet got in our static cache.
     if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
         $cache = cache::make('core', 'roledefs');
-        $ACCESSLIB_PRIVATE->cacheroledefs += array_filter($cache->get_many($uncached));
+        foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
+            if (is_array($cachedroledef)) {
+                $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
+            }
+        }
 
         // Check we have the remaining keys from the MUC.
         if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
@@ -313,20 +317,25 @@ function get_role_definitions_uncached(array $roleids) {
         return array();
     }
 
-    list($sql, $params) = $DB->get_in_or_equal($roleids);
+    // Create a blank results array: even if a role has no capabilities,
+    // we need to ensure it is included in the results to show we have
+    // loaded all the capabilities that there are.
     $rdefs = array();
+    foreach ($roleids as $roleid) {
+        $rdefs[$roleid] = array();
+    }
 
+    // Load all the capabilities for these roles in all contexts.
+    list($sql, $params) = $DB->get_in_or_equal($roleids);
     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
               FROM {role_capabilities} rc
               JOIN {context} ctx ON rc.contextid = ctx.id
              WHERE rc.roleid $sql";
     $rs = $DB->get_recordset_sql($sql, $params);
 
+    // Store the capabilities into the expected data structure.
     foreach ($rs as $rd) {
         if (!isset($rdefs[$rd->roleid][$rd->path])) {
-            if (!isset($rdefs[$rd->roleid])) {
-                $rdefs[$rd->roleid] = array();
-            }
             $rdefs[$rd->roleid][$rd->path] = array();
         }
         $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
index 187ab85..dc09ffb 100644 (file)
@@ -43,7 +43,8 @@ class registration {
     /** @var Fields used in a site registration form.
      * IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */
     const FORM_FIELDS = ['name', 'description', 'contactname', 'contactemail', 'contactphone', 'imageurl', 'privacy', 'street',
-        'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail', 'language'];
+        'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
+        'language', 'policyagreed'];
 
     /** @var List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added.
      * If site was already registered, admin will be promted to confirm new registration data manually. Until registration is manually confirmed,
index ee7368d..510a2da 100644 (file)
@@ -64,7 +64,8 @@ class site_registration_form extends \moodleform {
             'language' => explode('_', current_language())[0],
             'geolocation' => '',
             'emailalert' => 1,
-            'commnews' => 1
+            'commnews' => 1,
+            'policyagreed' => 0
 
         ]);
 
@@ -151,6 +152,10 @@ class site_registration_form extends \moodleform {
         $mform->addElement('hidden', 'imageurl', ''); // TODO: temporary.
         $mform->setType('imageurl', PARAM_URL);
 
+        $mform->addElement('checkbox', 'policyagreed', get_string('policyagreed', 'hub'),
+            get_string('policyagreeddesc', 'hub', HUB_MOODLEORGHUBURL . '/privacy'));
+        $mform->addRule('policyagreed', $strrequired, 'required', null, 'client');
+
         $mform->addElement('header', 'sitestats', get_string('sendfollowinginfo', 'hub'));
         $mform->setExpanded('sitestats', !empty($highlightfields));
         $mform->addElement('static', 'urlstring', get_string('siteurl', 'hub'), $siteinfo['url']);
@@ -182,7 +187,9 @@ class site_registration_form extends \moodleform {
         if (empty($siteinfo['commnewsnewemail'])) {
             $siteinfo['commnewsemail'] = '';
         }
-        $this->set_data($siteinfo);
+
+        // Set data. Always require to check policyagreed even if it was checked earlier.
+        $this->set_data(['policyagreed' => 0] + $siteinfo);
     }
 
     /**
@@ -262,7 +269,8 @@ class site_registration_form extends \moodleform {
 
             if (debugging('', DEBUG_DEVELOPER)) {
                 // Display debugging message for developers who added fields to the form and forgot to add them to registration::FORM_FIELDS.
-                $keys = array_diff(array_keys((array)$data), ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
+                $keys = array_diff(array_keys((array)$data),
+                    ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
                 if ($extrafields = array_diff($keys, registration::FORM_FIELDS)) {
                     debugging('Found extra fields in the form results: ' . join(', ', $extrafields), DEBUG_DEVELOPER);
                 }
index b9b9003..8e4dd62 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20180403" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20180618" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="fk_user" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="ix_concomitem" UNIQUE="false" FIELDS="contextid, commentarea, itemid" COMMENT="Allows the comments API to load comments for a particular area effectively."/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="external_services" COMMENT="built in and custom external services">
       <FIELDS>
index cc7edbd..7ddc876 100644 (file)
@@ -2233,5 +2233,26 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018062800.01) {
+        // Add foreign key fk_user to the comments table.
+        $table = new xmldb_table('comments');
+        $key = new xmldb_key('fk_user', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+        $dbman->add_key($table, $key);
+
+        upgrade_main_savepoint(true, 2018062800.01);
+    }
+
+    if ($oldversion < 2018062800.02) {
+        // Add composite index ix_concomitem to the table comments.
+        $table = new xmldb_table('comments');
+        $index = new xmldb_index('ix_concomitem', XMLDB_INDEX_NOTUNIQUE, array('contextid', 'commentarea', 'itemid'));
+
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        upgrade_main_savepoint(true, 2018062800.02);
+    }
+
     return true;
 }
index 270d79c..51ea48d 100644 (file)
@@ -1048,6 +1048,19 @@ function environment_check_database($version, $env_select) {
         return $result;
     }
 
+    // Check if the DB Vendor has been properly configured.
+    // Hack: this is required when playing with MySQL and MariaDB since they share the same PHP module and base DB classes,
+    // whilst they are slowly evolving using separate directions though MariaDB is still an "almost" drop-in replacement.
+    $dbvendorismysql = ($current_vendor === 'mysql');
+    $dbtypeismariadb = (stripos($dbinfo['description'], 'mariadb') !== false);
+    if ($dbvendorismysql && $dbtypeismariadb) {
+        $result->setStatus(false);
+        $result->setLevel($level);
+        $result->setInfo($current_vendor . ' (' . $dbinfo['description'] . ')');
+        $result->setFeedbackStr('environmentmariadbwrongdbtype');
+        return $result;
+    }
+
 /// And finally compare them, saving results
     if (version_compare($current_version, $needed_version, '>=')) {
         $result->setStatus(true);
index af8a77d..d8875b5 100644 (file)
@@ -89,7 +89,8 @@ LICENSE
 /**
  * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
  * skodak
- *
+ * This class was modified to allow comparison operators (<, <=, ==, >=, >)
+ * and synonyms functions (for the 'if' function). See MDL-14274 for more details.
  */
 
 class EvalMath {
@@ -113,7 +114,8 @@ class EvalMath {
         'average'=>array(-1), 'max'=>array(-1),  'min'=>array(-1),
         'mod'=>array(2),      'pi'=>array(0),    'power'=>array(2),
         'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
-        'rand_float'=>array(0));
+        'rand_float'=>array(0), 'ifthenelse'=>array(3));
+    var $fcsynonyms = array('if' => 'ifthenelse');
 
     var $allowimplicitmultiplication;
 
@@ -207,20 +209,25 @@ class EvalMath {
         $stack = new EvalMathStack;
         $output = array(); // postfix form of expression, to be passed to pfx()
         $expr = trim(strtolower($expr));
-
-        $ops   = array('+', '-', '*', '/', '^', '_');
+        // MDL-14274: new operators for comparison added.
+        $ops   = array('+', '-', '*', '/', '^', '_', '>', '<', '<=', '>=', '==');
         $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
-        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
+        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>3, '<'=>3, '<='=>3, '>='=>3, '=='=>3); // operator precedence
 
         $expecting_op = false; // we use this in syntax-checking the expression
                                // and determining when a - is a negation
 
-        if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
+        if (preg_match("/[^\w\s+*^\/()\.,-<>=]/", $expr, $matches)) { // make sure the characters are all good
             return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
         }
 
         while(1) { // 1 Infinite Loop ;)
-            $op = substr($expr, $index, 1); // get the first character at the current index
+            // MDL-14274 Test two character operators.
+            $op = substr($expr, $index, 2);
+            if (!in_array($op, $ops)) {
+                // MDL-14274 Get one character operator.
+                $op = substr($expr, $index, 1); // get the first character at the current index
+            }
             // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
             $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
             //===============
@@ -245,7 +252,7 @@ class EvalMath {
                 }
                 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
                 $stack->push($op); // finally put OUR operator onto the stack
-                $index++;
+                $index += strlen($op);
                 $expecting_op = false;
             //===============
             } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
@@ -265,7 +272,9 @@ class EvalMath {
                             $a->given = $arg_count;
                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
                         }
-                    } elseif (array_key_exists($fnn, $this->fc)) {
+                    } elseif ($this->get_native_function_name($fnn)) {
+                        $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
+
                         $counts = $this->fc[$fnn];
                         if (in_array(-1, $counts) and $arg_count > 0) {}
                         elseif (!in_array($arg_count, $counts)) {
@@ -309,7 +318,9 @@ class EvalMath {
                 $expecting_op = true;
                 $val = $match[1];
                 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
-                    if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func
+                    if (in_array($matches[1], $this->fb) or
+                                array_key_exists($matches[1], $this->f) or
+                                $this->get_native_function_name($matches[1])){ // it's a func
                         $stack->push($val);
                         $stack->push(1);
                         $stack->push('(');
@@ -331,6 +342,7 @@ class EvalMath {
                     $stack->pop();// 1
                     $fn = $stack->pop();
                     $fnn = $matches[1]; // get the function name
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     $counts = $this->fc[$fnn];
                     if (!in_array(0, $counts)){
                         $a= new stdClass();
@@ -368,7 +380,20 @@ class EvalMath {
         }
         return $output;
     }
-
+    /**
+     *
+     * @param string $fnn
+     * @return string|boolean false if function name unknown.
+     */
+    function get_native_function_name($fnn) {
+        if (array_key_exists($fnn, $this->fcsynonyms)) {
+            return $this->fcsynonyms[$fnn];
+        } else if (array_key_exists($fnn, $this->fc)) {
+            return $fnn;
+        } else {
+            return false;
+        }
+    }
     // evaluate postfix notation
     function pfx($tokens, $vars = array()) {
 
@@ -387,7 +412,8 @@ class EvalMath {
                     $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
                     if ($fnn == 'ln') $fnn = 'log';
                     eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
-                } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function
+                } elseif ($this->get_native_function_name($fnn)) { // calc emulation function
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     // get args
                     $args = array();
                     for ($i = $count-1; $i >= 0; $i--) {
@@ -407,7 +433,7 @@ class EvalMath {
                     $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
                 }
             // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
-            } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) {
+            } elseif (in_array($token, array('+', '-', '*', '/', '^', '>', '<', '==', '<=', '>='), true)) {
                 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 switch ($token) {
@@ -422,6 +448,16 @@ class EvalMath {
                         $stack->push($op1/$op2); break;
                     case '^':
                         $stack->push(pow($op1, $op2)); break;
+                    case '>':
+                        $stack->push((int)($op1 > $op2)); break;
+                    case '<':
+                        $stack->push((int)($op1 < $op2)); break;
+                    case '==':
+                        $stack->push((int)($op1 == $op2)); break;
+                    case '<=':
+                        $stack->push((int)($op1 <= $op2)); break;
+                    case '>=':
+                        $stack->push((int)($op1 >= $op2)); break;
                 }
             // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
             } elseif ($token == "_") {
@@ -483,7 +519,21 @@ class EvalMathStack {
 
 // spreadsheet functions emulation
 class EvalMathFuncs {
-
+    /**
+     * MDL-14274 new conditional function.
+     * @param boolean $condition boolean for conditional.
+     * @param variant $then value if condition is true.
+     * @param unknown $else value if condition is false.
+     * @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
+     * @return unknown
+     */
+    static function ifthenelse($condition, $then, $else) {
+        if ($condition == true) {
+            return $then;
+        } else {
+            return $else;
+        }
+    }
     static function average() {
         $args = func_get_args();
         return (call_user_func_array(array('self', 'sum'), $args) / count($args));
index e9b1a80..c9935c5 100644 (file)
@@ -18,3 +18,7 @@ To see all changes diff against version 1.1, available from:
 http://www.phpclasses.org/browse/package/2695.html
 
 skodak, Tim Hunt
+
+Changes by Juan Pablo de Castro (MDL-14274):
+* operators >,<,>=,<=,== added.
+* function if[thenelse](condition, true_value, false_value)
index ff38252..ef612b4 100644 (file)
@@ -116,7 +116,7 @@ class MoodleExcelWorkbook {
             header('Pragma: no-cache');
         }
 
-        if (core_useragent::is_ie()) {
+        if (core_useragent::is_ie() || core_useragent::is_edge()) {
             $filename = rawurlencode($filename);
         } else {
             $filename = s($filename);
index 53f07db..a8c844a 100644 (file)
@@ -2115,7 +2115,7 @@ function send_temp_file($path, $filename, $pathisstring=false) {
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = urlencode($filename);
     }
 
@@ -2264,7 +2264,7 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = rawurlencode($filename);
     }
 
index a06e1af..bbb9289 100644 (file)
@@ -302,15 +302,36 @@ class filter_manager {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class null_filter_manager {
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @param string $text The text to filter
+     * @param context $context not used.
+     * @param array $options not used
+     * @param array $skipfilters not used
+     * @return string resulting text.
+     */
     public function filter_text($text, $context, array $options = array(),
             array $skipfilters = null) {
         return $text;
     }
 
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @param string $string The text to filter
+     * @param context $context not used.
+     * @return string resulting string
+     */
     public function filter_string($string, $context) {
         return $string;
     }
 
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @deprecated Since Moodle 3.0 MDL-50491.
+     */
     public function text_filtering_hash() {
         throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more');
     }
@@ -429,9 +450,9 @@ abstract class moodle_text_filter {
     /**
      * Override this function to actually implement the filtering.
      *
-     * @param $text some HTML content.
+     * @param string $text some HTML content to process.
      * @param array $options options passed to the filters
-     * @return the HTML content after the filtering has been applied.
+     * @return string the HTML content after the filtering has been applied.
      */
     public abstract function filter($text, array $options = array());
 }
@@ -520,8 +541,6 @@ function filter_get_name($filter) {
  * sorted in alphabetical order of name.
  */
 function filter_get_all_installed() {
-    global $CFG;
-
     $filternames = array();
     foreach (core_component::get_plugin_list('filter') as $filter => $fulldir) {
         if (is_readable("$fulldir/filter.php")) {
@@ -692,16 +711,21 @@ function filter_is_enabled($filtername) {
  * @return array where the keys and values are both the filter name, like 'tex'.
  */
 function filter_get_globally_enabled() {
-    static $enabledfilters = null;
-    if (is_null($enabledfilters)) {
-        $filters = filter_get_global_states();
-        $enabledfilters = array();
-        foreach ($filters as $filter => $filerinfo) {
-            if ($filerinfo->active != TEXTFILTER_DISABLED) {
-                $enabledfilters[$filter] = $filter;
-            }
+    $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_filter', 'global_filters');
+    $enabledfilters = $cache->get('enabled');
+    if ($enabledfilters !== false) {
+        return $enabledfilters;
+    }
+
+    $filters = filter_get_global_states();
+    $enabledfilters = array();
+    foreach ($filters as $filter => $filerinfo) {
+        if ($filerinfo->active != TEXTFILTER_DISABLED) {
+            $enabledfilters[$filter] = $filter;
         }
     }
+
+    $cache->set('enabled', $enabledfilters);
     return $enabledfilters;
 }
 
@@ -1428,9 +1452,10 @@ function filter_phrases($text, &$link_array, $ignoretagsopen=NULL, $ignoretagscl
 }
 
 /**
- * @todo Document this function
- * @param array $linkarray
- * @return array
+ * Remove duplicate from a list of {@link filterobject}.
+ *
+ * @param filterobject[] $linkarray a list of filterobject.
+ * @return filterobject[] the same list, but with dupicates removed.
  */
 function filter_remove_duplicates($linkarray) {
 
index aa2e866..8753695 100644 (file)
@@ -56,10 +56,7 @@ class calc_formula {
             return;
         }
         $formula = substr($formula, 1);
-        if (strpos($formula, '=') !== false) {
-            $this->_error = "too many '='";
-            return;
-        }
+
         $this->_nfx = $this->_em->nfx($formula);
         if ($this->_nfx == false) {
             $this->_error = $this->_em->last_error;
index 34fe56b..47cf0b1 100644 (file)
@@ -1627,18 +1627,61 @@ function get_users_from_config($value, $capability, $includeadmins = true) {
 /**
  * Invalidates browser caches and cached data in temp.
  *
- * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
- * {@link phpunit_util::reset_dataroot()}
- *
  * @return void
  */
 function purge_all_caches() {
-    global $CFG, $DB;
+    purge_caches();
+}
 
-    reset_text_filters_cache();
-    js_reset_all_caches();
-    theme_reset_all_caches();
-    get_string_manager()->reset_caches();
+/**
+ * Selectively invalidate different types of cache.
+ *
+ * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
+ * areas alone or in combination.
+ *
+ * @param bool[] $options Specific parts of the cache to purge. Valid options are:
+ *        'muc'    Purge MUC caches?
+ *        'theme'  Purge theme cache?
+ *        'lang'   Purge language string cache?
+ *        'js'     Purge javascript cache?
+ *        'filter' Purge text filter cache?
+ *        'other'  Purge all other caches?
+ */
+function purge_caches($options = []) {
+    $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'filter', 'other'], false);
+    if (empty(array_filter($options))) {
+        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
+    } else {
+        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
+    }
+    if ($options['muc']) {
+        cache_helper::purge_all();
+    }
+    if ($options['theme']) {
+        theme_reset_all_caches();
+    }
+    if ($options['lang']) {
+        get_string_manager()->reset_caches();
+    }
+    if ($options['js']) {
+        js_reset_all_caches();
+    }
+    if ($options['filter']) {
+        reset_text_filters_cache();
+    }
+    if ($options['other']) {
+        purge_other_caches();
+    }
+}
+
+/**
+ * Purge all non-MUC caches not otherwise purged in purge_caches.
+ *
+ * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
+ * {@link phpunit_util::reset_dataroot()}
+ */
+function purge_other_caches() {
+    global $DB, $CFG;
     core_text::reset_caches();
     if (class_exists('core_plugin_manager')) {
         core_plugin_manager::reset_caches();
@@ -1652,7 +1695,6 @@ function purge_all_caches() {
     }
 
     $DB->reset_caches();
-    cache_helper::purge_all();
 
     // Purge all other caches: rss, simplepie, etc.
     clearstatcache();
index 00e9fff..a2ee601 100644 (file)
@@ -245,6 +245,7 @@ function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'l
  */
 function theme_reset_all_caches() {
     global $CFG, $PAGE;
+    require_once("{$CFG->libdir}/filelib.php");
 
     $next = theme_get_next_revision();
     theme_set_revision($next);
@@ -257,6 +258,12 @@ function theme_reset_all_caches() {
     // Purge compiled post processed css.
     cache::make('core', 'postprocessedcss')->purge();
 
+    // Delete all old theme localcaches.
+    $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
+    foreach ($themecachedirs as $localcachedir) {
+        fulldelete($localcachedir);
+    }
+
     if ($PAGE) {
         $PAGE->reload_theme();
     }
@@ -662,6 +669,12 @@ class theme_config {
      */
     public $remapiconcache = [];
 
+    /**
+     * The name of the function to call to get precompiled CSS.
+     * @var string
+     */
+    public $precompiledcsscallback = null;
+
     /**
      * Load the config.php file for a particular theme, and return an instance
      * of this class. (That is, this is a factory method.)
@@ -739,7 +752,8 @@ class theme_config {
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
-            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition', 'iconsystem');
+            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
+            'iconsystem', 'precompiledcsscallback');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -1090,7 +1104,13 @@ class theme_config {
                 } else {
                     if ($type === 'theme' && $identifier === self::SCSS_KEY) {
                         // We need the content from SCSS because this is the SCSS file from the theme.
-                        $csscontent .= $this->get_css_content_from_scss(false);
+                        if ($compiled = $this->get_css_content_from_scss(false)) {
+                            $csscontent .= $compiled;
+                        } else {
+                            // The compiler failed so default back to any precompiled css that might
+                            // exist.
+                            $csscontent .= $this->get_precompiled_css_content();
+                        }
                     } else if ($type === 'theme' && $identifier === $this->lessfile) {
                         // We need the content from LESS because this is the LESS file from the theme.
                         $csscontent .= $this->get_css_content_from_less(false);
@@ -1487,6 +1507,26 @@ class theme_config {
         return $compiled;
     }
 
+    /**
+     * Return the precompiled CSS if the precompiledcsscallback exists.
+     *
+     * @return string Return compiled css.
+     */
+    public function get_precompiled_css_content() {
+        $configs = [$this] + $this->parent_configs;
+        $css = '';
+
+        foreach ($configs as $config) {
+            if (isset($config->precompiledcsscallback)) {
+                $function = $config->precompiledcsscallback;
+                if (function_exists($function)) {
+                    $css .= $function($this);
+                }
+            }
+        }
+        return $css;
+    }
+
     /**
      * Get the icon system to use.
      *
index 5b50374..47d8728 100644 (file)
@@ -81,11 +81,25 @@ class renderer_base {
         global $CFG;
 
         if ($this->mustache === null) {
+            require_once("{$CFG->libdir}/filelib.php");
+
             $themename = $this->page->theme->name;
             $themerev = theme_get_revision();
 
+            // Create new localcache directory.
             $cachedir = make_localcache_directory("mustache/$themerev/$themename");
 
+            // Remove old localcache directories.
+            $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
+            foreach ($mustachecachedirs as $localcachedir) {
+                $cachedrev = [];
+                preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
+                $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
+                if ($cachedrev > 0 && $cachedrev < $themerev) {
+                    fulldelete($localcachedir);
+                }
+            }
+
             $loader = new \core\output\mustache_filesystem_loader();
             $stringhelper = new \core\output\mustache_string_helper();
             $quotehelper = new \core\output\mustache_quote_helper();
index 514e002..3ff6192 100644 (file)
@@ -1805,6 +1805,74 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertFalse(has_all_capabilities($sca, $coursecontext, 0));
     }
 
+    /**
+     * Test that the caching in get_role_definitions() and get_role_definitions_uncached()
+     * works as intended.
+     */
+    public function test_role_definition_caching() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Get some role ids.
+        $authenticatedrole = $DB->get_record('role', array('shortname' => 'user'), '*', MUST_EXIST);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $emptyroleid = create_role('No capabilities', 'empty', 'A role with no capabilties');
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Instantiate the cache instance, since that does DB queries (get_config)
+        // and we don't care about those.
+        cache::make('core', 'roledefs');
+
+        // One database query is not necessarily one database read, it seems. Find out how many.
+        $startdbreads = $DB->perf_get_reads();
+        $rs = $DB->get_recordset('user');
+        $rs->close();
+        $readsperquery = $DB->perf_get_reads() - $startdbreads;
+
+        // Now load some role definitions, and check when it queries the database.
+
+        // Load the capabilities for two roles. Should be one query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Load the capabilities for same two roles. Should not query the DB.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Include a third role. Should do one DB query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Repeat call. No DB queries.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Alter a role.
+        role_change_permission($studentrole->id, $coursecontext, 'moodle/course:tag', CAP_ALLOW);
+
+        // Should now know to do one query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Now clear the in-memory cache, and verify that it does not query the DB.
+        // Cannot use accesslib_clear_all_caches_for_unit_testing since that also
+        // clears the MUC cache.
+        global $ACCESSLIB_PRIVATE;
+        $ACCESSLIB_PRIVATE->cacheroledefs = array();
+
+        // Get all roles. Should not need the DB.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+    }
+
     /**
      * Tests get_user_capability_course() which checks a capability across all courses.
      */
diff --git a/lib/tests/behat/behat_filters.php b/lib/tests/behat/behat_filters.php
new file mode 100644 (file)
index 0000000..2b06662
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2018 the Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Note: You cannot use MOODLE_INTERNAL test here, or include files which do so.
+// This file is required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../behat/behat_base.php');
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2018 the Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_filters extends behat_base {
+
+    /**
+     * Set the global filter configuration.
+     *
+     * @Given /^the "(?P<filter_name>(?:[^"]|\\")*)" filter is "(on|off|disabled)"$/
+     *
+     * @param string $filtername the name of a filter, e.g. 'glossary'.
+     * @param string $statename 'on', 'off' or 'disabled'.
+     */
+    public function the_filter_is($filtername, $statename) {
+        require_once(__DIR__ . '/../../filterlib.php');
+
+        switch ($statename) {
+            case 'on':
+                $state = TEXTFILTER_ON;
+                break;
+            case 'off':
+                $state = TEXTFILTER_OFF;
+                break;
+            case 'disabled':
+                $state = TEXTFILTER_DISABLED;
+                break;
+            default:
+                throw new coding_exception('Unknown filter state: ' . $statename);
+        }
+        filter_set_global_state($filtername, $state);
+    }
+}
index b8af6ff..c3aab08 100644 (file)
@@ -676,6 +676,17 @@ class core_filterlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('performance_measuring_filter_manager', $filterman);
     }
 
+    public function test_filter_get_globally_enabled_default() {
+        $enabledfilters = filter_get_globally_enabled();
+        $this->assertArrayNotHasKey('glossary', $enabledfilters);
+    }
+
+    public function test_filter_get_globally_enabled_after_change() {
+        filter_set_global_state('glossary', TEXTFILTER_ON);
+        $enabledfilters = filter_get_globally_enabled();
+        $this->assertArrayHasKey('glossary', $enabledfilters);
+    }
+
     public function test_filter_get_globally_enabled_filters_with_config() {
         $this->setup_available_in_context_tests();
 
index fe3b1d2..6d37bcf 100644 (file)
@@ -81,6 +81,47 @@ class core_mathslib_testcase extends basic_testcase {
         $this->assertSame(8, $formula->evaluate());
     }
 
+    public function test_conditional_functions() {
+        $formula = new calc_formula('=ifthenelse(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+
+        // Test synonim if.
+        $formula = new calc_formula('=if(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=if(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=if(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+    }
+
+    public function test_conditional_operators() {
+        $formula = new calc_formula('=2==2');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=2>3');
+        $this->assertSame(0, $formula->evaluate());
+        $formula = new calc_