Merge branch 'MDL-62715-master' of git://github.com/andrewnicols/moodle
authorJake Dallimore <jake@moodle.com>
Tue, 3 Jul 2018 00:49:35 +0000 (08:49 +0800)
committerJake Dallimore <jake@moodle.com>
Tue, 3 Jul 2018 00:49:35 +0000 (08:49 +0800)
211 files changed:
admin/classes/form/purge_caches.php [new file with mode: 0644]
admin/cli/purge_caches.php
admin/purgecaches.php
admin/renderer.php
admin/roles/admins.php
admin/searchareas.php
admin/settings/development.php
admin/tests/behat/filter_users.feature
admin/tests/behat/purge_caches.feature [new file with mode: 0644]
admin/tool/customlang/renderer.php
admin/tool/dataprivacy/amd/src/data_registry.js
admin/tool/dataprivacy/amd/src/expand_contract.js
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/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/my_data_requests.mustache
admin/tool/dataprivacy/templates/request_details.mustache
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.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/db/auth.php
auth/db/lang/en/auth_db.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/lang/en/auth_oauth2.php
backup/moodle2/tests/restore_stepslib_date_test.php
backup/util/ui/restore_ui_stage.class.php
blocks/recent_activity/lang/en/block_recent_activity.php
course/classes/search/section.php
course/completion.js
course/renderer.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
grade/edit/tree/index.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/completion.php
lang/en/filters.php
lang/en/mimetypes.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/adminlib.php
lib/amd/build/tag.min.js
lib/amd/src/tag.js
lib/classes/filetypes.php
lib/db/install.xml
lib/db/upgrade.php
lib/editor/atto/plugins/charmap/styles.css
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js
lib/editor/atto/plugins/charmap/yui/src/button/js/button.js
lib/editor/atto/plugins/equation/styles.css
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/editor/atto/plugins/image/styles.css
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js
lib/editor/atto/plugins/link/yui/src/button/js/button.js
lib/editor/atto/plugins/media/styles.css
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/audiomodule.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/videomodule.js
lib/editor/atto/plugins/table/styles.css
lib/editor/atto/plugins/table/tests/behat/table.feature
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/styles.css
lib/environmentlib.php
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/excellib.class.php
lib/filelib.php
lib/mathslib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/tests/accesslib_test.php
lib/tests/mathslib_test.php
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_key.php
message/classes/task/migrate_message_data.php
message/output/popup/amd/build/message_popover_controller.min.js
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/message_popover_controller.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/lib.php
message/output/popup/templates/message_popover.mustache
message/output/popup/templates/notification_popover.mustache
message/tests/migrate_message_data_task_test.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/lang/en/assignfeedback_editpdf.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/submission/onlinetext/lang/en/assignsubmission_onlinetext.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/forum/post.php
mod/glossary/lang/en/glossary.php
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/classes/privacy/provider.php
mod/scorm/lang/en/scorm.php
mod/scorm/tests/privacy_test.php
mod/wiki/view.php
mod/workshop/lang/en/workshop.php
question/engine/renderer.php
question/engine/tests/helpers.php
question/engine/tests/questionengine_test.php
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
search/index.php
tag/classes/privacy/provider.php
tag/tests/privacy_test.php
theme/boost/amd/build/drawer.min.js
theme/boost/amd/src/drawer.js
theme/boost/classes/privacy/provider.php
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/undo.scss
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/core/filemanager_loginform.mustache
theme/boost/templates/core/filemanager_selectlayout.mustache
theme/boost/templates/core/filemanager_uploadform.mustache
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/boost/tests/privacy_test.php [new file with mode: 0644]
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
user/filters/anycourses.php [new file with mode: 0644]
user/filters/lib.php
version.php

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 f22c6a2..da59ef6 100644 (file)
@@ -1990,7 +1990,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if (empty($CFG->docroot) or $environment_result->plugin) {
                     $report = get_string($stringtouse, 'admin', $rec);
                 } else {
-                    $report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec));
+                    $report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec), true);
                 }
                 // Enclose report text in div so feedback text will be displayed underneath it.
                 $report = html_writer::div($report);
index 8245949..4b341b3 100644 (file)
@@ -80,9 +80,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
         }
 
         if (isset($admins[$newmain])) {
+            $logstringold = implode(', ', $admins);
+
             unset($admins[$newmain]);
             array_unshift($admins, $newmain);
+
+            $logstringnew = implode(', ', $admins);
+
             set_config('siteadmins', implode(',', $admins));
+            add_to_config_log('siteadmins', $logstringold, $logstringnew, null);
+
             redirect($PAGE->url);
         }
     }
@@ -95,8 +102,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
             $admins[$admin] = $admin;
         }
     }
+
+    $logstringold = implode(', ', $admins);
+
     $admins[$confirmadd] = $confirmadd;
+
+    $logstringnew = implode(', ', $admins);
+
     set_config('siteadmins', implode(',', $admins));
+    add_to_config_log('siteadmins', $logstringold, $logstringnew, 'core');
+
     redirect($PAGE->url);
 
 } else if ($confirmdel and confirm_sesskey() and $confirmdel != $USER->id) {
@@ -107,8 +122,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
             $admins[$admin] = $admin;
         }
     }
+
+    $logstringold = implode(', ', $admins);
+
     unset($admins[$confirmdel]);
+
+    $logstringnew = implode(', ', $admins);
+
     set_config('siteadmins', implode(',', $admins));
+    add_to_config_log('siteadmins', $logstringold, $logstringnew, 'core');
+
     redirect($PAGE->url);
 }
 
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
index b9a03e5..75e2b42 100644 (file)
@@ -65,3 +65,20 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I should not see "User Two"
     And I should not see "User Three"
     And I should not see "User Four"
+
+  Scenario: Filter user accounts by enrolled in any course
+    When I set the following fields to these values:
+      | id_anycourses | Yes |
+    And I press "Add filter"
+    Then I should see "User One"
+    And I should see "User Two"
+    And I should see "User Three"
+    And I should not see "User Four"
+    And I press "Remove all filters"
+    And I set the following fields to these values:
+      | id_anycourses | No |
+    And I press "Add filter"
+    And I should not see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should see "User Four"
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"
index 224e76b..aea7fd6 100644 (file)
@@ -134,11 +134,13 @@ class tool_customlang_renderer extends plugin_renderer_base {
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
-        $save1   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
-        $save2   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
-        $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
+        $save1   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
+            'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
+        $save2   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
+            'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
+        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
         $output .= html_writer::table($table);
-        $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
+        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
         $output .= html_writer::end_tag('div');
         $output .= html_writer::end_tag('form');
 
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() {
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..275044a 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', '');
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..c68c994 100644 (file)
@@ -132,7 +132,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.
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..a3e613a 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');
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 40dc1e7..4817d68 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.';
@@ -178,7 +180,7 @@ $string['privacy:metadata:request'] = 'Information from personal data requests (
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s 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.';
@@ -193,21 +195,22 @@ $string['purposeupdated'] = 'Purpose updated';
 $string['replyto'] = 'Reply to';
 $string['requestactions'] = 'Actions';
 $string['requestby'] = 'Requested by';
+$string['requestbydetail'] = 'Requested by:';
 $string['requestcomments'] = 'Comments';
 $string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
 $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 beed5ac..45cedc6 100644 (file)
                 "comments": "I would like to download all of my daughter's personal data",
                 "statuslabelclass": "label-default",
                 "statuslabel": "Pending",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 2,
                 "comments": "Please give me all of the information you have about me...",
                 "statuslabelclass": "label-warning",
                 "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 3,
                 "comments": "Please delete all of my son's personal data.",
                 "statuslabelclass": "label-success",
                 "statuslabel": "Complete",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 4,
                 "comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
                 "statuslabelclass": "label-danger",
                 "statuslabel": "Rejected",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 5,
                 "comments": "Please let me download my data",
                 "statuslabelclass": "label-info",
                 "statuslabel": "Processing",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             }
         ]
     }
                 <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>
               }}>
                 <td>{{typenameshort}}</td>
                 <td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</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>
index 1bdcbdb..2b654b2 100644 (file)
                 "comments": "I would like to download all of my daughter's personal data",
                 "statuslabelclass": "label-default",
                 "statuslabel": "Pending",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 2,
                 "comments": "Give me all of the information you have about me, or else...",
                 "statuslabelclass": "label-warning",
                 "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 3,
                 "comments": "Please delete all of my son's personal data.",
                 "statuslabelclass": "label-success",
                 "statuslabel": "Complete",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 4,
                 "comments": "Delete my data or I'm coming for you...",
                 "statuslabelclass": "label-danger",
                 "statuslabel": "Rejected",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 5,
                 "comments": "Please let me download my data",
                 "statuslabelclass": "label-info",
                 "statuslabel": "Processing",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             }
         ]
     }
             <tr>
                 <th scope="col">{{#str}}requesttype, 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>
               }} data-status="{{status}}"{{!
               }}>
                 <td>{{typename}}</td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</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>
index 531e394..c8e9dc1 100644 (file)
         "canreview": true,
         "reviewurl": "#",
         "timecreated": 1517561224,
+        "requestedbyuser" : {
+            "fullname": "Martha Smith",
+            "profileurl": "#"
+        },
         "statuslabel": "Pending",
         "statuslabelclass": "label-default",
         "messagehtml": "<p>Hello,</p><p>I would like to download all of my personal data.</p><p>Thanks!</p>"
             <a href="mailto:{{foruser.email}}">{{foruser.email}}</a>
             <div class="clearfix m-t-1 m-b-1">
             <span class="pull-left m-r-1">
-                <strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}
+                <strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}
             </span>
                 <span class="pull-left m-r-1">
                 <strong>{{#str}}statusdetail, tool_dataprivacy{{/str}}</strong>
                 <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
             </span>
+            <span class="pull-left m-r-1">
+                <strong>{{#str}}requestbydetail, tool_dataprivacy{{/str}}</strong>
+                <span><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></span>
+            </span>
             </div>
             {{#canreview}}
                 <!--a href="{{reviewurl}}" class="btn btn-default">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a-->
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 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 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 549e103..bcb26df 100644 (file)
@@ -709,12 +709,12 @@ class auth_plugin_db extends auth_plugin_base {
         raise_memory_limit(MEMORY_HUGE);
 
         if (empty($this->config->table)) {
-            echo $OUTPUT->notification('External table not specified.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbnoexttable', 'auth_db'), 'notifyproblem');
             return;
         }
 
         if (empty($this->config->fielduser)) {
-            echo $OUTPUT->notification('External user field not specified.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbnouserfield', 'auth_db'), 'notifyproblem');
             return;
         }
 
@@ -735,7 +735,7 @@ class auth_plugin_db extends auth_plugin_base {
             error_reporting($CFG->debug);
             ob_end_flush();
 
-            echo $OUTPUT->notification('Cannot connect the database.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbcannotconnect', 'auth_db'), 'notifyproblem');
             return;
         }
 
@@ -744,17 +744,17 @@ class auth_plugin_db extends auth_plugin_base {
                                 WHERE {$this->config->fielduser} <> 'random_unlikely_username'"); // Any unlikely name is ok here.
 
         if (!$rs) {
-            echo $OUTPUT->notification('Can not read external table.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbcannotreadtable', 'auth_db'), 'notifyproblem');
 
         } else if ($rs->EOF) {
-            echo $OUTPUT->notification('External table is empty.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbtableempty', 'auth_db'), 'notifyproblem');
             $rs->close();
 
         } else {
             $fields_obj = $rs->FetchObj();
             $columns = array_keys((array)$fields_obj);
 
-            echo $OUTPUT->notification('External table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess');
+            echo $OUTPUT->notification(get_string('auth_dbcolumnlist', 'auth_db', implode(', ', $columns)), 'notifysuccess');
             $rs->close();
         }
 
index 216fd31..8f77a3c 100644 (file)
@@ -68,5 +68,11 @@ $string['auth_dbuser'] = 'Username with read access to the database';
 $string['auth_dbuser_key'] = 'DB user';
 $string['auth_dbuserstoadd'] = 'User entries to add: {$a}';
 $string['auth_dbuserstoremove'] = 'User entries to remove: {$a}';
+$string['auth_dbnoexttable'] = 'External table not specified.';
+$string['auth_dbnouserfield'] = 'External user field not specified.';
+$string['auth_dbcannotconnect'] = 'Cannot connect to external database.';
+$string['auth_dbcannotreadtable'] = 'Cannot read external table.';
+$string['auth_dbtableempty'] = 'External table is empty.';
+$string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}';
 $string['pluginname'] = 'External database';
 $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.';
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 1bcce5f..e5cc3f4 100644 (file)
@@ -244,7 +244,7 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
         // Testing the restore of an overridden grade.
         list($course, $assign) = $this->create_course_and_module('assign', []);
         $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
-        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $assignobj = new mod_assign_testable_assign(context_module::instance($cm->id), $cm, $course);
         $submission = $assignobj->get_user_submission($USER->id, true);
         $grade = $assignobj->get_user_grade($USER->id, true);
         $grade->grade = 75;
@@ -300,7 +300,7 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
                 'completionusegrade' => 1 // Student must receive a grade to complete this activity.
             ]);
         $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
-        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $assignobj = new mod_assign_testable_assign(context_module::instance($cm->id), $cm, $course);
         $submission = $assignobj->get_user_submission($USER->id, true);
         $grade = $assignobj->get_user_grade($USER->id, true);
         $grade->grade = 75;
@@ -410,4 +410,4 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
             $this->assertEquals($dates['originaldate'], $dates['restoredate']);
         }
     }
-}
\ No newline at end of file
+}
index 2ff89e6..bc09bc5 100644 (file)
@@ -1061,7 +1061,26 @@ class restore_ui_stage_process extends restore_ui_stage {
                 if (!empty($info->role_mappings->mappings)) {
                     $context = context_course::instance($this->ui->get_controller()->get_courseid());
                     $assignableroles = get_assignable_roles($context, ROLENAME_ALIAS, false);
-                    $html .= $renderer->role_mappings($info->role_mappings->mappings, $assignableroles);
+
+                    // Get current role mappings.
+                    $currentroles = role_fix_names(get_all_roles(), $context);
+                    // Get backup role mappings.
+                    $rolemappings = $info->role_mappings->mappings;
+
+                    array_map(function($rolemapping) use ($currentroles) {
+                        foreach ($currentroles as $role) {
+                            // Find matching archetype to determine the backup's shortname for label display.
+                            if ($rolemapping->archetype == $role->archetype) {
+                                $rolemapping->name = $rolemapping->shortname;
+                                break;
+                            }
+                        }
+                        if ($rolemapping->name == null) {
+                            $rolemapping->name = get_string('undefinedrolemapping', 'backup', $rolemapping->archetype);
+                        }
+                    }, $rolemappings);
+
+                    $html .= $renderer->role_mappings($rolemappings, $assignableroles);
                 }
                 break;
             default:
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 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 58e5477..8e74142 100644 (file)
@@ -20,7 +20,8 @@ M.core_completion.init = function(Y) {
             var current = args.state.get('value');
             var modulename = args.modulename.get('value'),
                 altstr,
-                iconkey;
+                iconkey,
+                button = args.image.get('parentNode');
 
 
             if (current == 1) {
@@ -32,6 +33,7 @@ M.core_completion.init = function(Y) {
                 iconkey = 'i/completion-manual-n';
                 args.state.set('value', 1);
             }
+            button.set('title', altstr);
 
             require(['core/templates', 'core/notification'], function(Templates, Notification) {
                 Templates.renderPix(iconkey, 'core', altstr).then(function(html) {
index b3cd970..b78acb6 100644 (file)
@@ -551,7 +551,8 @@ class core_course_renderer extends plugin_renderer_base {
                 $output .= html_writer::empty_tag('input', array(
                     'type' => 'hidden', 'name' => 'completionstate', 'value' => $newstate));
                 $output .= html_writer::tag('button',
-                    $this->output->pix_icon('i/completion-' . $completionicon, $imgalt), array('class' => 'btn btn-link'));
+                    $this->output->pix_icon('i/completion-' . $completionicon, $imgalt),
+                        array('class' => 'btn btn-link', 'title' => $imgalt));
                 $output .= html_writer::end_tag('div');
                 $output .= html_writer::end_tag('form');
             } else {
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 ad195b1..c9ecd60 100644 (file)
@@ -43,7 +43,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 function xmldb_enrol_paypal_upgrade($oldversion) {
-    global $CFG;
+    global $DB;
+
+    $dbman = $DB->get_manager();
 
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
@@ -57,5 +59,84 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018053000) {
+
+        // Define field instanceid to be added to enrol_paypal.
+        // For some reason, some Moodle instances that are upgraded from old versions do not have this field.
+        $table = new xmldb_table('enrol_paypal');
+        $field = new xmldb_field('instanceid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid');
+
+        // Conditionally launch add field instanceid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Paypal savepoint reached.
+        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 04ee0a4..6ca2f17 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;        // 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 3a1ce5e..b898b99 100644 (file)
@@ -274,13 +274,13 @@ echo $OUTPUT->container_start('buttons mdl-align');
 if ($moving) {
     echo $OUTPUT->single_button(new moodle_url('index.php', array('id'=>$course->id)), get_string('cancel'), 'get');
 } else {
-    echo $OUTPUT->single_button(new moodle_url('category.php', array('courseid'=>$course->id)), get_string('addcategory', 'grades'), 'get');
-    echo $OUTPUT->single_button(new moodle_url('item.php', array('courseid'=>$course->id)), get_string('additem', 'grades'), 'get');
-
+    echo $OUTPUT->single_button(new moodle_url('item.php', array('courseid' => $course->id)), get_string('additem',
+        'grades'), 'get');
     if (!empty($CFG->enableoutcomes)) {
         echo $OUTPUT->single_button(new moodle_url('outcomeitem.php', array('courseid'=>$course->id)), get_string('addoutcomeitem', 'grades'), 'get');
     }
-
+    echo $OUTPUT->single_button(new moodle_url('category.php', array('courseid' => $course->id)), get_string('addcategory',
+        'grades'), 'get');
     //echo $OUTPUT->(new moodle_url('index.php', array('id'=>$course->id, 'action'=>'autosort')), get_string('autosort', 'grades'), 'get');
 }
 
index aa79e8e..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.';
@@ -631,7 +632,7 @@ $string['invalidsection'] = 'Invalid section.';
 $string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.';
 $string['ipblocked'] = 'This site is not available currently.';
 $string['ipblocker'] = 'IP blocker';
-$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines are ignored.';
+$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines, and text following a "#" character are ignored.';
 $string['iplookup'] = 'IP address lookup';
 $string['iplookupgeoplugin'] = '<a href="http://www.geoplugin.com">geoPlugin</a> service is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.';
 $string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The Internet Geographic Database) server to lookup location of IP addresses, unfortunately this database is not maintained anymore and may return <em>wildly incorrect</em> data.
@@ -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 dc711e8..abaf88e 100644 (file)
@@ -326,6 +326,7 @@ $string['timetaken'] = 'Time taken';
 $string['title'] = 'Title';
 $string['totalcategorysearchresults'] = 'Total categories: {$a}';
 $string['totalcoursesearchresults'] = 'Total courses: {$a}';
+$string['undefinedrolemapping'] = 'Role mapping undefined for: \'{$a}\' archetype';
 $string['unnamedsection'] = 'Unnamed section';
 $string['userinfo'] = 'Userinfo';
 $string['module'] = 'Module';
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 7c1b469..f178887 100644 (file)
@@ -26,6 +26,7 @@ $string['actfilterhdr'] = 'Active filters';
 $string['addfilter'] = 'Add filter';
 $string['anycategory'] = 'any category';
 $string['anycourse'] = 'any course';
+$string['anycourses'] = 'Enrolled in any course';
 $string['anyfield'] = 'any field';
 $string['anyrole'] = 'any role';
 $string['anyvalue'] = 'any value';
index 3889226..15b8463 100644 (file)
@@ -36,6 +36,7 @@
  */
 
 $string['application/epub_zip'] = 'EPUB ebook';
+$string['application/json'] = '{$a->MIMETYPE2} text';
 $string['application/msword'] = 'Word document';
 $string['application/pdf'] = 'PDF document';
 $string['application/vnd.moodle.backup'] = 'Moodle backup';
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 b14741e..c3e39cd 100644 (file)
@@ -3614,14 +3614,15 @@ class admin_setting_configiplist extends admin_setting_configtextarea {
      */
     public function validate($data) {
         if(!empty($data)) {
-            $ips = explode("\n", $data);
+            $lines = explode("\n", $data);
         } else {
             return true;
         }
         $result = true;
         $badips = array();
-        foreach($ips as $ip) {
-            $ip = trim($ip);
+        foreach ($lines as $line) {
+            $tokens = explode('#', $line);
+            $ip = trim($tokens[0]);
             if (empty($ip)) {
                 continue;
             }
index ab06031..fd51f0b 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
index 1f1d3fb..1609564 100644 (file)
@@ -150,8 +150,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 ]).done(function(s) {
                     var el = $('<div><form id="combinetags_form" class="form-inline">' +
                         '<p class="description"></p><p class="options"></p>' +
-                        '<p class="mdl-align"><input type="submit" id="combinetags_submit"/>' +
-                        '<input type="button" id="combinetags_cancel"/></p>' +
+                        '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="combinetags_submit"/>' +
+                        '<input type="button" class="btn btn-secondary" id="combinetags_cancel"/></p>' +
                         '</form></div>');
                     el.find('.description').html(s[1]);
                     el.find('#combinetags_submit').attr('value', s[2]);
@@ -223,8 +223,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                         '<input type="hidden" name="sesskey" value="' + M.cfg.sesskey + '"/>' +
                         '<p><label for="id_tagslist">' + s[1] + '</label>' +
                         '<input type="text" id="id_tagslist" name="tagslist"/></p>' +
-                        '<p class="mdl-align"><input type="submit" id="addtags_submit"/>' +
-                        '<input type="button" id="addtags_cancel"/></p>' +
+                        '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtags_submit"/>' +
+                        '<input type="button" class="btn btn-secondary" id="addtags_cancel"/></p>' +
                         '</form></div>');
                     el.find('#addtags_form').attr('action', window.location.href);
                     el.find('#addtags_submit').attr('value', s[2]);
@@ -295,8 +295,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                             '<input id="addtagcoll_name" type="text"/></p>' +
                             '<p><label for="addtagcoll_searchable"></label>: ' +
                             '<input id="addtagcoll_searchable" type="checkbox" value="1" checked/></p>' +
-                            '<p class="mdl-align"><input type="submit" id="addtagcoll_submit"/>' +
-                            '<input type="button" id="addtagcoll_cancel"/></p>' +
+                            '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtagcoll_submit"/>' +
+                            '<input type="button" class="btn btn-secondary" id="addtagcoll_cancel"/></p>' +
                             '</form></div>');
                         el.find('label[for="addtagcoll_name"]').html(s[1]);
                         el.find('label[for="addtagcoll_searchable"]').html(s[2]);
index 23c0191..4a84e55 100644 (file)
@@ -139,6 +139,7 @@ abstract class core_filetypes {
             'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
             'jqz' => array('type' => 'text/xml', 'icon' => 'markup'),
             'js' => array('type' => 'application/x-javascript', 'icon' => 'text', 'groups' => array('web_file')),
+            'json' => array('type' => 'application/json', 'icon' => 'text'),
             'latex' => array('type' => 'application/x-latex', 'icon' => 'text'),
             'm' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'mbz' => array('type' => 'application/vnd.moodle.backup', 'icon' => 'moodle'),
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 0f151ec..4656462 100644 (file)
@@ -1,4 +1,4 @@
 .atto_charmap_selector button {
-    width: 2em;
-    padding: 0 3px;
+    width: 2.18rem;
+    margin: 0.1rem;
 }
index 30e0e37..422e2d8 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js differ
index 77b7449..339dad4 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js differ
index 30e0e37..422e2d8 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js differ
index 6f549b4..397f1fe 100644 (file)
@@ -377,7 +377,7 @@ Y.namespace('M.atto_charmap').Button = Y.Base.create('button', Y.M.editor_atto.E
             '<div class="{{CSS.CHARMAP}}">' +
                 '{{#each CHARMAP}}' +
                     '{{#if this.[2]}}' +
-                    '<button class="{{../../CSS.BUTTON}}" ' +
+                    '<button class="btn btn-default btn-sm {{../../CSS.BUTTON}}" ' +
                         'aria-label="{{get_string this.[3] ../../component}}" ' +
                         'title="{{get_string this.[3] ../../component}}" ' +
                         'data-character="{{this.[0]}}" ' +
index f6081a0..8df2d6b 100644 (file)
@@ -1,16 +1,3 @@
-.atto_equation_library .yui3-tabview-list {
-    border: none;
-}
-
-.atto_equation_library .yui3-tab-selected .yui3-tab-label,
-.yui3-skin-sam #atto_equation_library .yui3-tab-selected .yui3-tab-label:focus,
-.yui3-skin-sam #atto_equation_library .yui3-tab-selected .yui3-tab-label:hover {
-    background: none;
-    color: black;
-    border-top-left-radius: 4px;
-    border-top-right-radius: 4px;
-}
-
 .atto_equation_library button {
     margin: 0.25%;
     min-width: 12%;
index ae1d92c..fdc94ce 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index a5ff287..00d0781 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index f98b58c..1274071 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 849a967..6db2cb8 100644 (file)
@@ -60,29 +60,34 @@ var COMPONENTNAME = 'atto_equation',
                 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" ' +
                         'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
                 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
-                '<div describedby="{{elementid}}_cursorinfo" class="well well-small fullwidth {{CSS.EQUATION_PREVIEW}}" ' +
+                '<div describedby="{{elementid}}_cursorinfo" class="well well-small p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' +
                         'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
                 '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
+                    '<button class="btn btn-default {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
                 '</div>' +
             '</form>',
         LIBRARY: '' +
             '<div class="{{CSS.LIBRARY}}">' +
-                '<ul>' +
+                '<ul class="root nav nav-tabs m-b-1" role="tablist">' +
                     '{{#each library}}' +
-                        '<li><a href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
-                            '{{get_string groupname ../component}}' +
-                        '</a></li>' +
+                        '<li  class="nav-item">' +
+                            '<a class="nav-link" href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' +
+                                ' role="tab" data-toggle="tab">' +
+                                '{{get_string groupname ../component}}' +
+                            '</a>' +
+                        '</li>' +
                     '{{/each}}' +
                 '</ul>' +
-                '<div class="{{CSS.LIBRARY_GROUPS}}">' +
+                '<div class="tab-content m-b-1 {{CSS.LIBRARY_GROUPS}}">' +
                     '{{#each library}}' +
-                        '<div id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
+                        '<div data-medium-type="{{CSS.LINK}}" class="tab-pane" ' +
+                        'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
                             '<div role="toolbar">' +
                             '{{#split "\n" elements}}' +
-                                '<button tabindex="-1" data-tex="{{this}}" aria-label="{{this}}" title="{{this}}">' +
+                                '<button class="btn btn-default" tabindex="-1" data-tex="{{this}}"' +
+                                    'aria-label="{{this}}" title="{{this}}">' +
                                     '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' +
                                 '</button>' +
                             '{{/split}}' +
@@ -220,13 +225,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         var content = this._getDialogueContent();
         dialogue.set('bodyContent', content);
 
-        var library = content.one(SELECTORS.LIBRARY);
+        content.one('.nav-item:first-child .nav-link').getDOMNode().click();
 
-        var tabview = new Y.TabView({
-            srcNode: library
-        });
-
-        tabview.render();
         dialogue.show();
         // Notify the filters about the modified nodes.
         require(['core/event'], function(event) {
index a913c73..8656f0e 100644 (file)
@@ -6,7 +6,7 @@
 }
 
 .atto_image_preview_box {
-    max-height: 200px;
+    max-height: 150px;
     margin-bottom: 1em;
     overflow: auto;
 }
     cursor: pointer;
 }
 
-.atto_image_size {
-    display: inline-block;
-}
-
-.atto_image_size input[type=checkbox] {
-    margin-left: 1em;
-    margin-right: 1em;
-}
-
-.atto_image_size input[type=text] {
-    width: 3em;
-}
-
-.atto_image_size label {
-    display: inline-block;
-}
-
 .atto_image_button_text-top {
     vertical-align: text-top;
     margin: 0 0.5em;
index 69ab2fd..899c1a8 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js differ
index 78edf53..bbe3e39 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js differ
index 18416c3..293794c 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js differ
index f9c0d60..5cc1626 100644 (file)
@@ -94,52 +94,79 @@ var CSS = {
 
     TEMPLATE = '' +
             '<form class="atto_form">' +
-                '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
-                '<input class="fullwidth {{CSS.INPUTURL}}" type="url" id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
-                '<br/>' +
 
                 // Add the repository browser button.
                 '{{#if showFilepicker}}' +
-                    '<button class="{{CSS.IMAGEBROWSER}}" type="button">{{get_string "browserepositories" component}}</button>' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
+                        '<div class="input-group input-append w-100">' +
+                            '<input class="form-control {{CSS.INPUTURL}}" type="url" ' +
+                            'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
+                            '<span class="input-group-append">' +
+                                '<button class="btn btn-default {{CSS.IMAGEBROWSER}}" type="button">' +
+                                '{{get_string "browserepositories" component}}</button>' +
+                            '</span>' +
+                        '</div>' +
+                    '</div>' +
+                '{{else}}' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
+                        '<input class="form-control fullwidth {{CSS.INPUTURL}}" type="url" ' +
+                        'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
+                    '</div>' +
                 '{{/if}}' +
 
                 // Add the Alt box.
-                '<div style="display:none" role="alert" class="warning {{CSS.IMAGEALTWARNING}}">' +
+                '<div style="display:none" role="alert" class="alert alert-warning m-b-1 {{CSS.IMAGEALTWARNING}}">' +
                     '{{get_string "presentationoraltrequired" component}}' +
                 '</div>' +
+                '<div class="m-b-1">' +
                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
-                '<input class="fullwidth {{CSS.INPUTALT}}" type="text" value="" id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
-                '<br/>' +
+                '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
+                'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
 
                 // Add the presentation select box.
-                '<input type="checkbox" class="{{CSS.IMAGEPRESENTATION}}" id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
-                '<label class="sameline" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
+                '<div class="form-check">' +
+                '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
+                    'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
+                '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
                     '{{get_string "presentation" component}}' +
                 '</label>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
 
                 // Add the size entry boxes.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
-                '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="{{CSS.INPUTSIZE}}">' +
+                '<div class="m-b-1">' +
+                '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
+                '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTWIDTH}} input-mini" id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x ' +
+                '<input type="text" class="form-control m-r-1 input-mini {{CSS.INPUTWIDTH}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
 
                 // Add the height entry box.
                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTHEIGHT}} input-mini" id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
+                '<input type="text" class="form-control m-l-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
 
                 // Add the constrain checkbox.
-                '<input type="checkbox" class="{{CSS.INPUTCONSTRAIN}} sameline" id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
-                '<label for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">{{get_string "constrain" component}}</label>' +
+                '<div class="form-check m-l-2">' +
+                '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
+                '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
+                '{{get_string "constrain" component}}</label>' +
+                '</div>' +
+                '</div>' +
                 '</div>' +
 
                 // Add the alignment selector.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
-                '<select class="{{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
+                '<div class="form-inline m-b-1">' +
+                '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
+                '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
                     '{{#each alignments}}' +
                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
                     '{{/each}}' +
                 '</select>' +
+                '</div>' +
                 // Hidden input to store custom styles.
                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
                 '<br/>' +
@@ -151,7 +178,7 @@ var CSS = {
                 '</div>' +
 
                 // Add the submit button and close the form.
-                '<button class="{{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveimage" component}}</button>' +
+                '<button class="btn btn-default {{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveimage" component}}</button>' +
                 '</div>' +
             '</form>',
 
index 7aa1907..d1c971e 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js differ
index f7b3aeb..4f1d046 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js differ
index 7aa1907..d1c971e 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js differ
index 8226483..51112dd 100644 (file)
@@ -41,20 +41,32 @@ var COMPONENTNAME = 'atto_link',
     },
     TEMPLATE = '' +
             '<form class="atto_form">' +
-                '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
-                '<input class="fullwidth url {{CSS.URLINPUT}}" type="url" id="{{elementid}}_atto_link_urlentry" size="32"/><br/>' +
-
-                // Add the repository browser button.
                 '{{#if showFilepicker}}' +
-                    '<button class="openlinkbrowser">{{get_string "browserepositories" component}}</button>' +
-                    '<br/>' +
+                    '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
+                    '<div class="input-group input-append w-100 m-b-1">' +
+                        '<input class="form-control url {{CSS.URLINPUT}}" type="url" ' +
+                        'id="{{elementid}}_atto_link_urlentry"/>' +
+                        '<span class="input-group-append">' +
+                            '<button class="btn btn-default openlinkbrowser" type="button">' +
+                            '{{get_string "browserepositories" component}}</button>' +
+                        '</span>' +
+                    '</div>' +
+                '{{else}}' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
+                        '<input class="form-control fullwidth url {{CSS.URLINPUT}}" type="url" ' +
+                        'id="{{elementid}}_atto_link_urlentry" size="32"/>' +
+                    '</div>' +
                 '{{/if}}' +
-                '<input type="checkbox" class="newwindow" id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
-                '<label class="sameline" for="{{elementid}}_{{CSS.NEWWINDOW}}">{{get_string "openinnewwindow" component}}</label>' +
-                '<br/>' +
+                '<div class="form-check">' +
+                    '<input type="checkbox" class="form-check-input newwindow" id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
+                    '<label class="form-check-label" for="{{elementid}}_{{CSS.NEWWINDOW}}">' +
+                    '{{get_string "openinnewwindow" component}}' +
+                    '</label>' +
+                '</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button type="submit" class="submit">{{get_string "createlink" component}}</button>' +
+                    '<button type="submit" class="btn btn-default submit">{{get_string "createlink" component}}</button>' +
                 '</div>' +
             '</form>';
 Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
index ee5a759..f1ecb10 100644 (file)
@@ -1,106 +1,4 @@
-.atto_form.atto_media #video input,
-.atto_form.atto_media #audio input,
-.atto_form.atto_media #link input {
-    box-sizing: border-box;
-    height: inherit;
-}
-
 .atto_form.atto_media > .tab-content {
     max-height: 45vh;
     overflow-x: hidden;
-    padding-left: 20px;
-    padding-right: 20px;
-    margin-left: -20px;
-    margin-right: -21px;
-}
-
-.atto_form.atto_media [id$="-advanced-settings"] label {
-    margin-right: 10px;
-}
-
-.atto_form.atto_media label {
-    display: inline-block;
-}
-
-.atto_form.atto_media label > span {
-    display: inline-block;
-    min-width: 6em;
-}
-
-.atto_form.atto_media .atto_media_track_lang_entry,
-.atto_form.atto_media .atto_media_track_label_entry {
-    width: 168px;
-}
-
-.atto_form.atto_media .atto_media_track_source {
-    margin-bottom: 10px;
-}
-
-.atto_form.atto_media select {
-    margin-right: 10px;
-}
-
-.atto_form.atto_media [id$="-tracks"] input[type=checkbox] {
-    margin-left: 10px;
-}
-
-.atto_form.atto_media .atto_media_track ~ .atto_media_track {
-    margin-top: 5px;
-    padding-top: 10px;
-    border-top: 1px solid #e5e5e5;
-}
-
-.atto_form.atto_media label.fullwidth {
-    width: 100%;
-}
-
-.atto_media_postersize {
-    display: inline-block;
-}
-
-.atto_media_postersize input[type=text] {
-    width: 3em;
-}
-
-input[size].atto_media_url_entry {
-    width: calc(100% - 15px);
-}
-
-.openmediabrowser {
-    margin-top: -4px;
-}
-
-.addcomponent,
-.removecomponent {
-    font-weight: bold;
-    margin-right: 10px;
-}
-
-.trackhelp {
-    text-align: right;
-}
-
-.atto_form.atto_media .atto_media_source > label {
-    width: calc(100% - 153px);
-}
-
-.atto_form.atto_media .atto_media_track_lang_entry,
-.atto_form.atto_media .atto_media_track_label_entry {
-    width: 116px;
-}
-
-.langlabel {
-    width: 42%;
-}
-
-.labellabel {
-    width: 44%;
-}
-
-.defaultlabel {
-    width: 14%;
-}
-
-[data-medium-type=link] label {
-    width: 100%;
-}
+}
\ No newline at end of file
index 603896a..541685a 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index 0049c93..cfdd7c1 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index 603896a..541685a 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index f5a63c0..b406076 100644 (file)
@@ -114,7 +114,7 @@ var COMPONENTNAME = 'atto_media',
     TEMPLATES = {
         ROOT: '' +
             '<form class="mform atto_form atto_media" id="{{elementid}}_atto_media_form">' +
-                '<ul class="root nav nav-tabs" role="tablist">' +
+                '<ul class="root nav nav-tabs m-b-1" role="tablist">' +
                     '<li data-medium-type="{{CSS.LINK}}" class="nav-item">' +
                         '<a class="nav-link active" href="#{{elementid}}_{{CSS.LINK}}" role="tab" data-toggle="tab">' +
                             '{{get_string "link" component}}' +
@@ -144,17 +144,15 @@ var COMPONENTNAME = 'atto_media',
                 '</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button class="submit" type="submit">{{get_string "createmedia" component}}</button>' +
+                    '<button class="btn btn-default submit" type="submit">{{get_string "createmedia" component}}</button>' +
                 '</div>' +
             '</form>',
         TAB_PANES: {
             LINK: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' +
-                '<label>' +
-                    'Enter name' +
-                    '<input class="fullwidth {{CSS.NAME_INPUT}}" type="text" id="{{elementid}}_link_nameentry"' +
-                        'size="32" required="true"/>' +
-                '</label>',
+                '<label for="{{elementid}}_link_nameentry">{{get_string "entername" component}}</label>' +
+                '<input class="form-control fullwidth {{CSS.NAME_INPUT}}" type="text" id="{{elementid}}_link_nameentry"' +
+                        'size="32" required="true"/>',
             VIDEO: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' +
                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
@@ -200,13 +198,19 @@ var COMPONENTNAME = 'atto_media',
         FORM_COMPONENTS: {
             SOURCE: '' +
                 '<div class="{{CSS.SOURCE}} {{id}}">' +
-                    '<label>' +
+                    '<div class="m-b-1">' +
+                        '<label for="url-input">' +
                         '{{#entersourcelabel}}{{get_string ../entersourcelabel ../component}}{{/entersourcelabel}}' +
-                        '{{^entersourcelabel}}{{get_string "entersource" ../component}}{{/entersourcelabel}}</a>' +
-                        '<br/>' +
-                        '<input class="{{CSS.URL_INPUT}}" type="url" size="32"/>' +
-                    '</label>' +
-                    '<button class="openmediabrowser" type="button">{{get_string "browserepositories" component}}</button>' +
+                        '{{^entersourcelabel}}{{get_string "entersource" ../component}}{{/entersourcelabel}}' +
+                        '</label>' +
+                        '<div class="input-group input-append w-100">' +
+                            '<input id="url-input" class="form-control {{CSS.URL_INPUT}}" type="url" size="32"/>' +
+                            '<span class="input-group-append">' +
+                                '<button class="btn btn-default openmediabrowser" type="button">' +
+                                '{{get_string "browserepositories" component}}</button>' +
+                            '</span>' +
+                        '</div>' +
+                    '</div>' +
                     '{{#multisource}}' +
                         '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' +
                             ' help=../addsourcehelp}}' +
@@ -229,44 +233,42 @@ var COMPONENTNAME = 'atto_media',
                 '</div>',
             DISPLAY_OPTIONS: '' +
                 '<div class="{{CSS.DISPLAY_OPTIONS}}">' +
-                    '<label>' +
-                        '{{get_string "size" component}}' +
-                        '<div class={{CSS.POSTER_SIZE}}>' +
-                            '<label>' +
-                                '<span class="accesshide">{{get_string "videowidth" component}}</span>' +
-                                '<input type="text" class="{{CSS.WIDTH_INPUT}} input-mini" size="4"/>' +
-                            '</label>' +
+                    '<div class="m-b-1">' +
+                        '<label>{{get_string "size" component}}</label>' +
+                        '<div class="form-inline {{CSS.POSTER_SIZE}}">' +
+                            '<label class="accesshide">{{get_string "videowidth" component}}</label>' +
+                            '<input type="text" class="form-control m-r-1 {{CSS.WIDTH_INPUT}} input-mini" size="4"/>' +
                             ' x ' +
-                            '<label>' +
-                                '<span class="accesshide">{{get_string "videoheight" component}}</span>' +
-                                '<input type="text" class="{{CSS.HEIGHT_INPUT}} input-mini" size="4"/>' +
-                            '</label>' +
+                            '<label class="accesshide">{{get_string "videoheight" component}}</label>' +
+                            '<input type="text" class="form-control m-l-1 {{CSS.HEIGHT_INPUT}} input-mini" size="4"/>' +
                         '</div>' +
-                    '</label>' +
+                    '</div>' +
                     '<div class="clearfix"></div>' +
                     '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' +
                 '<div>',
             ADVANCED_SETTINGS: '' +
                 '<div class="{{CSS.ADVANCED_SETTINGS}}">' +
-                    '<label>' +
-                        '<input type="checkbox" checked="true" class="{{CSS.MEDIA_CONTROLS_TOGGLE}}"/>' +
-                        '{{get_string "controls" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_AUTOPLAY_TOGGLE}}"/>' +
-                        '{{get_string "autoplay" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_MUTE_TOGGLE}}"/>' +
-                        '{{get_string "mute" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_LOOP_TOGGLE}}"/>' +
-                        '{{get_string "loop" component}}' +
-                    '</label>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" checked="true" class="form-check-input {{CSS.MEDIA_CONTROLS_TOGGLE}}"' +
+                        'id="media-controls-toggle"/>' +
+                        '<label class="form-check-label" for="media-controls-toggle">{{get_string "controls" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_AUTOPLAY_TOGGLE}}"' +
+                        'id="media-autoplay-toggle"/>' +
+                        '<label class="form-check-label" for="media-autoplay-toggle">{{get_string "autoplay" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" id="media-mute-toggle"/>' +
+                        '<label class="form-check-label" for="media-mute-toggle">{{get_string "mute" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" id="media-loop-toggle"/>' +
+                        '<label class="form-check-label" for="media-loop-toggle">{{get_string "loop" component}}</label>' +
+                    '</div>' +
                 '</div>',
             TRACK_TABS: '' +
-                '<ul class="nav nav-tabs">' +
+                '<ul class="nav nav-tabs mb-3">' +
                     '<li data-track-kind="{{CSS.TRACK_SUBTITLES}}" class="nav-item">' +
                         '<a class="nav-link active" href="#{{elementid}}_{{id}}_{{CSS.TRACK_SUBTITLES}}"' +
                             ' role="tab" data-toggle="tab">' +
@@ -328,11 +330,11 @@ var COMPONENTNAME = 'atto_media',
                     '</div>' +
                 '</div>',
             TRACK: '' +
-                '<div class="{{CSS.TRACK}}">' +
+                '<div class="m-b-1 {{CSS.TRACK}}">' +
                     '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' +
-                    '<label class="langlabel">' +
-                        '<span>{{get_string "srclang" component}}</span>' +
-                        '<select class="{{CSS.TRACK_LANG_INPUT}}">' +
+                    '<div class="form-group">' +
+                        '<label class="w-100" for="lang-input">{{get_string "srclang" component}}</label>' +
+                        '<select id="lang-input" class="custom-select {{CSS.TRACK_LANG_INPUT}}">' +
                             '<optgroup label="{{get_string "languagesinstalled" component}}">' +
                                 '{{#langsinstalled}}' +
                                     '<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>' +
@@ -342,15 +344,15 @@ var COMPONENTNAME = 'atto_media',
                                 '{{#langsavailable}}<option value="{{code}}">{{lang}}</option>{{/langsavailable}}' +
                             '</optgroup>' +
                         '</select>' +
-                    '</label>' +
-                    '<label class="labellabel">' +
-                        '<span>{{get_string "label" component}}</span>' +
-                        '<input class="{{CSS.TRACK_LABEL_INPUT}}" type="text"/>' +
-                    '</label>' +
-                    '<label class="defaultlabel">' +
-                        '<input type="checkbox" class="{{CSS.TRACK_DEFAULT_SELECT}}"/>' +
-                        '{{get_string "default" component}}' +
-                    '</label>' +
+                    '</div>' +
+                    '<div class="form-group">' +
+                        '<label class="w-100" for="track-input">{{get_string "label" component}}</label>' +
+                        '<input id="track-input" class="form-control {{CSS.TRACK_LABEL_INPUT}}" type="text"/>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.TRACK_DEFAULT_SELECT}}"/>' +
+                        '<label class="form-check-label">{{get_string "default" component}}</label>' +
+                    '</div>' +
                     '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' +
                 '</div>'
         },
index fedd668..3565c9b 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js differ
index 4208609..54e9fb4 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js differ
index fedd668..3565c9b 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js differ
index 7c2abc4..e61cbec 100644 (file)
@@ -75,6 +75,7 @@ M.atto_recordrtc.audiomodule = {
                 // Empty the array containing the previously recorded chunks.
                 cm.chunks = [];
                 cm.blobSize = 0;
+                cm.uploadBtn.detach('click');
 
                 // Initialize common configurations.
                 var commonConfig = {
index 4069294..3dfb629 100644 (file)
@@ -74,6 +74,7 @@ M.atto_recordrtc.videomodule = {
                 // Empty the array containing the previously recorded chunks.
                 cm.chunks = [];
                 cm.blobSize = 0;
+                cm.uploadBtn.detach('click');
 
                 // Initialize common configurations.
                 var commonConfig = {
index ac4e854..1c68da3 100644 (file)
@@ -3,8 +3,8 @@ div.editor_atto_content th,
 div.editor_atto_content caption {
     border: 1px dashed #bbb;
     position: relative;
-    min-width: 30px;
-    height: 13px;
+    min-width: 2rem;
+    height: 2rem;
 }
 
 div.editor_atto_content caption {
@@ -12,30 +12,18 @@ div.editor_atto_content caption {
 }
 
 div.availablecolors {
-    max-width: 55%;
-    display: inline-block;
-    vertical-align: middle;
+    background-color: #eee;
+    padding: 0.5rem;
 }
-
-div.availablecolors label:not(.hideborder) {
-    border: 1px solid #ddd;
-}
-
-div.availablecolors label {
-    border-radius: 4px;
-    display: inline-block;
-    font-size: 0.1em;
-    padding: 2px;
-    padding-left: 22px;
-}
-
-div.availablecolors label input[type="radio"] {
-    float: none;
-    margin: 0;
-    margin-left: -15px;
-}
-
-input[name="bordersize"],
-input[name="width"] {
-    margin-right: 0.3em;
+div.availablecolors .tablebordercolor,
+div.availablecolors .tablebackgroundcolor {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 2rem;
+    height: 2rem;
+    text-align: center;
+    vertical-align: sub;
+    border-radius: 25%;
+    margin-right: 0.2rem;
 }
index 1083781..1f3e5d4 100644 (file)
@@ -86,13 +86,13 @@ Feature: Atto tables
     And I click on "Table" "button"
     When I click on "Edit table" "link"
     # Check that the background colour is set correctly.
-    Then the field with xpath "//label/input[@name='backgroundColour' and @value='#FFFFFF']" matches value "1"
+    Then the field with xpath "//div[@class='tablebackgroundcolor']/input[@name='backgroundColour' and @value='#FFFFFF']" matches value "1"
     And the field "Table width (in %)" matches value "100"
     And the field "Borders" matches value "Around table"
     And the field "Style of borders" matches value "dashed"
     And the field "Size of borders" matches value "2"
     # Check that the border colour is set correctly.
-    And the field with xpath "//label/input[@name='borderColour' and @value='#FFFFFF']" matches value "1"
+    And the field with xpath "//div[@class='tablebordercolor']/input[@name='borderColour' and @value='#FFFFFF']" matches value "1"
 
   @javascript
   Scenario: Create a table with background colour and width with border settings off
index c8db3c5..8624a7b 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js differ
index 5b9e915..3039fcd 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js differ
index c8db3c5..8624a7b 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js differ
index f33bc87..5a36fa9 100644 (file)
@@ -41,132 +41,175 @@ var COMPONENT = 'atto_table',
     },
     TEMPLATE = '' +
         '<form class="{{CSS.FORM}}">' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
-            '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
-            '<br/>' +
-            '<br/>' +
-            '<label for="{{elementid}}_atto_table_captionposition" class="sameline">' +
+            '</div><div class="col-sm-8 span8">' +
+            '<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +
+            '</div>' +
+            '</div>' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
+            '<label for="{{elementid}}_atto_table_captionposition">' +
             '{{get_string "captionposition" component}}</label>' +
-            '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
+            '</div><div class="col-sm-8 span8">' +
+            '<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
                 '<option value=""></option>' +
                 '<option value="top">{{get_string "top" "editor"}}</option>' +
                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
             '</select>' +
-            '<br/>' +
-            '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
-            '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
+            '</div>' +
+            '</div>' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
+            '<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +
+            '</div><div class="col-sm-8 span8">' +
+            '<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
                 '<option value="both">{{get_string "both" component}}' + '</option>' +
             '</select>' +
-            '<br/>' +
+            '</div>' +
+            '</div>' +
             '{{#if nonedit}}' +
-                '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
-                '<input class="{{CSS.ROWS}}" type="number" value="3" ' +
+                '<div class="m-b-1 form-group row-fluid">' +
+                '<div class="col-sm-4 span4">' +
+                '<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +
+                '</div><div class="col-sm-8 span8">' +
+                '<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +
                 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
+                '<div class="m-b-1 form-group row-fluid">' +
+                '<div class="col-sm-4 span4">' +
                 '<label for="{{elementid}}_atto_table_columns" ' +
-                'class="sameline">{{get_string "numberofcolumns" component}}</label>' +
-                '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns"' +
+                '>{{get_string "numberofcolumns" component}}</label>' +
+                '</div><div class="col-sm-8 span8">' +
+                '<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +
+                    'id="{{elementid}}_atto_table_columns"' +
                 'size="8" min="1" max="20"/>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
             '{{/if}}' +
             '{{#if allowStyling}}' +
                 '<fieldset>' +
                 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
                 '{{#if allowBorders}}' +
-                    '<label for="{{elementid}}_atto_table_borders" class="sameline">{{get_string "borders" component}}</label>' +
-                    '<select name="borders" class="{{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
                         '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
                         '<option value="outer">{{get_string "outer" component}}' + '</option>' +
                         '<option value="all">{{get_string "all" component}}' + '</option>' +
                     '</select>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_borderstyle" class="sameline">' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_borderstyle">' +
                     '{{get_string "borderstyles" component}}</label>' +
-                    '<select name="borderstyles" class="{{CSS.BORDERSTYLE}}" id="{{elementid}}_atto_table_borderstyle">' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +
+                        'id="{{elementid}}_atto_table_borderstyle">' +
                         '{{#each borderStyles}}' +
                             '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
                         '{{/each}}' +
                     '</select>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_bordersize" class="sameline">' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_bordersize">' +
                     '{{get_string "bordersize" component}}</label>' +
-                    '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" class="{{CSS.BORDERSIZE}}"' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<div class="form-inline">' +
+                    '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +
+                    'class="form-control w-auto m-r-1 {{CSS.BORDERSIZE}}"' +
                     'type="number" value="1" size="8" min="1" max="50"/>' +
-                    '<label style="display: inline-block;">{{CSS.BORDERSIZEUNIT}}</label>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_bordercolour" class="sameline">' +
+                    '<label>{{CSS.BORDERSIZEUNIT}}</label>' +
+                    '</div>' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_bordercolour">' +
                     '{{get_string "bordercolour" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
                     '<div id="{{elementid}}_atto_table_bordercolour"' +
-                    'class="{{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
-                        '<label class="hideborder" for="{{../elementid}}_atto_table_bordercolour_-1"' +
-                        'style="background-color:transparent;color:transparent">' +
-
+                    'class="form-inline {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
+                        '<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +
                             '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
-                            'type="radio" name="borderColour" value="none" checked="checked"' +
+                            'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +
                             'title="{{get_string "themedefault" component}}"></input>' +
-
-                            '{{get_string "themedefault" component}}' +
-                        '</label>' +
+                            '<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +
+                            '{{get_string "themedefault" component}}</label>' +
+                        '</div>' +
                         '{{#each availableColours}}' +
-                            '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
-                            'style="background-color:{{this}};color:{{this}}">' +
-
+                            '<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +
                                 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
-                                'type="radio" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
-
-                                '{{this}}' +
-                            '</label>' +
+                                'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
+                                '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +
+                                '{{this}}</label>' +
+                            '</div>' +
                         '{{/each}}' +
                     '</div>' +
-                    '<br>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '{{#if allowBackgroundColour}}' +
-                    '<label for="{{elementid}}_atto_table_backgroundcolour" class="sameline">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_backgroundcolour">' +
                     '{{get_string "backgroundcolour" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
                     '<div id="{{elementid}}_atto_table_backgroundcolour"' +
-                    'class="{{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
-                        '<label class="hideborder" for="{{../elementid}}_atto_table_backgroundcolour_-1"' +
-                        'style="background-color:transparent;color:transparent">' +
-
+                    'class="form-inline {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
+                        '<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +
                             '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
-                            'type="radio" name="backgroundColour" value="none" checked="checked"' +
+                            'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +
                             'title="{{get_string "themedefault" component}}"></input>' +
-
-                            '{{get_string "themedefault" component}}' +
-                        '</label>' +
+                            '<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +
+                            '{{get_string "themedefault" component}}</label>' +
+                        '</div>' +
 
                         '{{#each availableColours}}' +
-                            '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
-                            'style="background-color:{{this}};color:{{this}}">' +
-
+                            '<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +
                                 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
-                                'type="radio" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
-
-                                '{{this}}' +
-                            '</label>' +
+                                'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
+                                '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +
+                                '{{this}}</label>' +
+                            '</div>' +
                         '{{/each}}' +
                     '</div>' +
-                    '<br>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '{{#if allowWidth}}' +
-                    '<label for="{{elementid}}_atto_table_width" class="sameline">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_width">' +
                     '{{get_string "width" component}}</label>' +
-                    '<input name="width" id="{{elementid}}_atto_table_width" class="{{CSS.WIDTH}}" size="8" ' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<div class="form-inline">' +
+                    '<input name="width" id="{{elementid}}_atto_table_width" ' +
+                        'class="form-control w-auto m-r-1 {{CSS.WIDTH}}" size="8" ' +
                         'type="number" min="0" max="100"/>' +
-                    '<label style="display: inline-block;">{{CSS.WIDTHUNIT}}</label>' +
-                    '<br>' +
+                    '<label>{{CSS.WIDTHUNIT}}</label>' +
+                    '</div>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '</fieldset>' +
             '{{/if}}' +
             '<div class="mdl-align">' +
             '<br/>' +
             '{{#if edit}}' +
-                '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
+                '<button class="btn btn-default submit" type="submit">{{get_string "updatetable" component}}</button>' +
             '{{/if}}' +
             '{{#if nonedit}}' +
-                '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
+                '<button class="btn btn-default submit" type="submit">{{get_string "createtable" component}}</button>' +
             '{{/if}}' +
             '</div>' +
         '</form>',
index 84c777e..f2b74e1 100644 (file)
@@ -107,10 +107,10 @@ div.editor_atto_toolbar div.atto_group {
     margin: 4px;
 }
 
-.atto_form label.sameline {
+/*.atto_form label.sameline {
     display: inline-block;
     min-width: 10em;
-}
+}*/
 
 .atto_form textarea.fullwidth,
 .atto_form input.fullwidth {
@@ -118,14 +118,13 @@ div.editor_atto_toolbar div.atto_group {
 }
 
 .atto_form {
-    padding-left: 30px;
-    padding-right: 30px;
+    padding: 0.5rem;
 }
 
-.atto_form label {
+/*.atto_form label {
     display: block;
     margin: 0 0 5px 0;
-}
+}*/
 
 .atto_control {
     position: absolute;
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 aa2e866..8753695 100644 (file)
--- a/