Merge branch 'MDL-62670-master' of git://github.com/sarjona/moodle
authorJake Dallimore <jake@moodle.com>
Thu, 5 Jul 2018 01:25:41 +0000 (09:25 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 5 Jul 2018 01:25:41 +0000 (09:25 +0800)
159 files changed:
.stylelintignore
Gruntfile.js
admin/classes/form/purge_caches.php [new file with mode: 0644]
admin/cli/purge_caches.php
admin/purgecaches.php
admin/settings/development.php
admin/tests/behat/purge_caches.feature [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/request_filter.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/request_filter.js [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/output/data_deletion_page.php
admin/tool/dataprivacy/classes/output/data_requests_page.php
admin/tool/dataprivacy/classes/output/data_requests_table.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/request_filter.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/privacy/provider.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/data_requests.mustache
admin/tool/dataprivacy/templates/request_filter.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/log/classes/local/privacy/helper.php
admin/tool/log/classes/privacy/provider.php
admin/tool/log/lang/en/tool_log.php
admin/tool/log/settings.php
admin/tool/log/version.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/tests/behat/consent.feature
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/tests/course_test.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/lang/en/auth_oauth2.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/recent_activity/lang/en/block_recent_activity.php
calendar/classes/external/event_exporter_base.php
calendar/classes/local/event/entities/action_event.php
calendar/classes/local/event/entities/event.php
calendar/classes/local/event/entities/event_interface.php
calendar/classes/local/event/factories/event_abstract_factory.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/mappers/event_mapper.php
calendar/classes/privacy/provider.php
calendar/export_execute.php
calendar/lib.php
calendar/templates/event_item.mustache
calendar/templates/event_summary_body.mustache
calendar/tests/action_event_test.php
calendar/tests/behat/calendar.feature
calendar/tests/behat/calendar_import.feature
calendar/tests/container_test.php
calendar/tests/event_factory_test.php
calendar/tests/event_mapper_test.php
calendar/tests/event_test.php
calendar/tests/externallib_test.php
calendar/tests/fixtures/import.ics
calendar/tests/helpers.php
calendar/tests/lib_test.php
calendar/tests/local_api_test.php
calendar/tests/raw_event_retrieval_strategy_test.php
calendar/tests/repeat_event_collection_test.php
course/classes/management_renderer.php
course/classes/search/section.php
course/externallib.php
course/format/lib.php
course/format/upgrade.txt
course/reset_form.php
course/tests/externallib_test.php
dataformat/json/lang/en/dataformat_json.php
dataformat/ods/lang/en/dataformat_ods.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/paypal/lang/en/enrol_paypal.php
filter/mathjaxloader/filter.php
lang/en/admin.php
lang/en/analytics.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/completion.php
lang/en/hub.php
lang/en/moodle.php
lang/en/notes.php
lang/en/role.php
lang/en/search.php
lang/en/user.php
lang/en/userkey.php
lang/en/webservice.php
lib/classes/hub/registration.php
lib/classes/hub/site_registration_form.php
lib/classes/output/icon_system_fontawesome.php
lib/db/install.xml
lib/db/upgrade.php
lib/filterlib.php
lib/moodlelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/behat/behat_filters.php [new file with mode: 0644]
lib/tests/filterlib_test.php
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/feedback/comments/lang/en/assignfeedback_comments.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/lang/en/assign.php
mod/assign/submission/onlinetext/lang/en/assignsubmission_onlinetext.php
mod/assign/tests/generator.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/lang/en/data.php
mod/feedback/lang/en/feedback.php
mod/forum/lang/en/forum.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/tests/behat/categories.feature
mod/lesson/format.php
mod/lesson/renderer.php
mod/lesson/tests/behat/lesson_complete_report.feature
mod/lesson/tests/behat/lesson_delete_answers.feature
mod/lesson/tests/behat/lesson_informations_at_end.feature
mod/lesson/tests/behat/lesson_number_of_student_attempts.feature
mod/lesson/tests/behat/lesson_outline_report.feature
mod/lesson/tests/behat/lesson_question_attempts.feature
mod/lesson/tests/behat/lesson_report.feature
mod/lesson/tests/behat/lesson_review.feature
mod/lesson/tests/behat/lesson_student_dashboard.feature
mod/lesson/tests/behat/lesson_student_resume.feature
mod/lesson/tests/behat/questions_images.feature
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/quiz/lang/en/quiz.php
mod/quiz/report/grading/lang/en/quiz_grading.php
mod/scorm/lang/en/scorm.php
mod/workshop/lang/en/workshop.php
npm-shrinkwrap.json
package.json
pix/i/location.png [new file with mode: 0644]
pix/i/location.svg [new file with mode: 0644]
question/format.php
report/stats/lang/en/report_stats.php
repository/wikimedia/lang/en/repository_wikimedia.php
repository/youtube/lang/en/repository_youtube.php
search/engine/simpledb/lang/en/search_simpledb.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/config.php
theme/boost/lib.php
theme/boost/readme_moodle.txt
theme/boost/scss/moodle/calendar.scss
theme/boost/style/moodle.css [new file with mode: 0644]
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/event_item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core_calendar/event_summary_body.mustache
theme/styles.php
version.php

index 6368112..b0dc0dc 100644 (file)
@@ -2,6 +2,7 @@
 theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
+theme/boost/style/moodle.css
 node_modules/
 vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
index e0afa2d..4501fc6 100644 (file)
@@ -137,6 +137,16 @@ module.exports = function(grunt) {
                 }
            }
         },
+        sass: {
+            dist: {
+                files: {
+                    "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss"
+                }
+            },
+            options: {
+                includePaths: ["theme/boost/scss/"]
+            }
+        },
         watch: {
             options: {
                 nospawn: true // We need not to spawn so config can be changed dynamically.
@@ -214,7 +224,8 @@ module.exports = function(grunt) {
           '# Generated by "grunt ignorefiles"',
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
-          'theme/more/style/custom.css'
+          'theme/more/style/custom.css',
+          'theme/boost/style/moodle.css'
       ].concat(thirdPartyPaths);
       grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
     };
@@ -362,6 +373,7 @@ module.exports = function(grunt) {
     grunt.loadNpmTasks('grunt-contrib-uglify');
     grunt.loadNpmTasks('grunt-contrib-less');
     grunt.loadNpmTasks('grunt-contrib-watch');
+    grunt.loadNpmTasks('grunt-sass');
     grunt.loadNpmTasks('grunt-eslint');
     grunt.loadNpmTasks('grunt-stylelint');
 
@@ -374,7 +386,7 @@ module.exports = function(grunt) {
     grunt.registerTask('js', ['amd', 'yui']);
 
     // Register CSS taks.
-    grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
+    grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']);
 
     // Register the startup task.
     grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
diff --git a/admin/classes/form/purge_caches.php b/admin/classes/form/purge_caches.php
new file mode 100644 (file)
index 0000000..3151445
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form for selective purging of caches.
+ *
+ * @package    core
+ * @copyright  2018 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form for selecting which caches to purge on admin/purgecaches.php
+ *
+ * @package   core
+ * @copyright 2018 The Open University
+ * @author    Mark Johnson <mark.johnson@open.ac.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class purge_caches extends \moodleform {
+    /**
+     * Define a "Purge all caches" button, and a fieldset with checkboxes for selectively purging separate caches.
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
+        $mform->setType('returnurl', PARAM_LOCALURL);
+        $mform->addElement('submit', 'all', get_string('purgecaches', 'admin'));
+        $mform->addElement('header', 'purgecacheheader', get_string('purgeselectedcaches', 'admin'));
+        $checkboxes = [
+            $mform->createElement('advcheckbox', 'theme', '', get_string('purgethemecache', 'admin')),
+            $mform->createElement('advcheckbox', 'lang', '', get_string('purgelangcache', 'admin')),
+            $mform->createElement('advcheckbox', 'js', '', get_string('purgejscache', 'admin')),
+            $mform->createElement('advcheckbox', 'filter', '', get_string('purgefiltercache', 'admin')),
+            $mform->createElement('advcheckbox', 'muc', '', get_string('purgemuc', 'admin')),
+            $mform->createElement('advcheckbox', 'other', '', get_string('purgeothercaches', 'admin'))
+        ];
+        $mform->addGroup($checkboxes, 'purgeselectedoptions');
+        $mform->addElement('submit', 'purgeselectedcaches', get_string('purgeselectedcaches', 'admin'));
+    }
+
+    /**
+     * If the "Purge selected caches" button was pressed, ensure at least one cache was selected.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array Error messages
+     */
+    public function validation($data, $files) {
+        $errors = [];
+        if (isset($data['purgeselectedcaches']) && empty(array_filter($data['purgeselectedoptions']))) {
+            $errors['purgeselectedoptions'] = get_string('purgecachesnoneselected', 'admin');
+        }
+        return $errors;
+    }
+}
index 64f29bd..9846b20 100644 (file)
@@ -28,7 +28,16 @@ define('CLI_SCRIPT', true);
 require(__DIR__.'/../../config.php');
 require_once($CFG->libdir.'/clilib.php');
 
-list($options, $unrecognized) = cli_get_params(array('help' => false), array('h' => 'help'));
+$longoptions = [
+    'help' => false,
+    'muc' => false,
+    'theme' => false,
+    'lang' => false,
+    'js' => false,
+    'filter' => false,
+    'other' => false
+];
+list($options, $unrecognized) = cli_get_params($longoptions, ['h' => 'help']);
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -36,20 +45,32 @@ if ($unrecognized) {
 }
 
 if ($options['help']) {
-    $help =
-"Invalidates all Moodle internal caches
+    // The indentation of this string is "wrong" but this is to avoid a extra whitespace in console output.
+    $help = <<<EOF
+Invalidates Moodle internal caches
+
+Specific caches can be defined (alone or in combination) using arguments. If none are specified,
+all caches will be purged.
 
 Options:
 -h, --help            Print out this help
+    --muc             Purge all MUC caches (includes lang cache)
+    --theme           Purge theme cache
+    --lang            Purge language string cache
+    --js              Purge JavaScript cache
+    --filter          Purge text filter cache
+    --other           Purge all file caches and other miscellaneous caches (may include MUC
+                      if using cachestore_file).
 
 Example:
-\$sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
-";
+\$ sudo -u www-data /usr/bin/php admin/cli/purge_caches.php
+
+EOF;
 
     echo $help;
     exit(0);
 }
 
-purge_all_caches();
+purge_caches(array_filter($options));
 
 exit(0);
index dd47087..b4401c4 100644 (file)
@@ -27,36 +27,43 @@ require_once('../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
 $confirm = optional_param('confirm', 0, PARAM_BOOL);
-$returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
+$returnurl = optional_param('returnurl', '/admin/purgecaches.php', PARAM_LOCALURL);
+$returnurl = new moodle_url($returnurl);
 
 admin_externalpage_setup('purgecaches');
 
+$form = new core_admin\form\purge_caches(null, ['returnurl' => $returnurl]);
+
 // If we have got here as a confirmed aciton, do it.
-if ($confirm && confirm_sesskey()) {
+if ($data = $form->get_data()) {
 
     // Valid request. Purge, and redirect the user back to where they came from.
-    purge_all_caches();
+    $selected = $data->purgeselectedoptions;
+    purge_caches($selected);
 
-    if ($returnurl) {
-        $returnurl = $CFG->wwwroot . $returnurl;
+    if (isset($data->all)) {
+        $message = get_string('purgecachesfinished', 'admin');
     } else {
-        $returnurl = new moodle_url('/admin/purgecaches.php');
+        $message = get_string('purgeselectedcachesfinished', 'admin');
     }
-    redirect($returnurl, get_string('purgecachesfinished', 'admin'));
+
+} else if ($confirm && confirm_sesskey()) {
+    purge_caches();
+    $message = get_string('purgecachesfinished', 'admin');
 }
 
-// Otherwise, show a button to actually purge the caches.
-$actionurl = new moodle_url('/admin/purgecaches.php', array('sesskey'=>sesskey(), 'confirm'=>1));
-if ($returnurl) {
-    $actionurl->param('returnurl', $returnurl);
+if (isset($message)) {
+    redirect($returnurl, $message);
 }
 
+// Otherwise, show a form to actually purge the caches.
+
 echo $OUTPUT->header();
-echo $OUTPUT->heading(get_string('purgecaches', 'admin'));
+echo $OUTPUT->heading(get_string('purgecachespage', 'admin'));
 
 echo $OUTPUT->box_start('generalbox', 'notice');
 echo html_writer::tag('p', get_string('purgecachesconfirm', 'admin'));
-echo $OUTPUT->single_button($actionurl, get_string('purgecaches', 'admin'), 'post');
+echo $form->render();
 echo $OUTPUT->box_end();
 
 echo $OUTPUT->footer();
index f8dab27..cc2a620 100644 (file)
@@ -83,7 +83,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         $ADMIN->add('development', new admin_externalpage('mnettestclient', new lang_string('testclient', 'mnet'), "$CFG->wwwroot/$CFG->admin/mnet/testclient.php"));
     }
 
-    $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecaches','admin'), "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
+    $ADMIN->add('development', new admin_externalpage('purgecaches', new lang_string('purgecachespage', 'admin'),
+            "$CFG->wwwroot/$CFG->admin/purgecaches.php"));
 
     $ADMIN->add('development', new admin_externalpage('thirdpartylibs', new lang_string('thirdpartylibs','admin'), "$CFG->wwwroot/$CFG->admin/thirdpartylibs.php"));
 } // end of speedup
diff --git a/admin/tests/behat/purge_caches.feature b/admin/tests/behat/purge_caches.feature
new file mode 100644 (file)
index 0000000..7f1e805
--- /dev/null
@@ -0,0 +1,34 @@
+@core @core_admin
+Feature: Purge caches
+  In order to see changes to cached data
+  As a Moodle administrator
+  I want manually purge different data and file caches
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Development > Purge caches" in site administration
+
+  Scenario: Purge all caches
+    Given I should not see "All caches were purged"
+    When I press "Purge all caches"
+    Then I should see "All caches were purged"
+
+  Scenario: Purge selected caches
+    Given I should not see "Selected caches were purged"
+    When I set the field "Themes" to "1"
+    And I press "Purge selected caches"
+    Then I should see "The selected caches were purged"
+
+  Scenario: Purge selected caches without selecting any caches
+    Given I should not see "Select one or more caches to purge"
+    When I press "Purge selected caches"
+    Then I should not see "The selected caches were purged"
+    And I should see "Select one or more caches to purge"
+
+  Scenario: Redirect back to the original page after following a Purge all caches link
+    Given I am on site homepage
+    And I should see "Available courses"
+    And I should not see "All caches were purged"
+    When I follow "Purge all caches"
+    Then I should see "All caches were purged"
+    And I should see "Available courses"
diff --git a/admin/tool/dataprivacy/amd/build/request_filter.min.js b/admin/tool/dataprivacy/amd/build/request_filter.min.js
new file mode 100644 (file)
index 0000000..61344eb
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/request_filter.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/request_filter.js b/admin/tool/dataprivacy/amd/src/request_filter.js
new file mode 100644 (file)
index 0000000..6b915c8
--- /dev/null
@@ -0,0 +1,84 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * JS module for the data requests filter.
+ *
+ * @module     tool_dataprivacy/request_filter
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
+
+    /**
+     * Selectors.
+     *
+     * @access private
+     * @type {{REQUEST_FILTERS: string}}
+     */
+    var SELECTORS = {
+        REQUEST_FILTERS: '#request-filters'
+    };
+
+    /**
+     * Init function.
+     *
+     * @method init
+     * @private
+     */
+    var init = function() {
+        var stringkeys = [
+            {
+                key: 'filter',
+                component: 'moodle'
+            },
+            {
+                key: 'nofiltersapplied',
+                component: 'moodle'
+            }
+        ];
+
+        Str.get_strings(stringkeys).then(function(langstrings) {
+            var placeholder = langstrings[0];
+            var noSelectionString = langstrings[1];
+            return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
+        }).fail(Notification.exception);
+
+        var last = $(SELECTORS.REQUEST_FILTERS).val();
+        $(SELECTORS.REQUEST_FILTERS).on('change', function() {
+            var current = $(this).val();
+            // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
+            if (last.join(',') !== current.join(',')) {
+                // If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
+                if (current.length === 0) {
+                    $('#filters-cleared').val(1);
+                }
+                $(this.form).submit();
+            }
+        });
+    };
+
+    return /** @alias module:core/form-autocomplete */ {
+        /**
+         * Initialise the unified user filter.
+         *
+         * @method init
+         */
+        init: function() {
+            init();
+        }
+    };
+});
index 275044a..42acb42 100644 (file)
@@ -232,16 +232,42 @@ class api {
      * (e.g. Users with the Data Protection Officer roles)
      *
      * @param int $userid The User ID.
+     * @param int[] $statuses The status filters.
+     * @param int[] $types The request type filters.
+     * @param string $sort The order by clause.
+     * @param int $offset Amount of records to skip.
+     * @param int $limit Amount of records to fetch.
      * @return data_request[]
+     * @throws coding_exception
      * @throws dml_exception
      */
-    public static function get_data_requests($userid = 0) {
+    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
         global $DB, $USER;
         $results = [];
-        $sort = 'status ASC, timemodified ASC';
+        $sqlparams = [];
+        $sqlconditions = [];
+
+        // Set default sort.
+        if (empty($sort)) {
+            $sort = 'status ASC, timemodified ASC';
+        }
+
+        // Set status filters.
+        if (!empty($statuses)) {
+            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "status $statusinsql";
+        }
+
+        // Set request type filter.
+        if (!empty($types)) {
+            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "type $typeinsql";
+            $sqlparams = array_merge($sqlparams, $typeparams);
+        }
+
         if ($userid) {
             // Get the data requests for the user or data requests made by the user.
-            $select = "(userid = :userid OR requestedby = :requestedby)";
+            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
             $params = [
                 'userid' => $userid,
                 'requestedby' => $userid
@@ -256,20 +282,87 @@ class api {
                 $alloweduserids = array_merge($alloweduserids, array_keys($children));
             }
             list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
-            $select .= " AND userid $insql";
-            $params = array_merge($params, $inparams);
+            $sqlconditions[] .= "userid $insql";
+            $select = implode(' AND ', $sqlconditions);
+            $params = array_merge($params, $inparams, $sqlparams);
 
-            $results = data_request::get_records_select($select, $params, $sort);
+            $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
         } else {
             // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
             if (self::is_site_dpo($USER->id)) {
-                $results = data_request::get_records(null, $sort, '');
+                if (!empty($sqlconditions)) {
+                    $select = implode(' AND ', $sqlconditions);
+                    $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
+                } else {
+                    $results = data_request::get_records(null, $sort, '', $offset, $limit);
+                }
             }
         }
 
         return $results;
     }
 
+    /**
+     * Fetches the count of data request records based on the given parameters.
+     *
+     * @param int $userid The User ID.
+     * @param int[] $statuses The status filters.
+     * @param int[] $types The request type filters.
+     * @return int
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
+        global $DB, $USER;
+        $count = 0;
+        $sqlparams = [];
+        $sqlconditions = [];
+        if (!empty($statuses)) {
+            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "status $statusinsql";
+        }
+        if (!empty($types)) {
+            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+            $sqlconditions[] = "type $typeinsql";
+            $sqlparams = array_merge($sqlparams, $typeparams);
+        }
+        if ($userid) {
+            // Get the data requests for the user or data requests made by the user.
+            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
+            $params = [
+                'userid' => $userid,
+                'requestedby' => $userid
+            ];
+
+            // Build a list of user IDs that the user is allowed to make data requests for.
+            // Of course, the user should be included in this list.
+            $alloweduserids = [$userid];
+            // Get any users that the user can make data requests for.
+            if ($children = helper::get_children_of_user($userid)) {
+                // Get the list of user IDs of the children and merge to the allowed user IDs.
+                $alloweduserids = array_merge($alloweduserids, array_keys($children));
+            }
+            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
+            $sqlconditions[] .= "userid $insql";
+            $select = implode(' AND ', $sqlconditions);
+            $params = array_merge($params, $inparams, $sqlparams);
+
+            $count = data_request::count_records_select($select, $params);
+        } else {
+            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
+            if (self::is_site_dpo($USER->id)) {
+                if (!empty($sqlconditions)) {
+                    $select = implode(' AND ', $sqlconditions);
+                    $count = data_request::count_records_select($select, $sqlparams);
+                } else {
+                    $count = data_request::count_records();
+                }
+            }
+        }
+
+        return $count;
+    }
+
     /**
      * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
      *
index c68c994..f98362d 100644 (file)
@@ -35,6 +35,17 @@ use tool_dataprivacy\api;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class helper {
+    /** The default number of results to be shown per page. */
+    const DEFAULT_PAGE_SIZE = 20;
+
+    /** Filter constant associated with the request type filter. */
+    const FILTER_TYPE = 1;
+
+    /** Filter constant associated with the request status filter. */
+    const FILTER_STATUS = 2;
+
+    /** The request filters preference key. */
+    const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
 
     /**
      * Retrieves the human-readable text value of a data request type.
@@ -45,16 +56,11 @@ class helper {
      * @throws moodle_exception
      */
     public static function get_request_type_string($requesttype) {
-        switch ($requesttype) {
-            case api::DATAREQUEST_TYPE_EXPORT:
-                return get_string('requesttypeexport', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_DELETE:
-                return get_string('requesttypedelete', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_OTHERS:
-                return get_string('requesttypeothers', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+        $types = self::get_request_types();
+        if (!isset($types[$requesttype])) {
+            throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
         }
+        return $types[$requesttype];
     }
 
     /**
@@ -66,16 +72,37 @@ class helper {
      * @throws moodle_exception
      */
     public static function get_shortened_request_type_string($requesttype) {
-        switch ($requesttype) {
-            case api::DATAREQUEST_TYPE_EXPORT:
-                return get_string('requesttypeexportshort', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_DELETE:
-                return get_string('requesttypedeleteshort', 'tool_dataprivacy');
-            case api::DATAREQUEST_TYPE_OTHERS:
-                return get_string('requesttypeothersshort', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+        $types = self::get_request_types_short();
+        if (!isset($types[$requesttype])) {
+            throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
         }
+        return $types[$requesttype];
+    }
+
+    /**
+     * Returns the key value-pairs of request type code and their string value.
+     *
+     * @return array
+     */
+    public static function get_request_types() {
+        return [
+            api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
+        ];
+    }
+
+    /**
+     * Returns the key value-pairs of request type code and their shortened string value.
+     *
+     * @return array
+     */
+    public static function get_request_types_short() {
+        return [
+            api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
+            api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
+        ];
     }
 
     /**
@@ -83,30 +110,32 @@ class helper {
      *
      * @param int $status The request status.
      * @return string
-     * @throws coding_exception
      * @throws moodle_exception
      */
     public static function get_request_status_string($status) {
-        switch ($status) {
-            case api::DATAREQUEST_STATUS_PENDING:
-                return get_string('statuspending', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_PREPROCESSING:
-                return get_string('statuspreprocessing', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
-                return get_string('statusawaitingapproval', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_APPROVED:
-                return get_string('statusapproved', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_PROCESSING:
-                return get_string('statusprocessing', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_COMPLETE:
-                return get_string('statuscomplete', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_CANCELLED:
-                return get_string('statuscancelled', 'tool_dataprivacy');
-            case api::DATAREQUEST_STATUS_REJECTED:
-                return get_string('statusrejected', 'tool_dataprivacy');
-            default:
-                throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
+        $statuses = self::get_request_statuses();
+        if (!isset($statuses[$status])) {
+            throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
         }
+        return $statuses[$status];
+    }
+
+    /**
+     * Returns the key value-pairs of request status code and string value.
+     *
+     * @return array
+     */
+    public static function get_request_statuses() {
+        return [
+            api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
+            api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+        ];
     }
 
     /**
@@ -146,4 +175,34 @@ class helper {
         }
         return $finalresults;
     }
+
+    /**
+     * Get options for the data requests filter.
+     *
+     * @return array
+     * @throws coding_exception
+     */
+    public static function get_request_filter_options() {
+        $filters = [
+            self::FILTER_TYPE => (object)[
+                'name' => get_string('requesttype', 'tool_dataprivacy'),
+                'options' => self::get_request_types_short()
+            ],
+            self::FILTER_STATUS => (object)[
+                'name' => get_string('requeststatus', 'tool_dataprivacy'),
+                'options' => self::get_request_statuses()
+            ],
+        ];
+        $options = [];
+        foreach ($filters as $category => $filtercategory) {
+            foreach ($filtercategory->options as $key => $name) {
+                $option = (object)[
+                    'category' => $filtercategory->name,
+                    'name' => $name
+                ];
+                $options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
+            }
+        }
+        return $options;
+    }
 }
index c1a2202..444569e 100644 (file)
@@ -25,7 +25,6 @@ namespace tool_dataprivacy\output;
 defined('MOODLE_INTERNAL') || die();
 
 use coding_exception;
-use dml_exception;
 use moodle_exception;
 use moodle_url;
 use renderable;
@@ -34,7 +33,7 @@ use single_select;
 use stdClass;
 use templatable;
 use tool_dataprivacy\data_request;
-use tool_dataprivacy\output\expired_contexts_table;
+use tool_dataprivacy\local\helper;
 
 /**
  * Class containing data for a user's data requests.
@@ -44,9 +43,6 @@ use tool_dataprivacy\output\expired_contexts_table;
  */
 class data_deletion_page implements renderable, templatable {
 
-    /** The default number of results to be shown per page. */
-    const DEFAULT_PAGE_SIZE = 20;
-
     /** @var data_request[] $requests List of data requests. */
     protected $filter = null;
 
@@ -57,7 +53,7 @@ class data_deletion_page implements renderable, templatable {
      * Construct this renderable.
      *
      * @param \tool_dataprivacy\data_request[] $filter
-     * @param \tool_dataprivacy\expired_contexts_table $expiredcontextstable
+     * @param expired_contexts_table $expiredcontextstable
      */
     public function __construct($filter, expired_contexts_table $expiredcontextstable) {
         $this->filter = $filter;
@@ -70,7 +66,6 @@ class data_deletion_page implements renderable, templatable {
      * @param renderer_base $output
      * @return stdClass
      * @throws coding_exception
-     * @throws dml_exception
      * @throws moodle_exception
      */
     public function export_for_template(renderer_base $output) {
@@ -87,7 +82,7 @@ class data_deletion_page implements renderable, templatable {
         $data->filter = $filterselector->export_for_template($output);
 
         ob_start();
-        $this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true);
+        $this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true);
         $expiredcontexts = ob_get_contents();
         ob_end_clean();
         $data->expiredcontexts = $expiredcontexts;
index c1b2861..7ea4bf8 100644 (file)
 namespace tool_dataprivacy\output;
 defined('MOODLE_INTERNAL') || die();
 
-use action_menu;
-use action_menu_link_secondary;
 use coding_exception;
-use context_system;
 use dml_exception;
 use moodle_exception;
 use moodle_url;
 use renderable;
 use renderer_base;
+use single_select;
 use stdClass;
 use templatable;
 use tool_dataprivacy\api;
-use tool_dataprivacy\data_request;
-use tool_dataprivacy\external\data_request_exporter;
+use tool_dataprivacy\local\helper;
 
 /**
  * Class containing data for a user's data requests.
@@ -47,16 +44,21 @@ use tool_dataprivacy\external\data_request_exporter;
  */
 class data_requests_page implements renderable, templatable {
 
-    /** @var data_request[] $requests List of data requests. */
-    protected $requests = [];
+    /** @var data_requests_table $table The data requests table. */
+    protected $table;
+
+    /** @var int[] $filters The applied filters. */
+    protected $filters = [];
 
     /**
      * Construct this renderable.
      *
-     * @param data_request[] $requests
+     * @param data_requests_table $table The data requests table.
+     * @param int[] $filters The applied filters.
      */
-    public function __construct($requests) {
-        $this->requests = $requests;
+    public function __construct($table, $filters) {
+        $this->table = $table;
+        $this->filters = $filters;
     }
 
     /**
@@ -78,43 +80,17 @@ class data_requests_page implements renderable, templatable {
             $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
         }
 
-        $requests = [];
-        foreach ($this->requests as $request) {
-            $requestid = $request->get('id');
-            $status = $request->get('status');
-            $requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]);
-            $item = $requestexporter->export($output);
-
-            // Prepare actions.
-            $actions = [];
-
-            // View action.
-            $actionurl = new moodle_url('#');
-            $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
-            $actiontext = get_string('viewrequest', 'tool_dataprivacy');
-            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+        $filteroptions = helper::get_request_filter_options();
+        $filter = new request_filter($filteroptions, $this->filters, $url);
+        $data->filter = $filter->export_for_template($output);
 
-            if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
-                // Approve.
-                $actiondata['data-action'] = 'approve';
-                $actiontext = get_string('approverequest', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        ob_start();
+        $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+        $requests = ob_get_contents();
+        ob_end_clean();
 
-                // Deny.
-                $actiondata['data-action'] = 'deny';
-                $actiontext = get_string('denyrequest', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
-            }
-
-            $actionsmenu = new action_menu($actions);
-            $actionsmenu->set_menu_trigger(get_string('actions'));
-            $actionsmenu->set_owner_selector('request-actions-' . $requestid);
-            $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
-            $item->actions = $actionsmenu->export_for_template($output);
-
-            $requests[] = $item;
-        }
-        $data->requests = $requests;
+        $data->datarequests = $requests;
         return $data;
     }
 }
diff --git a/admin/tool/dataprivacy/classes/output/data_requests_table.php b/admin/tool/dataprivacy/classes/output/data_requests_table.php
new file mode 100644 (file)
index 0000000..97918d9
--- /dev/null
@@ -0,0 +1,262 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the class used for the displaying the data requests table.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta <jun@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+use action_menu;
+use action_menu_link_secondary;
+use coding_exception;
+use dml_exception;
+use html_writer;
+use moodle_url;
+use stdClass;
+use table_sql;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external\data_request_exporter;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * The class for displaying the data requests table.
+ *
+ * @copyright  2018 Jun Pataleta <jun@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_requests_table extends table_sql {
+
+    /** @var int The user ID. */
+    protected $userid = 0;
+
+    /** @var int[] The status filters. */
+    protected $statuses = [];
+
+    /** @var int[] The request type filters.  */
+    protected $types = [];
+
+    /** @var bool Whether this table is being rendered for managing data requests. */
+    protected $manage = false;
+
+    /** @var stdClass[] Array of data request persistents. */
+    protected $datarequests = [];
+
+    /**
+     * data_requests_table constructor.
+     *
+     * @param int $userid The user ID
+     * @param int[] $statuses
+     * @param int[] $types
+     * @param bool $manage
+     * @throws coding_exception
+     */
+    public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
+        parent::__construct('data-requests-table');
+
+        $this->userid = $userid;
+        $this->statuses = $statuses;
+        $this->types = $types;
+        $this->manage = $manage;
+
+        $columnheaders = [
+            'type' => get_string('requesttype', 'tool_dataprivacy'),
+            'userid' => get_string('user', 'tool_dataprivacy'),
+            'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
+            'requestedby' => get_string('requestby', 'tool_dataprivacy'),
+            'status' => get_string('requeststatus', 'tool_dataprivacy'),
+            'comments' => get_string('message', 'tool_dataprivacy'),
+            'actions' => '',
+        ];
+
+        $this->define_columns(array_keys($columnheaders));
+        $this->define_headers(array_values($columnheaders));
+        $this->no_sorting('actions');
+    }
+
+    /**
+     * The type column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_type($data) {
+        if ($this->manage) {
+            return $data->typenameshort;
+        }
+        return $data->typename;
+    }
+
+    /**
+     * The user column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_userid($data) {
+        $user = $data->foruser;
+        return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+    }
+
+    /**
+     * The context information column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_timecreated($data) {
+        return userdate($data->timecreated);
+    }
+
+    /**
+     * The requesting user's column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_requestedby($data) {
+        $user = $data->requestedbyuser;
+        return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+    }
+
+    /**
+     * The status column.
+     *
+     * @param stdClass $data The row data.
+     * @return mixed
+     */
+    public function col_status($data) {
+        return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
+    }
+
+    /**
+     * The comments column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_comments($data) {
+        return shorten_text($data->comments, 60);
+    }
+
+    /**
+     * The actions column.
+     *
+     * @param stdClass $data The row data.
+     * @return string
+     */
+    public function col_actions($data) {
+        global $OUTPUT;
+
+        $requestid = $data->id;
+        $status = $data->status;
+
+        // Prepare actions.
+        $actions = [];
+
+        // View action.
+        $actionurl = new moodle_url('#');
+        $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid];
+        $actiontext = get_string('viewrequest', 'tool_dataprivacy');
+        $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+        if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
+            // Approve.
+            $actiondata['data-action'] = 'approve';
+            $actiontext = get_string('approverequest', 'tool_dataprivacy');
+            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+
+            // Deny.
+            $actiondata['data-action'] = 'deny';
+            $actiontext = get_string('denyrequest', 'tool_dataprivacy');
+            $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata);
+        }
+
+        $actionsmenu = new action_menu($actions);
+        $actionsmenu->set_menu_trigger(get_string('actions'));
+        $actionsmenu->set_owner_selector('request-actions-' . $requestid);
+        $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+
+        return $OUTPUT->render($actionsmenu);
+    }
+
+    /**
+     * Query the database for results to display in the table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     * @throws dml_exception
+     * @throws coding_exception
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        global $PAGE;
+
+        // Count data requests from the given conditions.
+        $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+        $this->pagesize($pagesize, $total);
+
+        $sort = $this->get_sql_sort();
+
+        // Get data requests from the given conditions.
+        $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
+                $this->get_page_start(), $this->get_page_size());
+        $this->rawdata = [];
+        $context = \context_system::instance();
+        $renderer = $PAGE->get_renderer('tool_dataprivacy');
+        foreach ($datarequests as $persistent) {
+            $exporter = new data_request_exporter($persistent, ['context' => $context]);
+            $this->rawdata[] = $exporter->export($renderer);
+        }
+
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+
+    /**
+     * Override default implementation to display a more meaningful information to the user.
+     */
+    public function print_nothing_to_display() {
+        global $OUTPUT;
+        echo $this->render_reset_button();
+        $this->print_initials_bar();
+        if (!empty($this->statuses) || !empty($this->types)) {
+            $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
+        } else {
+            $message = get_string('nodatarequests', 'tool_dataprivacy');
+        }
+        echo $OUTPUT->notification($message, 'warning');
+    }
+
+    /**
+     * Override the table's show_hide_link method to prevent the show/hide links from rendering.
+     *
+     * @param string $column the column name, index into various names.
+     * @param int $index numerical index of the column.
+     * @return string HTML fragment.
+     */
+    protected function show_hide_link($column, $index) {
+        return '';
+    }
+}
diff --git a/admin/tool/dataprivacy/classes/output/request_filter.php b/admin/tool/dataprivacy/classes/output/request_filter.php
new file mode 100644 (file)
index 0000000..ff3108d
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_filter implements renderable, templatable {
+
+    /** @var array $filteroptions The filter options. */
+    protected $filteroptions;
+
+    /** @var array $selectedoptions The list of selected filter option values. */
+    protected $selectedoptions;
+
+    /** @var moodle_url|string $baseurl The url with params needed to call up this page. */
+    protected $baseurl;
+
+    /**
+     * request_filter constructor.
+     *
+     * @param array $filteroptions The filter options.
+     * @param array $selectedoptions The list of selected filter option values.
+     * @param string|moodle_url $baseurl The url with params needed to call up this page.
+     */
+    public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
+        $this->filteroptions = $filteroptions;
+        $this->selectedoptions = $selectedoptions;
+        if (!empty($baseurl)) {
+            $this->baseurl = new moodle_url($baseurl);
+        }
+    }
+
+    /**
+     * Function to export the renderer data in a format that is suitable for a mustache template.
+     *
+     * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+     * @return stdClass|array
+     */
+    public function export_for_template(renderer_base $output) {
+        global $PAGE;
+        $data = new stdClass();
+        if (empty($this->baseurl)) {
+            $this->baseurl = $PAGE->url;
+        }
+        $data->action = $this->baseurl->out(false);
+
+        foreach ($this->selectedoptions as $option) {
+            if (!isset($this->filteroptions[$option])) {
+                $this->filteroptions[$option] = $option;
+            }
+        }
+
+        $data->filteroptions = [];
+        foreach ($this->filteroptions as $value => $label) {
+            $selected = in_array($value, $this->selectedoptions);
+            $filteroption = (object)[
+                'value' => $value,
+                'label' => $label
+            ];
+            $filteroption->selected = $selected;
+            $data->filteroptions[] = $filteroption;
+        }
+        return $data;
+    }
+}
index 59ca8c8..4c2d8c4 100644 (file)
@@ -51,7 +51,10 @@ class provider implements
         \core_privacy\local\metadata\provider,
 
         // This tool may provide access to and deletion of user data.
-        \core_privacy\local\request\plugin\provider {
+        \core_privacy\local\request\plugin\provider,
+
+        // This plugin has some sitewide user preferences to export.
+        \core_privacy\local\request\user_preference_provider {
     /**
      * Returns meta data about this system.
      *
@@ -70,6 +73,10 @@ class provider implements
             ],
             'privacy:metadata:request'
         );
+
+        $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
+            'privacy:metadata:preference:tool_dataprivacy_request-filters');
+
         return $collection;
     }
 
@@ -162,4 +169,36 @@ class provider implements
      */
     public static function delete_data_for_user(approved_contextlist $contextlist) {
     }
+
+    /**
+     * Export all user preferences for the plugin.
+     *
+     * @param   int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
+        if ($preffilter !== null) {
+            $filters = json_decode($preffilter);
+            $descriptions = [];
+            foreach ($filters as $filter) {
+                list($category, $value) = explode(':', $filter);
+                $option = new stdClass();
+                switch($category) {
+                    case tool_helper::FILTER_TYPE:
+                        $option->category = get_string('requesttype', 'tool_dataprivacy');
+                        $option->name = tool_helper::get_shortened_request_type_string($value);
+                        break;
+                    case tool_helper::FILTER_STATUS:
+                        $option->category = get_string('requeststatus', 'tool_dataprivacy');
+                        $option->name = tool_helper::get_request_status_string($value);
+                        break;
+                }
+                $descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
+            }
+            // Export the filter preference as comma-separated values and text descriptions.
+            $values = implode(', ', $filters);
+            $descriptionstext = implode(', ', $descriptions);
+            writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
+        }
+    }
 }
index a3e613a..0887134 100644 (file)
@@ -36,8 +36,38 @@ $title = get_string('datarequests', 'tool_dataprivacy');
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-$requests = tool_dataprivacy\api::get_data_requests();
-$requestlist = new tool_dataprivacy\output\data_requests_page($requests);
+$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
+$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+if ($filtersapplied === [-1]) {
+    // If there are no filters submitted, check if there is a saved filters from the user preferences.
+    $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
+    if ($filterprefs && empty($filterscleared)) {
+        $filtersapplied = json_decode($filterprefs);
+    } else {
+        $filtersapplied = [];
+    }
+}
+// Save the current applied filters to the user preferences.
+set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
+
+$types = [];
+$statuses = [];
+foreach ($filtersapplied as $filter) {
+    list($category, $value) = explode(':', $filter);
+    switch($category) {
+        case \tool_dataprivacy\local\helper::FILTER_TYPE:
+            $types[] = $value;
+            break;
+        case \tool_dataprivacy\local\helper::FILTER_STATUS:
+            $statuses[] = $value;
+            break;
+    }
+}
+
+$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+$table->baseurl = $url;
+
+$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
 $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
 echo $requestlistoutput->render($requestlist);
 
index b8622d8..aa64047 100644 (file)
@@ -48,8 +48,8 @@ $string['compliant'] = 'Compliant';
 $string['confirmapproval'] = 'Do you really want to approve this data request?';
 $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.';
 $string['confirmdenial'] = 'Do you really want deny this data request?';
-$string['contactdataprotectionofficer'] = 'Contact Data Protection Officer';
-$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the Data Protection Officer and make a data request via a link on their profile page.';
+$string['contactdataprotectionofficer'] = 'Contact the privacy officer';
+$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
 $string['contextlevelname10'] = 'Site';
 $string['contextlevelname30'] = 'Users';
 $string['contextlevelname40'] = 'Course categories';
@@ -57,7 +57,7 @@ $string['contextlevelname50'] = 'Courses';
 $string['contextlevelname70'] = 'Activity modules';
 $string['contextlevelname80'] = 'Blocks';
 $string['contextpurposecategorysaved'] = 'Purpose and category saved.';
-$string['contactdpoviaprivacypolicy'] = 'Please contact the Data Protection Officer as described in the privacy policy.';
+$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
 $string['createcategory'] = 'Create data category';
 $string['createpurpose'] = 'Create data purpose';
 $string['datadeletion'] = 'Data deletion';
@@ -82,8 +82,8 @@ $string['defaultssaved'] = 'Defaults saved';
 $string['deny'] = 'Deny';
 $string['denyrequest'] = 'Deny request';
 $string['download'] = 'Download';
-$string['dporolemapping'] = 'Data Protection Officer role mapping';
-$string['dporolemapping_desc'] = 'The Data Protection Officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a Data Protection Officer role mapping option.';
+$string['dporolemapping'] = 'Privacy officer role mapping';
+$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
 $string['editcategories'] = 'Edit categories';
 $string['editcategory'] = 'Edit category';
 $string['editcategories'] = 'Edit categories';
@@ -101,8 +101,10 @@ $string['errorrequestalreadyexists'] = 'You already have an ongoing request.';
 $string['errorrequestnotfound'] = 'Request not found';
 $string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.';
 $string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.';
-$string['exceptionnotificationsubject'] = "Exception occured while processing privacy data";
-$string['exceptionnotificationbody'] = "<p>Exception occured while calling <b>{\$a->fullmethodname}</b>.<br>This means that plugin <b>{\$a->component}</b> did not complete processing data. Below you can find exception information that can be passed to the plugin developer.</p><pre>{\$a->message}<br>\n\n{\$a->backtrace}</pre>";
+$string['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data';
+$string['exceptionnotificationbody'] = '<p>Exception occurred while calling <b>{$a->fullmethodname}</b>.<br>This means that plugin <b>{$a->component}</b> did not complete the processing of data. The following exception information may be passed on to the plugin developer:</p><pre>{$a->message}<br>
+
+{$a->backtrace}</pre>';
 $string['expiredretentionperiodtask'] = 'Expired retention period';
 $string['expiry'] = 'Expiry';
 $string['expandplugin'] = 'Expand and collapse plugin.';
@@ -110,6 +112,7 @@ $string['expandplugintype'] = 'Expand and collapse plugin type.';
 $string['explanationtitle'] = 'Icons used on this page and what they mean.';
 $string['external'] = 'Additional';
 $string['externalexplanation'] = 'An additional plugin installed on this site.';
+$string['filteroption'] = '{$a->category}: {$a->name}';
 $string['frontpagecourse'] = 'Front page course';
 $string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
 $string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
@@ -160,6 +163,7 @@ $string['nameemail'] = '{$a->name} ({$a->email})';
 $string['nchildren'] = '{$a} children';
 $string['newrequest'] = 'New request';
 $string['nodatarequests'] = 'There are no data requests';
+$string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
 $string['noactivitiestoload'] = 'No activities';
 $string['noassignedroles'] = 'No assigned roles in this context';
 $string['noblockstoload'] = 'No blocks';
@@ -174,11 +178,12 @@ $string['notset'] = 'Not set (use the default value)';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
+$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
 $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.';
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
+$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
@@ -199,16 +204,16 @@ $string['requestcomments_help'] = 'This box enables you to enter any further det
 $string['requestemailintro'] = 'You have received a data request:';
 $string['requestfor'] = 'Requesting for';
 $string['requeststatus'] = 'Status';
-$string['requestsubmitted'] = 'Your request has been submitted to the site\'s Data Protection Officer';
+$string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
 $string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the site\'s Data Protection Officer';
+$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
 $string['requesttypedelete'] = 'Delete all of my personal data';
 $string['requesttypedeleteshort'] = 'Delete';
 $string['requesttypeexport'] = 'Export all of my personal data';
 $string['requesttypeexportshort'] = 'Export';
 $string['requesttypeothers'] = 'General inquiry';
-$string['requesttypeothersshort'] = 'Others';
+$string['requesttypeothersshort'] = 'Message';
 $string['requiresattention'] = 'Requires attention.';
 $string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
 $string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
index 45cedc6..594aaab 100644 (file)
     * none
 
     Context variables required for this template:
-    * requests - Array of data requests.
+    * newdatarequesturl string The URL pointing to the data request creation page.
+    * datarequests string The HTML of the data requests table.
 
     Example context (json):
     {
-        "requests": [
-            {
-                "id": 1,
-                "foruser" : {
-                    "fullname": "Oscar Olsen",
-                    "profileurl": "#"
+        "newdatarequesturl": "#",
+        "datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>",
+        "filter": {
+            "action": "#",
+            "filteroptions": [
+                {
+                    "value": "1",
+                    "label": "Option 1"
                 },
-                "typenameshort" : "Export",
-                "comments": "I would like to download all of my daughter's personal data",
-                "statuslabelclass": "label-default",
-                "statuslabel": "Pending",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 2,
-                "foruser" : {
-                    "fullname": "Alexandre Denys",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Export",
-                "comments": "Please give me all of the information you have about me...",
-                "statuslabelclass": "label-warning",
-                "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 3,
-                "foruser" : {
-                    "fullname": "Hirondino Moura",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Delete",
-                "comments": "Please delete all of my son's personal data.",
-                "statuslabelclass": "label-success",
-                "statuslabel": "Complete",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            },
-            {
-                "id": 4,
-                "foruser" : {
-                    "fullname": "Florian Krause",
-                    "profileurl": "#"
+                {
+                    "value": "2",
+                    "label": "Option 2",
+                    "selected": true
                 },
-                "typenameshort" : "Delete",
-                "comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
-                "statuslabelclass": "label-danger",
-                "statuslabel": "Rejected",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
+                {
+                    "value": "3",
+                    "label": "Option 3",
+                    "selected": true
                 }
-            },
-            {
-                "id": 5,
-                "foruser" : {
-                    "fullname": "Nicklas Sørensen",
-                    "profileurl": "#"
-                },
-                "typenameshort" : "Export",
-                "comments": "Please let me download my data",
-                "statuslabelclass": "label-info",
-                "statuslabel": "Processing",
-                "timecreated" : 1517902435,
-                "requestedbyuser" : {
-                    "fullname": "Martha Smith",
-                    "profileurl": "#"
-                }
-            }
-        ]
+            ]
+        }
     }
 }}
 
 
 <div data-region="datarequests">
     <div class="m-t-1 m-b-1">
-        <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
-            {{#str}}newrequest, tool_dataprivacy{{/str}}
-        </a>
+        <div class="pull-right">
+            <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request">
+                {{#str}}newrequest, tool_dataprivacy{{/str}}
+            </a>
+        </div>
+        {{#filter}}
+            {{>tool_dataprivacy/request_filter}}
+        {{/filter}}
+    </div>
+
+    <div class="m-t-1 m-b-1" data-region="data-requests-table">
+        {{{datarequests}}}
     </div>
-    <table class="generaltable fullwidth">
-        <thead>
-            <tr>
-                <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
-                <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
-                <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
-            </tr>
-        </thead>
-        <tbody>
-            {{#requests}}
-            <tr {{!
-              }} data-region="request-node"{{!
-              }} data-id="{{id}}"{{!
-              }} data-type="{{type}}"{{!
-              }} data-status="{{status}}"{{!
-              }}>
-                <td>{{typenameshort}}</td>
-                <td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
-                <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
-                <td>
-                    <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
-                </td>
-                <td>{{#shortentext}}60, {{comments}}{{/shortentext}}</td>
-                <td>
-                    {{#actions}}
-                        {{> core/action_menu}}
-                    {{/actions}}
-                </td>
-            </tr>
-            {{/requests}}
-            {{^requests}}
-            <tr>
-                <td class="text-muted" colspan="6">
-                    {{#str}}nodatarequests, tool_dataprivacy{{/str}}
-                </td>
-            </tr>
-            {{/requests}}
-        </tbody>
-    </table>
 </div>
 
 {{#js}}
diff --git a/admin/tool/dataprivacy/templates/request_filter.mustache b/admin/tool/dataprivacy/templates/request_filter.mustache
new file mode 100644 (file)
index 0000000..ea4f15d
--- /dev/null
@@ -0,0 +1,67 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/request_filter
+
+    Template for the request filter element.
+
+    Context variables required for this template:
+    * action string - The action URL for the form.
+    * filteroptions - Array of filter options.
+      * value string - The option value.
+      * label string - The option label.
+      * selected boolean - Whether the option is selected
+
+    Example context (json):
+    {
+        "action": "#",
+        "filteroptions": [
+            {
+                "value": "1",
+                "label": "Option 1"
+            },
+            {
+                "value": "2",
+                "label": "Option 2",
+                "selected": true
+            },
+            {
+                "value": "3",
+                "label": "Option 3",
+                "selected": true
+            },
+            {
+                "value": "4",
+                "label": "Option 4"
+            }
+        ]
+    }
+}}
+<form method="post" action="{{action}}" class="m-b-1" role="search" id="request_filter_form">
+    <label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label>
+    <select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select">
+        {{#filteroptions}}
+            <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option>
+        {{/filteroptions}}
+    </select>
+    <input type="hidden" id="filters-cleared" name="filters-cleared" value="0" />
+</form>
+{{#js}}
+require(['tool_dataprivacy/request_filter'], function(Filter) {
+    Filter.init();
+});
+{{/js}}
index 6e06478..f4a7a66 100644 (file)
@@ -29,6 +29,7 @@ use tool_dataprivacy\api;
 use tool_dataprivacy\data_registry;
 use tool_dataprivacy\expired_context;
 use tool_dataprivacy\data_request;
+use tool_dataprivacy\local\helper;
 use tool_dataprivacy\task\initiate_data_request_task;
 use tool_dataprivacy\task\process_data_request_task;
 
@@ -411,42 +412,128 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
     }
 
     /**
-     * Test for api::get_data_requests()
+     * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
+     *
+     * @return array
      */
-    public function test_get_data_requests() {
+    public function get_data_requests_provider() {
         $generator = new testing_data_generator();
         $user1 = $generator->create_user();
         $user2 = $generator->create_user();
-        $comment = 'sample comment';
+        $user3 = $generator->create_user();
+        $user4 = $generator->create_user();
+        $user5 = $generator->create_user();
+        $users = [$user1, $user2, $user3, $user4, $user5];
+        $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
+        $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
 
-        // Make a data request as user 1.
-        $this->setUser($user1);
-        $d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-        // Make a data request as user 2.
-        $this->setUser($user2);
-        $d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
-
-        // Fetching data requests of specific users.
-        $requests = api::get_data_requests($user1->id);
-        $this->assertCount(1, $requests);
-        $datarequest = reset($requests);
-        $this->assertEquals($d1->to_record(), $datarequest->to_record());
-
-        $requests = api::get_data_requests($user2->id);
-        $this->assertCount(1, $requests);
-        $datarequest = reset($requests);
-        $this->assertEquals($d2->to_record(), $datarequest->to_record());
-
-        // Fetching data requests of all users.
-        // As guest.
-        $this->setGuestUser();
-        $requests = api::get_data_requests();
-        $this->assertEmpty($requests);
-
-        // As DPO (admin in this case, which is default if no site DPOs are set).
-        $this->setAdminUser();
-        $requests = api::get_data_requests();
-        $this->assertCount(2, $requests);
+        return [
+            // Own data requests.
+            [$users, $user1, false, $completeonly],
+            // Non-DPO fetching all requets.
+            [$users, $user2, true, $completeonly],
+            // Admin fetching all completed and cancelled requests.
+            [$users, get_admin(), true, $completeandcancelled],
+            // Admin fetching all completed requests.
+            [$users, get_admin(), true, $completeonly],
+            // Guest fetching all requests.
+            [$users, guest_user(), true, $completeonly],
+        ];
+    }
+
+    /**
+     * Test for api::get_data_requests()
+     *
+     * @dataProvider get_data_requests_provider
+     * @param stdClass[] $users Array of users to create data requests for.
+     * @param stdClass $loggeduser The user logging in.
+     * @param boolean $fetchall Whether to fetch all records.
+     * @param int[] $statuses Status filters.
+     */
+    public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
+        $comment = 'Data %s request comment by user %d';
+        $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
+        $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
+        // Make a data requests for the users.
+        foreach ($users as $user) {
+            $this->setUser($user);
+            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
+            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
+        }
+
+        // Log in as the target user.
+        $this->setUser($loggeduser);
+        // Get records count based on the filters.
+        $userid = $loggeduser->id;
+        if ($fetchall) {
+            $userid = 0;
+        }
+        $count = api::get_data_requests_count($userid);
+        if (api::is_site_dpo($loggeduser->id)) {
+            // DPOs should see all the requests.
+            $this->assertEquals(count($users) * 2, $count);
+        } else {
+            if (empty($userid)) {
+                // There should be no data requests for this user available.
+                $this->assertEquals(0, $count);
+            } else {
+                // There should be only one (request with pending status).
+                $this->assertEquals(2, $count);
+            }
+        }
+        // Get data requests.
+        $requests = api::get_data_requests($userid);
+        // The number of requests should match the count.
+        $this->assertCount($count, $requests);
+
+        // Test filtering by status.
+        if ($count && !empty($statuses)) {
+            $filteredcount = api::get_data_requests_count($userid, $statuses);
+            // There should be none as they are all pending.
+            $this->assertEquals(0, $filteredcount);
+            $filteredrequests = api::get_data_requests($userid, $statuses);
+            $this->assertCount($filteredcount, $filteredrequests);
+
+            $statuscounts = [];
+            foreach ($statuses as $stat) {
+                $statuscounts[$stat] = 0;
+            }
+            $numstatus = count($statuses);
+            // Get all requests with status filter and update statuses, randomly.
+            foreach ($requests as $request) {
+                if (rand(0, 1)) {
+                    continue;
+                }
+
+                if ($numstatus > 1) {
+                    $index = rand(0, $numstatus - 1);
+                    $status = $statuses[$index];
+                } else {
+                    $status = reset($statuses);
+                }
+                $statuscounts[$status]++;
+                api::update_request_status($request->get('id'), $status);
+            }
+            $total = array_sum($statuscounts);
+            $filteredcount = api::get_data_requests_count($userid, $statuses);
+            $this->assertEquals($total, $filteredcount);
+            $filteredrequests = api::get_data_requests($userid, $statuses);
+            $this->assertCount($filteredcount, $filteredrequests);
+            // Confirm the filtered requests match the status filter(s).
+            foreach ($filteredrequests as $request) {
+                $this->assertContains($request->get('status'), $statuses);
+            }
+
+            if ($numstatus > 1) {
+                // Fetch by individual status to check the numbers match.
+                foreach ($statuses as $status) {
+                    $filteredcount = api::get_data_requests_count($userid, [$status]);
+                    $this->assertEquals($statuscounts[$status], $filteredcount);
+                    $filteredrequests = api::get_data_requests($userid, [$status]);
+                    $this->assertCount($filteredcount, $filteredrequests);
+                }
+            }
+        }
     }
 
     /**
index 4aa17d1..8a057cf 100644 (file)
@@ -99,8 +99,8 @@ class helper {
             'name' => $name,
             'description' => $description,
             'timecreated' => transform::datetime($record->timecreated),
-            'ip' => $record->ip,
             'origin' => static::transform_origin($record->origin),
+            'ip' => $isauthor ? $record->ip : '',
             'other' => $other ? $other : []
         ];
 
index af91b8b..252ba50 100644 (file)
@@ -74,7 +74,9 @@ class provider implements
      * @param approved_contextlist $contextlist The approved contexts to export information for.
      */
     public static function export_user_data(approved_contextlist $contextlist) {
-        static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+        if (get_config('tool_log', 'exportlog')) {
+            static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+        }
     }
 
     /**
index bfda300..571b91f 100644 (file)
@@ -24,6 +24,8 @@
 
 $string['actlogshdr'] = 'Available log stores';
 $string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
+$string['exportlog'] = 'Include logs when exporting.';
+$string['exportlogdetail'] = 'Include logs that relate to the user when exporting.';
 $string['logging'] = 'Logging';
 $string['managelogging'] = 'Manage log stores';
 $string['pluginname'] = 'Log store manager';
index 3fee812..cbc3f31 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+
+    $privacysettings = $ADMIN->locate('privacysettings');
+
+    if ($ADMIN->fulltree) {
+        $privacysettings->add(new admin_setting_configcheckbox('tool_log/exportlog',
+                new lang_string('exportlog', 'tool_log'),
+                new lang_string('exportlogdetail', 'tool_log'), 1)
+        );
+    }
+
     $ADMIN->add('modules', new admin_category('logging', new lang_string('logging', 'tool_log')));
 
     $temp = new admin_settingpage('managelogging', new lang_string('managelogging', 'tool_log'));
index b5a7d90..1af8d1c 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2018051401; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2018050800; // Requires this Moodle version.
 $plugin->component = 'tool_log'; // Full name of the plugin (used for diagnostics).
index b6b20d7..fd29175 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 8bffdf5..62919d3 100644 (file)
@@ -83,7 +83,7 @@ function tool_policy_before_standard_html_head() {
     if (!empty($CFG->sitepolicyhandler)
             && $CFG->sitepolicyhandler == 'tool_policy'
             && empty($USER->policyagreed)
-            && isguestuser()) {
+            && (isguestuser() || !isloggedin())) {
         $output = $PAGE->get_renderer('tool_policy');
         $page = new \tool_policy\output\guestconsent();
 
index fde43cc..6c504eb 100644 (file)
@@ -81,12 +81,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     And I press "Next"
     And I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
     And I set the field "I agree to the This site policy" to "1"
@@ -129,22 +129,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     And I press "Next"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     And I press "Next"
     And I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
@@ -495,20 +495,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I set the field "I agree to the This site policy" to "1"
     And I set the field "I agree to the This privacy policy" to "1"
@@ -528,22 +528,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I follow "Log in"
     When I press "Create new account"
     # Confirm that the user can view and accept policies when attempting to create another account.
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
index c23c25f..7656ae8 100644 (file)
@@ -693,7 +693,15 @@ class tool_uploadcourse_course {
             return false;
         }
 
-        // TODO MDL-59259 allow to set course format options for the current course format.
+        // Add data for course format options.
+        if (isset($coursedata['format']) || $exists) {
+            if (isset($coursedata['format'])) {
+                $courseformat = course_get_format((object)['format' => $coursedata['format']]);
+            } else {
+                $courseformat = course_get_format($existingdata);
+            }
+            $coursedata += $courseformat->validate_course_format_options($this->rawdata);
+        }
 
         // Special case, 'numsections' is not a course format option any more but still should apply from defaults.
         if (!$exists || !array_key_exists('numsections', $coursedata)) {
index f52ec31..4896658 100644 (file)
@@ -99,7 +99,8 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
         $co->proceed();
-        $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+        $course = $DB->get_record('course', array('shortname' => 'newcourse'), '*', MUST_EXIST);
+        $this->assertEquals(0, course_get_format($course)->get_course()->coursedisplay);
 
         // Try to add a new course, that already exists.
         $coursecount = $DB->count_records('course', array());
@@ -118,6 +119,16 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $co->proceed();
         $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+
+        // Add a new course with non-default course format option.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'c3', 'fullname' => 'C3', 'summary' => 'New c3', 'category' => 1,
+            'format' => 'weeks', 'coursedisplay' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c3'), '*', MUST_EXIST);
+        $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
     }
 
     public function test_create_with_sections() {
@@ -260,6 +271,16 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($co->prepare());
         $co->proceed();
         $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Update course format option.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'coursedisplay' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c1'), '*', MUST_EXIST);
+        $this->assertEquals(1, course_get_format($course)->get_course()->coursedisplay);
     }
 
     public function test_data_saved() {
index 024fe2f..4c0ca9e 100644 (file)
@@ -1187,9 +1187,18 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding);
                     empty($nuvalue) ? $nuvalue = array() : $nuvalue;
                     $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
-
                     foreach ($ldapkeys as $ldapkey) {
-                        $ldapkey   = $ldapkey;
+                        // Skip update if $ldapkey does not exist in LDAP.
+                        if (!isset($user_entry[$ldapkey][0])) {
+                            $success = false;
+                            error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
+                                                                     array('ldapkey' => $ldapkey,
+                                                                            'key' => $key,
+                                                                            'ouvalue' => $ouvalue,
+                                                                            'nuvalue' => $nuvalue)));
+                            continue;
+                        }
+
                         $ldapvalue = $user_entry[$ldapkey][0];
                         if (!$ambiguous) {
                             // Skip update if the values already match
index 237423e..5040996 100644 (file)
@@ -149,6 +149,7 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 412d35b..6074f71 100644 (file)
@@ -38,7 +38,7 @@ $string['pluginname'] = 'MNet authentication';
 $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
 $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
 $string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
 $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
@@ -53,8 +53,8 @@ $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user
 $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
 $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
 $string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:mnet_external:id'] = 'The identifier for the user.';
-$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:mnet_external:id'] = 'The user ID';
+$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
 $string['privacy:metadata:mnet_external:institution'] = 'The institution that this user is a member of.';
 $string['privacy:metadata:mnet_external:lang'] = 'A user preference for the language shown.';
@@ -64,20 +64,20 @@ $string['privacy:metadata:mnet_external:lastname'] = 'The surname of the user.';
 $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
 $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user.';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
 $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
 $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
-$string['privacy:metadata:mnet_external:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
 $string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
 $string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
 $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
 $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
@@ -94,7 +94,7 @@ $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried
 $string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
 $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
 $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
-$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier.';
+$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
 $string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
index d91aaca..ed38e2d 100644 (file)
@@ -93,7 +93,7 @@ $string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connecte
 $string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.';
 $string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.';
 $string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.';
-$string['privacy:metadata:auth_oauth2:issuerid'] = 'The identifier of the OAuth 2 issuer for this OAuth 2 login.';
+$string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login';
 $string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.';
 $string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.';
 $string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.';
index a2f922a..3793d9b 100644 (file)
@@ -865,7 +865,7 @@ class backup_calendarevents_structure_step extends backup_structure_step {
                 'name', 'description', 'format', 'courseid', 'groupid', 'userid',
                 'repeatid', 'modulename', 'instance', 'type', 'eventtype', 'timestart',
                 'timeduration', 'timesort', 'visible', 'uuid', 'sequence', 'timemodified',
-                'priority'));
+                'priority', 'location'));
 
         // Build the tree
         $events->add_child($event);
index 56ca4d8..8443174 100644 (file)
@@ -2725,7 +2725,8 @@ class restore_calendarevents_structure_step extends restore_structure_step {
                 'uuid'           => $data->uuid,
                 'sequence'       => $data->sequence,
                 'timemodified'   => $data->timemodified,
-                'priority'       => isset($data->priority) ? $data->priority : null);
+                'priority'       => isset($data->priority) ? $data->priority : null,
+                'location'       => isset($data->location) ? $data->location : null);
         if ($this->name == 'activity_calendar') {
             $params['instance'] = $this->task->get_activityid();
         } else {
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 a6bef22..770b5c7 100644 (file)
@@ -79,6 +79,7 @@ class event_exporter_base extends exporter {
             $event->get_id()
         );
         $data->descriptionformat = $event->get_description()->get_format();
+        $data->location = external_format_text($event->get_location(), FORMAT_PLAIN, $related['context']->id)[0];
         $data->groupid = $groupid;
         $data->userid = $userid;
         $data->categoryid = $categoryid;
@@ -123,6 +124,12 @@ class event_exporter_base extends exporter {
                 'default' => null,
                 'null' => NULL_ALLOWED
             ],
+            'location' => [
+                'type' => PARAM_RAW_TRIMMED,
+                'optional' => true,
+                'default' => null,
+                'null' => NULL_ALLOWED
+            ],
             'categoryid' => [
                 'type' => PARAM_INT,
                 'optional' => true,
index ad1d98c..6c8b0f1 100644 (file)
@@ -78,6 +78,10 @@ class action_event implements action_event_interface {
         return $this->event->get_description();
     }
 
+    public function get_location() {
+        return $this->event->get_location();
+    }
+
     public function get_category() {
         return $this->event->get_category();
     }
index 2f5ea39..fdfee97 100644 (file)
@@ -52,6 +52,11 @@ class event implements event_interface {
      */
     protected $description;
 
+    /**
+     * @var string $location Location of this event.
+     */
+    protected $location;
+
     /**
      * @var proxy_interface $category Category for this event.
      */
@@ -118,6 +123,7 @@ class event implements event_interface {
      * @param times_interface            $times          The times associated with the event.
      * @param bool                       $visible        The event's visibility. True for visible, false for invisible.
      * @param proxy_interface            $subscription   The event's subscription.
+     * @param string                     $location       The event's location.
      */
     public function __construct(
         $id,
@@ -132,11 +138,13 @@ class event implements event_interface {
         $type,
         times_interface $times,
         $visible,
-        proxy_interface $subscription = null
+        proxy_interface $subscription = null,
+        $location = null
     ) {
         $this->id = $id;
         $this->name = $name;
         $this->description = $description;
+        $this->location = $location;
         $this->category = $category;
         $this->course = $course;
         $this->group = $group;
@@ -161,6 +169,10 @@ class event implements event_interface {
         return $this->description;
     }
 
+    public function get_location() {
+        return $this->location;
+    }
+
     public function get_category() {
         return $this->category;
     }
index 0e2de4f..5561f62 100644 (file)
@@ -56,6 +56,13 @@ interface event_interface {
      */
     public function get_description();
 
+    /**
+     * Get the event's location.
+     *
+     * @return location_interface
+     */
+    public function get_location();
+
     /**
      * Get the category object associated with the event.
      *
index 9574f7f..807ecad 100644 (file)
@@ -189,7 +189,8 @@ abstract class event_abstract_factory implements event_factory_interface {
                 (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified)
             ),
             !empty($dbrow->visible),
-            $subscription
+            $subscription,
+            $dbrow->location
         );
 
         $isactionevent = !empty($dbrow->type) && $dbrow->type == CALENDAR_EVENT_TYPE_ACTION;
index 1e36407..924a6fa 100644 (file)
@@ -98,6 +98,10 @@ class create extends \moodleform {
         $mform->setType('description', PARAM_RAW);
         $mform->setAdvanced('description');
 
+        $mform->addElement('text', 'location', get_string('location', 'moodle'), 'size="50"');
+        $mform->setType('location', PARAM_RAW_TRIMMED);
+        $mform->setAdvanced('location');
+
         // Add the variety of elements allowed for selecting event duration.
         $this->add_event_duration_elements($mform);
 
@@ -119,10 +123,9 @@ class create extends \moodleform {
         global $DB, $CFG;
 
         $errors = parent::validation($data, $files);
-        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
         $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
-
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
         if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
         }
index 214983f..5600ec7 100644 (file)
@@ -68,6 +68,7 @@ class event_mapper implements event_mapper_interface {
                 'id' => $coalesce('id'),
                 'name' => $coalesce('name'),
                 'description' => $coalesce('description'),
+                'location' => $coalesce('location'),
                 'format' => $coalesce('format'),
                 'categoryid' => $coalesce('categoryid'),
                 'courseid' => $coalesce('courseid'),
@@ -119,6 +120,7 @@ class event_mapper implements event_mapper_interface {
             'name'             => $event->get_name(),
             'description'      => $event->get_description()->get_value(),
             'format'           => $event->get_description()->get_format(),
+            'location'         => $event->get_location(),
             'courseid'         => $event->get_course() ? $event->get_course()->get('id') : null,
             'categoryid'       => $event->get_category() ? $event->get_category()->get('id') : null,
             'groupid'          => $event->get_group() ? $event->get_group()->get('id') : null,
index 4fc58de..3332421 100644 (file)
@@ -273,6 +273,7 @@ class provider implements
                 $eventdetails = (object) [
                     'name' => $event->name,
                     'description' => $event->description,
+                    'location' => $event->location,
                     'eventtype' => $event->eventtype,
                     'timestart' => transform::datetime($event->timestart),
                     'timeduration' => $event->timeduration
@@ -456,6 +457,7 @@ class provider implements
                        details.id as eventid,
                        details.name as name,
                        details.description as description,
+                       details.location as location,
                        details.eventtype as eventtype,
                        details.timestart as timestart,
                        details.timeduration as timeduration
index ca162a2..53bd01d 100644 (file)
@@ -211,6 +211,11 @@ foreach($events as $event) {
 
     $ev->add_property('class', 'PUBLIC'); // PUBLIC / PRIVATE / CONFIDENTIAL
     $ev->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
+
+    if (!empty($event->location)) {
+        $ev->add_property('location', $event->location);
+    }
+
     $ev->add_property('dtstamp', Bennu::timestamp_to_datetime()); // now
     if ($event->timeduration > 0) {
         //dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
index 0f684dd..60d3d0f 100644 (file)
@@ -2912,6 +2912,8 @@ function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $
         \core_date::set_default_server_timezone();
     }
 
+    $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
+            str_replace('\\', '', $event->properties['LOCATION'][0]->value);
     $eventrecord->uuid = $event->properties['UID'][0]->value;
     $eventrecord->timemodified = time();
 
index 91d0751..157a652 100644 (file)
                     </a>
                 {{/canedit}}
             </div>
-            {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
-            <h3 class="name d-inline-block">{{name}}</h3>
-            <span class="date pull-xs-right m-r-1">{{{formattedtime}}}</span>
+            {{#icon}}<div class="d-inline-block mt-1 align-top">{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}</div>{{/icon}}
+            <div class="d-inline-block">
+                <h3 class="name d-inline-block">{{name}}</h3>
+                <span class="date pull-xs-right m-r-1">{{{formattedtime}}}</span>
+                <div class="location">{{#location}}{{{location}}}{{/location}}</div>
+            </div>
         </div>
         <div class="description card-block calendar_event_{{eventtype}}">
             <p>{{{description}}}</p>
index de5424e..576ffbe 100644 (file)
@@ -23,6 +23,7 @@
     {
         "timestart": 1490320388,
         "description": "An random event description",
+        "location": "13th floor, building 42",
         "eventtype": "User",
         "source": "Ical imported",
         "groupname": "Group 1"
             <div class="description-content col-xs-11">{{{.}}}</div>
         </div>
         {{/description}}
+        {{#location}}
+        <div class="row m-t-1">
+            <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
+            <div class="location-content col-xs-11">{{{.}}}</div>
+        </div>
+        {{/location}}
         {{#isactionevent}}
         <div class="row m-t-1">
             <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
index acb8643..d6ae9f6 100644 (file)
@@ -108,6 +108,10 @@ class core_calendar_action_event_test_event implements event_interface {
         return new event_description('asdf', 1);
     }
 
+    public function get_location() {
+        return 'Cube office';
+    }
+
     public function get_category() {
         return new \stdClass();
     }
index e0e7850..5603daa 100644 (file)
@@ -118,15 +118,22 @@ Feature: Perform basic calendar functionality
       | Type of event | user |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
+      | Location | Cube office |
     And I am on "Course 1" course homepage
     When I follow "This month"
     And I click on "Really awesome event!" "link"
+    And ".location-content" "css_element" should exist
+    And I should see "Cube office"
     And I click on "Edit" "button"
     And I set the following fields to these values:
       | Event title | Mediocre event :( |
       | Description | Wait, this event isn't that great. |
+      | Location | |
     And I press "Save"
+    And I should see "Mediocre event"
+    And I click on "Mediocre event :(" "link"
     Then I should see "Mediocre event"
+    And ".location-content" "css_element" should not exist
 
   @javascript
   Scenario: Module events editing
@@ -154,3 +161,22 @@ Feature: Perform basic calendar functionality
     When I click on "Go to activity" "link"
     And I wait to be redirected
     Then I should see "Test choice"
+
+  @javascript
+  Scenario: Attempt to create event without fill required fields should display validation errors
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "New event" "button"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I am on site homepage
+    And I follow "Calendar"
+    And I click on "New event" "button"
+    And I set the field "Type of event" to "Course"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I should see "Select a course"
+    And I set the field "Event title" to "Really awesome event!"
+    When I click on "Save" "button"
+    Then I should see "Select a course"
index 6930b1e..a2c47cd 100644 (file)
@@ -31,6 +31,7 @@ Feature: Import and edit calendar events
     And I should see "Event on 2-15-2017"
     And I should see "Event on 2-25-2017"
     And I click on "Event on 2-15-2017" "link"
+    And I should see "Some place"
     And I click on "Edit" "button"
     And I set the following fields to these values:
       | Event title    | Event on 2-20-2017 |
index 8439caa..011464b 100644 (file)
@@ -100,6 +100,7 @@ class core_calendar_container_testcase extends advanced_testcase {
         $this->assertEquals($dbrow->description, $event->get_description()->get_value());
         $this->assertEquals($dbrow->format, $event->get_description()->get_format());
         $this->assertEquals($dbrow->courseid, $event->get_course()->get('id'));
+        $this->assertEquals($dbrow->location, $event->get_location());
 
         if ($dbrow->groupid == 0) {
             $this->assertNull($event->get_group());
@@ -337,6 +338,7 @@ class core_calendar_container_testcase extends advanced_testcase {
         $event = new \stdClass();
         $event->name = 'An event';
         $event->description = 'Event description';
+        $event->location = 'Event location';
         $event->format = FORMAT_HTML;
         $event->eventtype = \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED;
         $event->userid = 1;
@@ -386,6 +388,7 @@ class core_calendar_container_testcase extends advanced_testcase {
         $event = new \stdClass();
         $event->name = 'An event';
         $event->description = 'Event description';
+        $event->location = 'Event location';
         $event->format = FORMAT_HTML;
         $event->eventtype = 'close';
         $event->userid = $user->id;
@@ -490,7 +493,8 @@ class core_calendar_container_testcase extends advanced_testcase {
                     'timesort' => 1486396800,
                     'visible' => 1,
                     'timemodified' => 1485793098,
-                    'subscriptionid' => null
+                    'subscriptionid' => null,
+                    'location' => 'Test location',
                 ]
             ],
 
@@ -512,7 +516,8 @@ class core_calendar_container_testcase extends advanced_testcase {
                     'timesort' => 1486396800,
                     'visible' => 1,
                     'timemodified' => 1485793098,
-                    'subscriptionid' => null
+                    'subscriptionid' => null,
+                    'location' => 'Test location',
                 ]
             ]
         ];
@@ -567,7 +572,8 @@ class core_calendar_container_testcase extends advanced_testcase {
             'timesort' => 1486396800,
             'visible' => 1,
             'timemodified' => 1485793098,
-            'subscriptionid' => null
+            'subscriptionid' => null,
+            'location' => 'Test location',
         ];
 
         foreach ((array) $skeleton as $key => $value) {
index e2bf0bf..abf40ea 100644 (file)
@@ -128,7 +128,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                 'timemodified' => 123456789,
                 'timesort' => 123456789,
                 'visible' => 1,
-                'subscriptionid' => 1
+                'subscriptionid' => 1,
+                'location' => 'Test location',
             ]
         );
     }
@@ -177,7 +178,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                 'timemodified' => 123456789,
                 'timesort' => 123456789,
                 'visible' => 1,
-                'subscriptionid' => 1
+                'subscriptionid' => 1,
+                'location' => 'Test location',
             ]
         );
     }
@@ -226,7 +228,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                 'timemodified' => 123456789,
                 'timesort' => 123456789,
                 'visible' => 1,
-                'subscriptionid' => 1
+                'subscriptionid' => 1,
+                'location' => 'Test location',
             ]
         );
     }
@@ -275,7 +278,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                 'timemodified' => 123456789,
                 'timesort' => 123456789,
                 'visible' => 1,
-                'subscriptionid' => 1
+                'subscriptionid' => 1,
+                'location' => 'Test location',
             ]
         );
 
@@ -330,7 +334,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                 'timemodified' => 123456789,
                 'timesort' => 123456789,
                 'visible' => 1,
-                'subscriptionid' => 1
+                'subscriptionid' => 1,
+                'location' => 'Test location',
             ]
         );
 
@@ -364,7 +369,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                     'timemodified' => 123456789,
                     'timesort' => 123456789,
                     'visible' => true,
-                    'subscriptionid' => 1
+                    'subscriptionid' => 1,
+                    'location' => 'Test location',
                 ],
                 'actioncallbackapplier' => function(event_interface $event) {
                     $event->testattribute = 'Hello';
@@ -398,7 +404,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                     'timemodified' => 123456789,
                     'timesort' => 123456789,
                     'visible' => true,
-                    'subscriptionid' => 1
+                    'subscriptionid' => 1,
+                    'location' => 'Test location',
                 ],
                 'actioncallbackapplier' => function(event_interface $event) {
                     $event->testattribute = 'Hello';
@@ -432,7 +439,8 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
                     'timemodified' => 123456789,
                     'timesort' => 123456789,
                     'visible' => true,
-                    'subscriptionid' => 1
+                    'subscriptionid' => 1,
+                    'location' => 'Test location',
                 ],
                 'actioncallbackapplier' => function(event_interface $event) {
                     $event->testattribute = 'Hello';
index 47f3f4b..d59bccc 100644 (file)
@@ -203,6 +203,10 @@ class event_mapper_test_action_event implements action_event_interface {
         return $this->event->get_description();
     }
 
+    public function get_location() {
+        return $this->event->get_location();
+    }
+
     public function get_category() {
         return $this->event->get_category();
     }
@@ -293,7 +297,7 @@ class event_mapper_test_event implements event_interface {
     /**
      * Constructor.
      *
-     * @param calendar_event $legacyevent Legacy event to exctract IDs etc from.
+     * @param calendar_event $legacyevent Legacy event to extract IDs etc from.
      */
     public function __construct($legacyevent = null) {
         if ($legacyevent) {
@@ -322,6 +326,10 @@ class event_mapper_test_event implements event_interface {
         return new event_description('asdf', 1);
     }
 
+    public function get_location() {
+        return 'Cube office';
+    }
+
     public function get_category() {
         return $this->categoryproxy;
     }
index 8b28046..3a70913 100644 (file)
@@ -58,7 +58,8 @@ class core_calendar_event_testcase extends advanced_testcase {
             $constructorparams['type'],
             $constructorparams['times'],
             $constructorparams['visible'],
-            $constructorparams['subscription']
+            $constructorparams['subscription'],
+            $constructorparams['location']
         );
 
         foreach ($constructorparams as $name => $value) {
@@ -98,7 +99,8 @@ class core_calendar_event_testcase extends advanced_testcase {
                         (new \DateTimeImmutable())->setTimestamp(time())
                     ),
                     'visible' => true,
-                    'subscription' => new std_proxy(1, $lamecallable)
+                    'subscription' => new std_proxy(1, $lamecallable),
+                    'location' => 'Test',
                 ]
             ],
         ];
index 9fcf147..28b42d3 100644 (file)
@@ -1516,6 +1516,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1574,6 +1575,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1637,6 +1639,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1698,6 +1701,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1762,6 +1766,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1826,6 +1831,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1894,6 +1900,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -1959,6 +1966,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -2026,6 +2034,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -2098,6 +2107,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -2171,6 +2181,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'format' => 1,
                 'itemid' => 0
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
@@ -2243,6 +2254,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'text' => '',
                 'format' => 1,
             ],
+            'location' => 'Test',
             'duration' => 1,
             'timedurationuntil' => [
                 'day' => $timedurationuntil->format('j'),
index d414e63..3130b22 100644 (file)
@@ -8,6 +8,7 @@ SUMMARY:Event on 2-15-2017
 DESCRIPTION:Event on 2-15-2017
 CLASS:PUBLIC
 LAST-MODIFIED:20170226T014326Z
+LOCATION:Some place
 DTSTAMP:20170226T014355Z
 DTSTART;VALUE=DATE:20170214
 DTEND;VALUE=DATE:20170215
index aa53fd4..92f2e2e 100644 (file)
@@ -136,7 +136,8 @@ class action_event_test_factory implements event_factory_interface {
                 (new \DateTimeImmutable())->setTimestamp($record->timemodified)
             ),
             !empty($record->visible),
-            $subscription
+            $subscription,
+            $record->location
         );
 
         $action = new action(
index 6c6e753..bf8b6ca 100644 (file)
@@ -61,6 +61,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             [
                 'name' => 'Start of assignment',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -74,6 +75,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             ], [
                 'name' => 'Start of lesson',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -272,6 +274,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             [
                 'name' => 'Assignment 1 due date',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 0,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -285,6 +288,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - User override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => 0,
                 'groupid' => 0,
@@ -299,6 +303,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - Group A override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group1->id,
@@ -313,6 +318,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - Group B override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group2->id,
@@ -372,6 +378,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
@@ -387,6 +394,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
index ff2e065..a6466dc 100644 (file)
@@ -662,6 +662,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             [
                 'name' => 'Start of assignment',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -675,6 +676,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             ], [
                 'name' => 'Start of lesson',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -754,6 +756,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             [
                 'name' => 'Assignment 1 due date',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 0,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -767,6 +770,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - User override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => 0,
                 'groupid' => 0,
@@ -781,6 +785,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - Group A override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group1->id,
@@ -795,6 +800,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             ], [
                 'name' => 'Assignment 1 due date - Group B override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group2->id,
@@ -854,6 +860,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
@@ -869,6 +876,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
index 9d8b137..1a927c2 100644 (file)
@@ -53,6 +53,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             [
                 'name' => 'Start of assignment',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -66,6 +67,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             ], [
                 'name' => 'Start of lesson',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -144,6 +146,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             [
                 'name' => 'Assignment 1 due date',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 0,
                 'courseid' => $course->id,
                 'groupid' => 0,
@@ -157,6 +160,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             ], [
                 'name' => 'Assignment 1 due date - User override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => 0,
                 'groupid' => 0,
@@ -171,6 +175,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             ], [
                 'name' => 'Assignment 1 due date - Group A override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group1->id,
@@ -185,6 +190,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             ], [
                 'name' => 'Assignment 1 due date - Group B override',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => $course->id,
                 'groupid' => $group2->id,
@@ -240,6 +246,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
@@ -255,6 +262,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             [
                 'name' => 'Repeating site event',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'courseid' => SITEID,
                 'groupid' => 0,
@@ -294,6 +302,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
                 'name' => 'E1',
                 'eventtype' => 'category',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'categoryid' => $category1->id,
                 'userid' => 2,
@@ -303,6 +312,7 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
                 'name' => 'E2',
                 'eventtype' => 'category',
                 'description' => '',
+                'location' => 'Test',
                 'format' => 1,
                 'categoryid' => $category2->id,
                 'userid' => 2,
@@ -342,4 +352,3 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
         $this->assertCount(2, $events);
     }
 }
-
index 98c1c0b..7d335ea 100644 (file)
@@ -204,7 +204,8 @@ class core_calendar_repeat_event_collection_event_test_factory implements event_
                 (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified)
             ),
             !empty($dbrow->visible),
-            new std_proxy($dbrow->subscriptionid, $identity)
+            new std_proxy($dbrow->subscriptionid, $identity),
+            $dbrow->location
         );
     }
 }
index 4390d5a..eca4de1 100644 (file)
@@ -860,7 +860,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     protected function detail_pair($key, $value, $class ='') {
         $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
         $html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
-        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4 form-inline');
         $html .= html_writer::end_div();
         return $html;
     }
index 11f6866..ce28664 100644 (file)
@@ -85,6 +85,9 @@ class section extends \core_search\base {
      * @return \core_search\document
      */
     public function get_document($record, $options = array()) {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
+
         // Get the context, modinfo, and section.
         try {
             $context = \context_course::instance($record->course);
index 1f1cb78..07f2d9f 100644 (file)
@@ -1600,9 +1600,7 @@ class core_course_external extends external_api {
                             break;
 
                         case 'visible':
-                            if (has_capability('moodle/category:manage', $context)
-                                or has_capability('moodle/category:viewhiddencategories',
-                                        context_system::instance())) {
+                            if (has_capability('moodle/category:viewhiddencategories', $context)) {
                                 $value = clean_param($crit['value'], PARAM_INT);
                                 $conditions[$key] = $value;
                                 $wheres[] = $key . " = :" . $key;
@@ -1712,9 +1710,7 @@ class core_course_external extends external_api {
             if (!isset($excludedcats[$category->id])) {
 
                 // Final check to see if the category is visible to the user.
-                if ($category->visible
-                        or has_capability('moodle/category:viewhiddencategories', context_system::instance())
-                        or has_capability('moodle/category:manage', $context)) {
+                if ($category->visible or has_capability('moodle/category:viewhiddencategories', $context)) {
 
                     $categoryinfo = array();
                     $categoryinfo['id'] = $category->id;
index 045d049..f70371c 100644 (file)
@@ -735,6 +735,43 @@ abstract class format_base {
         return array();
     }
 
+    /**
+     * Prepares values of course or section format options before storing them in DB
+     *
+     * If an option has invalid value it is not returned
+     *
+     * @param array $rawdata associative array of the proposed course/section format options
+     * @param int|null $sectionid null if it is course format option
+     * @return array array of options that have valid values
+     */
+    protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
+        if (!$sectionid) {
+            $allformatoptions = $this->course_format_options(true);
+        } else {
+            $allformatoptions = $this->section_format_options(true);
+        }
+        $data = array_intersect_key($rawdata, $allformatoptions);
+        foreach ($data as $key => $value) {
+            $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
+            $data[$key] = clean_param($value, $option['type']);
+            if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
+                // Value invalid for select element, skip.
+                unset($data[$key]);
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Validates format options for the course
+     *
+     * @param array $data data to insert/update
+     * @return array array of options that have valid values
+     */
+    public function validate_course_format_options(array $data) : array {
+        return $this->validate_format_options($data);
+    }
+
     /**
      * Updates format options for a course or section
      *
@@ -747,6 +784,7 @@ abstract class format_base {
      */
     protected function update_format_options($data, $sectionid = null) {
         global $DB;
+        $data = $this->validate_format_options((array)$data, $sectionid);
         if (!$sectionid) {
             $allformatoptions = $this->course_format_options();
             $sectionid = 0;
@@ -772,7 +810,6 @@ abstract class format_base {
                       'sectionid' => $sectionid
                     ), '', 'name,id,value');
         $changed = $needrebuild = false;
-        $data = (array)$data;
         foreach ($defaultoptions as $key => $value) {
             if (isset($records[$key])) {
                 if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) {
index 394bf21..16a2c66 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.6 ===
+* New method validate_format_options() cleans the values of the course/section format options before inserting them
+  in the database. Course format options can now be set in tool_uploadcourse and validation of user-submitted data is important.
+  Note that validate_format_options() is now always called when somebody creates or edits course or section and also
+  during restore and course upload. Default implementation validates against the definition of the form elements for
+  format options.
+
 === 3.5 ===
 * Course formats should overwrite get_config_for_external function to return the course format settings viewable by the
   current user.
index 1accadd..23fe7f8 100644 (file)
@@ -77,15 +77,11 @@ class course_reset_form extends moodleform {
         $mform->addElement('header', 'groupheader', get_string('groups'));
 
         $mform->addElement('checkbox', 'reset_groups_remove', get_string('deleteallgroups', 'group'));
-        $mform->setAdvanced('reset_groups_remove');
         $mform->addElement('checkbox', 'reset_groups_members', get_string('removegroupsmembers', 'group'));
-        $mform->setAdvanced('reset_groups_members');
         $mform->disabledIf('reset_groups_members', 'reset_groups_remove', 'checked');
 
         $mform->addElement('checkbox', 'reset_groupings_remove', get_string('deleteallgroupings', 'group'));
-        $mform->setAdvanced('reset_groupings_remove');
         $mform->addElement('checkbox', 'reset_groupings_members', get_string('removegroupingsmembers', 'group'));
-        $mform->setAdvanced('reset_groupings_members');
         $mform->disabledIf('reset_groupings_members', 'reset_groupings_remove', 'checked');
 
         $unsupported_mods = array();
index fdd0380..19cb9b2 100644 (file)
@@ -201,6 +201,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // Set the required capabilities by the external function.
         $context = context_system::instance();
         $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
+        $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 
         // Retrieve category1 + sub-categories except not visible ones
         $categories = core_course_external::get_categories(array(
@@ -278,10 +279,10 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals($DB->count_records('course_categories'), count($categories));
 
-        $this->unassignUserCapability('moodle/category:manage', $context->id, $roleid);
+        $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
 
-        // Ensure maxdepthcategory is 2 and retrieve all categories without category:manage capability. It should retrieve all
-        // visible categories as well.
+        // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
+        // It should retrieve all visible categories as well.
         set_config('maxcategorydepth', 2);
         $categories = core_course_external::get_categories();
 
index a8028a4..da840ff 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Javascript Object Notation (.json)';
-$string['privacy:metadata'] = 'The JavaScript Object Notation data format plugin does not store any personal data.';
-$string['shortname'] = 'JSON';
+$string['privacy:metadata'] = 'The JavaScript Object Notation (JSON) data format plugin does not store any personal data.';
+$string['shortname'] = 'JavaScript Object Notation (JSON)';
 
index 1d2773c..5eeaf6d 100644 (file)
@@ -24,5 +24,5 @@
 
 $string['dataformat'] = 'OpenDocument (.ods)';
 $string['privacy:metadata'] = 'The OpenDocument data format plugin does not store any personal data.';
-$string['shortname'] = 'OpenDoc';
+$string['shortname'] = 'OpenDocument';
 
index e815bf9..508fa9a 100644 (file)
@@ -64,9 +64,9 @@ It could look something like this:
 </pre>';
 $string['privacy:metadata:enrol_flatfile'] = 'The Flat file (CSV) enrolment plugin may store personal data relating to future enrolments in the enrol_flatfile table.';
 $string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected at the given date.';
-$string['privacy:metadata:enrol_flatfile:courseid'] = 'The courseid to which the enrolment relates.';
-$string['privacy:metadata:enrol_flatfile:roleid'] = 'The id of the role to be assigned or revoked.';
+$string['privacy:metadata:enrol_flatfile:courseid'] = 'The course ID to which the enrolment relates';
+$string['privacy:metadata:enrol_flatfile:roleid'] = 'The ID of the role to be assigned or unassigned';
 $string['privacy:metadata:enrol_flatfile:timestart'] = 'The time at which the enrolment change starts.';
 $string['privacy:metadata:enrol_flatfile:timeend'] = 'The time at which the enrolment change ends.';
 $string['privacy:metadata:enrol_flatfile:timemodified'] = 'The modification time of this enrolment change.';
-$string['privacy:metadata:enrol_flatfile:userid'] = 'The id of the user to which the role assignment relates.';
+$string['privacy:metadata:enrol_flatfile:userid'] = 'The ID of the user to which the role assignment relates';
index 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 a6010bf..50e568e 100644 (file)
@@ -79,10 +79,8 @@ class filter_mathjaxloader extends moodle_text_filter {
      * @param context $context The current context.
      */
     public function setup($page, $context) {
-        // This only requires execution once per request.
-        static $jsinitialised = false;
 
-        if (empty($jsinitialised)) {
+        if ($page->requires->should_create_one_time_item_now('filter_mathjaxloader-scripts')) {
             $url = get_config('filter_mathjaxloader', 'httpsurl');
             $lang = $this->map_language_code(current_language());
             $url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
@@ -102,8 +100,6 @@ class filter_mathjaxloader extends moodle_text_filter {
             $params = array('mathjaxconfig' => $config, 'lang' => $lang);
 
             $page->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.configure', array($params));
-
-            $jsinitialised = true;
         }
     }
 
index 880a8c5..cf656d3 100644 (file)
@@ -979,6 +979,16 @@ $string['requires'] = 'Requires';
 $string['purgecaches'] = 'Purge all caches';
 $string['purgecachesconfirm'] = 'Moodle can cache themes, javascript, language strings, filtered text, rss feeds and many other pieces of calculated data.  Purging these caches will delete that data from the server and force browsers to refetch data, so that you can be sure you are seeing the most up-to-date values produced by the current code.  There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
 $string['purgecachesfinished'] = 'All caches were purged.';
+$string['purgecachesnoneselected'] = 'Select one or more caches to purge';
+$string['purgecachespage'] = 'Purge caches';
+$string['purgefiltercache'] = 'Text filters';
+$string['purgejscache'] = 'JavaScript';
+$string['purgelangcache'] = 'Language strings';
+$string['purgemuc'] = 'All MUC caches';
+$string['purgeothercaches'] = 'All file and miscellaneous caches';
+$string['purgeselectedcaches'] = 'Purge selected caches';
+$string['purgeselectedcachesfinished'] = 'The selected caches were purged.';
+$string['purgethemecache'] = 'Themes';
 $string['requestcategoryselection'] = 'Enable category selection';
 $string['restorecourse'] = 'Restore course';
 $string['restorernewroleid'] = 'Restorers\' role in courses';
index efd2552..9d4715e 100644 (file)
@@ -88,14 +88,14 @@ $string['privacy:metadata:analytics:indicatorcalc:starttime'] = 'Calculation sta
 $string['privacy:metadata:analytics:indicatorcalc:endtime'] = 'Calculation end time';
 $string['privacy:metadata:analytics:indicatorcalc:contextid'] = 'The context';
 $string['privacy:metadata:analytics:indicatorcalc:sampleorigin'] = 'The origin table of the sample';
-$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample ID';
 $string['privacy:metadata:analytics:indicatorcalc:indicator'] = 'The indicator calculator class';
 $string['privacy:metadata:analytics:indicatorcalc:value'] = 'The calculated value';
 $string['privacy:metadata:analytics:indicatorcalc:timecreated'] = 'When the prediction was made';
 $string['privacy:metadata:analytics:predictions'] = 'Predictions';
-$string['privacy:metadata:analytics:predictions:modelid'] = 'The model id';
+$string['privacy:metadata:analytics:predictions:modelid'] = 'The model ID';
 $string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
-$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample id';
+$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample ID';
 $string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time splitting method';
 $string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
 $string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
@@ -104,7 +104,7 @@ $string['privacy:metadata:analytics:predictions:timecreated'] = 'When the predic
 $string['privacy:metadata:analytics:predictions:timestart'] = 'Calculations time start';
 $string['privacy:metadata:analytics:predictions:timeend'] = 'Calculations time end';
 $string['privacy:metadata:analytics:predictionactions'] = 'Prediction actions';
-$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction id';
+$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction ID';
 $string['privacy:metadata:analytics:predictionactions:userid'] = 'The user that made the action';
 $string['privacy:metadata:analytics:predictionactions:actionname'] = 'The action name';
 $string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the prediction action was performed';
index 0ca7302..2be7f58 100644 (file)
@@ -144,7 +144,7 @@ $string['privacy:metadata:post:userid'] = 'The ID of the user who added the blog
 $string['privacy:metadata:post:subject'] = 'Blog entry title.';
 $string['privacy:metadata:post:summary'] = 'Blog entry.';
 $string['privacy:metadata:post:content'] = 'The content of an external blog entry.';
-$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL.';
+$string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL';
 $string['privacy:metadata:post:publishstate'] = 'Whether the entry is visible to others or not';
 $string['privacy:metadata:post:created'] = 'Date when the entry was created.';
 $string['privacy:metadata:post:lastmodified'] = 'Date when the entry was last modified.';
index cfda2a1..5f0ea26 100644 (file)
@@ -62,7 +62,7 @@ $string['invalidtheme'] = 'Cohort theme does not exist';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
-$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes column names.';
+$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes the correct column names. Note that Upload cohorts only allows you to add new users to an existing cohort and does not allow removal from an existing cohort.';
 $string['namefieldempty'] = 'Field name can not be empty';
 $string['newnamefor'] = 'New name for cohort {$a}';
 $string['newidnumberfor'] = 'New ID number for cohort {$a}';
index a3f66c3..606cac2 100644 (file)
@@ -139,7 +139,7 @@ $string['privacy:metadata:evidence:actionuserid'] = 'The user performing the act
 $string['privacy:metadata:evidence:desca'] = 'The optional parameters of the translatable evidence description';
 $string['privacy:metadata:evidence:desccomponent'] = 'The component of the translatable evidence description';
 $string['privacy:metadata:evidence:descidentifier'] = 'An identifier of the translatable evidence description';
-$string['privacy:metadata:evidence:grade'] = 'The grade associted with the evidence';
+$string['privacy:metadata:evidence:grade'] = 'The grade associated with the evidence';
 $string['privacy:metadata:evidence:note'] = 'A non-localised note attached to the evidence';
 $string['privacy:metadata:evidence:url'] = 'A URL associated with the evidence';
 $string['privacy:metadata:plan:description'] = 'The description of the learning plan';
index 56efe38..dc1a66f 100644 (file)
@@ -182,7 +182,7 @@ $string['overallaggregation_any'] = 'Course is complete when ANY of the conditio
 $string['pending'] = 'Pending';
 $string['periodpostenrolment'] = 'Period post enrolment';
 $string['privacy:metadata:completionstate'] = 'If the activity has been completed';
-$string['privacy:metadata:course'] = 'A course identifier.';
+$string['privacy:metadata:course'] = 'A course identifier';
 $string['privacy:metadata:coursecompletedsummary'] = 'Stores information about users who have completed criteria in a course';
 $string['privacy:metadata:coursemoduleid'] = 'The activity ID';
 $string['privacy:metadata:coursemodulesummary'] = 'Stores activity completion data for a user';
index fcd85b0..4a9937d 100644 (file)
@@ -111,6 +111,8 @@ $string['nosearch'] = 'Don\'t publish hub or courses';
 $string['notregisteredonhub'] = 'Your administrator needs to register this site with Moodle.net before you can share a course.';
 $string['operation'] = 'Actions';
 $string['participantnumberaverage'] = 'Average number of participants ({$a})';
+$string['policyagreed'] = 'Privacy notice and data processing agreement';
+$string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Privacy notice and data processing agreement</a> for Moodle.net';
 $string['postaladdress'] = 'Postal address';
 $string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
 $string['postsnumber'] = 'Number of posts ({$a})';
index efd628b..88883be 100644 (file)
@@ -1020,9 +1020,9 @@ $string['idnumbergroup_help'] = 'The ID number of a group is only used when matc
 $string['idnumbergrouping'] = 'Grouping ID number';
 $string['idnumbergrouping_help'] = 'The ID number of a grouping is only used when matching the grouping against external systems and is not displayed anywhere on the site. If the grouping has an official code name it may be entered, otherwise the field can be left blank.';
 $string['idnumbermod'] = 'ID number';
-$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity for grade calculation purposes. If the activity is not included in any grade calculation then the ID number field may be left blank.
+$string['idnumbermod_help'] = 'Setting an ID number provides a way of identifying the activity or resource for purposes such as grade calculation or custom reporting. Otherwise the field may be left blank.
 
-The ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
+For gradable activities, the ID number can also be set in the gradebook, though it can only be edited on the activity settings page.';
 $string['idnumbertaken'] = 'This ID number is already taken';
 $string['imagealt'] = 'Picture description';
 $string['import'] = 'Import';
@@ -1565,7 +1565,7 @@ $string['privacy:metadata:events_queue'] = 'The queue of user events waiting to
 $string['privacy:metadata:events_queue:eventdata'] = 'The data stored in the event.';
 $string['privacy:metadata:events_queue:stackdump'] = 'Any stacktrace associated with this event.';
 $string['privacy:metadata:events_queue:timecreated'] = 'The time that this event was created.';
-$string['privacy:metadata:events_queue:userid'] = 'The userid associated with this event.';
+$string['privacy:metadata:events_queue:userid'] = 'The user ID associated with this event';
 $string['privacy:metadata:log'] = 'A collection of past events';
 $string['privacy:metadata:log:action'] = 'A description of the action';
 $string['privacy:metadata:log:cmid'] = 'cmid';
index 5186147..3010079 100644 (file)
@@ -60,11 +60,11 @@ $string['personal'] = 'personal';
 $string['personalnotes'] = 'Personal notes';
 $string['privacy:metadata:core_notes'] = 'The Notes component stores user notes within the core subsystem.';
 $string['privacy:metadata:core_notes:content'] = 'The content of the note.';
-$string['privacy:metadata:core_notes:courseid'] = 'The Id of the course associated with the note.';
+$string['privacy:metadata:core_notes:courseid'] = 'The ID of the course associated with the note';
 $string['privacy:metadata:core_notes:created'] = 'The creation date/time for the note.';
 $string['privacy:metadata:core_notes:lastmodified'] = 'The last modified date/time for the note.';
 $string['privacy:metadata:core_notes:publishstate'] = 'The publish state of the note.';
-$string['privacy:metadata:core_notes:userid'] = 'The Id of the user associated with the note.';
+$string['privacy:metadata:core_notes:userid'] = 'The ID of the user associated with the note';
 $string['publishstate'] = 'Context';
 $string['publishstate_help'] = 'A note\'s context determines who can see the note in everyday use. Users should be aware that all notes, including personal ones, may be disclosed under the laws of their jurisdictions.
 
index a5fae08..1255ad8 100644 (file)
@@ -473,17 +473,17 @@ $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
 $string['privacy:metadata:preference:showadvanced'] = 'Handle the toggle advanced mode button.';
 $string['privacy:metadata:role_assignments'] = 'Role assignments';
 $string['privacy:metadata:role_assignments:component'] = 'Plugin responsible for role assignment, empty when manually assigned.';
-$string['privacy:metadata:role_assignments:itemid'] = 'The Id of enrolment/auth instance responsible for this role assignment.';
-$string['privacy:metadata:role_assignments:modifierid'] = 'The Id of the user who created or modified the role assignment.';
-$string['privacy:metadata:role_assignments:roleid'] = 'The Id of the role.';
+$string['privacy:metadata:role_assignments:itemid'] = 'The ID of enrolment/auth instance responsible for this role assignment';
+$string['privacy:metadata:role_assignments:modifierid'] = 'The ID of the user who created or modified the role assignment';
+$string['privacy:metadata:role_assignments:roleid'] = 'The ID of the role';
 $string['privacy:metadata:role_assignments:tableexplanation'] = 'This table stores the assigned roles in each context.';
 $string['privacy:metadata:role_assignments:timemodified'] = 'The date when the role assignment was created or modified.';
-$string['privacy:metadata:role_assignments:userid'] = 'The Id of the user.';
+$string['privacy:metadata:role_assignments:userid'] = 'The ID of the user';
 $string['privacy:metadata:role_capabilities'] = 'Role capabilities';
 $string['privacy:metadata:role_capabilities:capability'] = 'The name of the capability.';
-$string['privacy:metadata:role_capabilities:modifierid'] = 'The Id of the user who created or modified the capability.';
+$string['privacy:metadata:role_capabilities:modifierid'] = 'The ID of the user who created or modified the capability';
 $string['privacy:metadata:role_capabilities:permission'] = 'The permission for a capability: inherit, allow, prevent or prohibit.';
-$string['privacy:metadata:role_capabilities:roleid'] = 'The Id of the role.';
-$string['privacy:metadata:role_capabilities:tableexplanation'] = 'This table stores the capabilities and the override capabilities for a particular role in a particular context.';
+$string['privacy:metadata:role_capabilities:roleid'] = 'The ID of the role';
+$string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabilities and override capabilities for a particular role in a particular context';
 $string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
 $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
index 09917b6..e956098 100644 (file)
@@ -68,7 +68,7 @@ $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';
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 187ab85..dc09ffb 100644 (file)
@@ -43,7 +43,8 @@ class registration {
     /** @var Fields used in a site registration form.
      * IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */
     const FORM_FIELDS = ['name', 'description', 'contactname', 'contactemail', 'contactphone', 'imageurl', 'privacy', 'street',
-        'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail', 'language'];
+        'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
+        'language', 'policyagreed'];
 
     /** @var List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added.
      * If site was already registered, admin will be promted to confirm new registration data manually. Until registration is manually confirmed,
index ee7368d..510a2da 100644 (file)
@@ -64,7 +64,8 @@ class site_registration_form extends \moodleform {
             'language' => explode('_', current_language())[0],
             'geolocation' => '',
             'emailalert' => 1,
-            'commnews' => 1
+            'commnews' => 1,
+            'policyagreed' => 0
 
         ]);
 
@@ -151,6 +152,10 @@ class site_registration_form extends \moodleform {
         $mform->addElement('hidden', 'imageurl', ''); // TODO: temporary.
         $mform->setType('imageurl', PARAM_URL);
 
+        $mform->addElement('checkbox', 'policyagreed', get_string('policyagreed', 'hub'),
+            get_string('policyagreeddesc', 'hub', HUB_MOODLEORGHUBURL . '/privacy'));
+        $mform->addRule('policyagreed', $strrequired, 'required', null, 'client');
+
         $mform->addElement('header', 'sitestats', get_string('sendfollowinginfo', 'hub'));
         $mform->setExpanded('sitestats', !empty($highlightfields));
         $mform->addElement('static', 'urlstring', get_string('siteurl', 'hub'), $siteinfo['url']);
@@ -182,7 +187,9 @@ class site_registration_form extends \moodleform {
         if (empty($siteinfo['commnewsnewemail'])) {
             $siteinfo['commnewsemail'] = '';
         }
-        $this->set_data($siteinfo);
+
+        // Set data. Always require to check policyagreed even if it was checked earlier.
+        $this->set_data(['policyagreed' => 0] + $siteinfo);
     }
 
     /**
@@ -262,7 +269,8 @@ class site_registration_form extends \moodleform {
 
             if (debugging('', DEBUG_DEVELOPER)) {
                 // Display debugging message for developers who added fields to the form and forgot to add them to registration::FORM_FIELDS.
-                $keys = array_diff(array_keys((array)$data), ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
+                $keys = array_diff(array_keys((array)$data),
+                    ['returnurl', 'mform_isexpanded_id_sitestats', 'submitbutton', 'update']);
                 if ($extrafields = array_diff($keys, registration::FORM_FIELDS)) {
                     debugging('Found extra fields in the form results: ' . join(', ', $extrafields), DEBUG_DEVELOPER);
                 }
index 71d15a6..75abeda 100644 (file)
@@ -243,6 +243,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/item' => 'fa-circle',
             'core:i/loading' => 'fa-circle-o-notch fa-spin',
             'core:i/loading_small' => 'fa-circle-o-notch fa-spin',
+            'core:i/location' => 'fa-map-marker',
             'core:i/lock' => 'fa-lock',
             'core:i/log' => 'fa-list-alt',
             'core:i/mahara_host' => 'fa-id-badge',
index b9b9003..c7d1a81 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"
 >
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="subscriptionid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The event_subscription id this event is associated with."/>
         <FIELD NAME="priority" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The event's display priority. For multiple events with the same module name, instance and eventtype (e.g. for group overrides), the one with the higher priority will be displayed."/>
+        <FIELD NAME="location" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Event Location"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </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..dd3643f 100644 (file)
@@ -2233,5 +2233,40 @@ 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);
+    }
+
+    if ($oldversion < 2018062800.03) {
+        // Define field location to be added to event.
+        $table = new xmldb_table('event');
+        $field = new xmldb_field('location', XMLDB_TYPE_TEXT, null, null, null, null, null, 'priority');
+
+        // Conditionally launch add field location.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018062800.03);
+    }
+
     return true;
 }
index 0a9753b..bbb9289 100644 (file)
@@ -302,15 +302,36 @@ class filter_manager {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class null_filter_manager {
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @param string $text The text to filter
+     * @param context $context not used.
+     * @param array $options not used
+     * @param array $skipfilters not used
+     * @return string resulting text.
+     */
     public function filter_text($text, $context, array $options = array(),
             array $skipfilters = null) {
         return $text;
     }
 
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @param string $string The text to filter
+     * @param context $context not used.
+     * @return string resulting string
+     */
     public function filter_string($string, $context) {
         return $string;
     }
 
+    /**
+     * As for the equivalent {@link filter_manager} method.
+     *
+     * @deprecated Since Moodle 3.0 MDL-50491.
+     */
     public function text_filtering_hash() {
         throw new coding_exception('filter_manager::text_filtering_hash() can not be used any more');
     }
@@ -429,9 +450,9 @@ abstract class moodle_text_filter {
     /**
      * Override this function to actually implement the filtering.
      *
-     * @param $text some HTML content.
+     * @param string $text some HTML content to process.
      * @param array $options options passed to the filters
-     * @return the HTML content after the filtering has been applied.
+     * @return string the HTML content after the filtering has been applied.
      */
     public abstract function filter($text, array $options = array());
 }
@@ -520,8 +541,6 @@ function filter_get_name($filter) {
  * sorted in alphabetical order of name.
  */
 function filter_get_all_installed() {
-    global $CFG;
-
     $filternames = array();
     foreach (core_component::get_plugin_list('filter') as $filter => $fulldir) {
         if (is_readable("$fulldir/filter.php")) {
@@ -692,16 +711,21 @@ function filter_is_enabled($filtername) {
  * @return array where the keys and values are both the filter name, like 'tex'.
  */
 function filter_get_globally_enabled() {
-    static $enabledfilters = null;
-    if (is_null($enabledfilters)) {
-        $filters = filter_get_global_states();
-        $enabledfilters = array();
-        foreach ($filters as $filter => $filerinfo) {
-            if ($filerinfo->active != TEXTFILTER_DISABLED) {
-                $enabledfilters[$filter] = $filter;
-            }
+    $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_filter', 'global_filters');
+    $enabledfilters = $cache->get('enabled');
+    if ($enabledfilters !== false) {
+        return $enabledfilters;
+    }
+
+    $filters = filter_get_global_states();
+    $enabledfilters = array();
+    foreach ($filters as $filter => $filerinfo) {
+        if ($filerinfo->active != TEXTFILTER_DISABLED) {
+            $enabledfilters[$filter] = $filter;
         }
     }
+
+    $cache->set('enabled', $enabledfilters);
     return $enabledfilters;
 }
 
@@ -1240,7 +1264,7 @@ function filter_phrases($text, &$link_array, $ignoretagsopen=NULL, $ignoretagscl
         // A list of open/close tags that we should not replace within
         // Extended to include <script>, <textarea>, <select> and <a> tags
         // Regular expression allows tags with or without attributes
-        $filterignoretagsopen  = array('<head>' , '<nolink>' , '<span class="nolink">',
+        $filterignoretagsopen  = array('<head>' , '<nolink>' , '<span(\s[^>]*?)?class="nolink"(\s[^>]*?)?>',
                 '<script(\s[^>]*?)?>', '<textarea(\s[^>]*?)?>',
                 '<select(\s[^>]*?)?>', '<a(\s[^>]*?)?>');
         $filterignoretagsclose = array('</head>', '</nolink>', '</span>',
@@ -1428,9 +1452,10 @@ function filter_phrases($text, &$link_array, $ignoretagsopen=NULL, $ignoretagscl
 }
 
 /**
- * @todo Document this function
- * @param array $linkarray
- * @return array
+ * Remove duplicate from a list of {@link filterobject}.
+ *
+ * @param filterobject[] $linkarray a list of filterobject.
+ * @return filterobject[] the same list, but with dupicates removed.
  */
 function filter_remove_duplicates($linkarray) {
 
index 34fe56b..47cf0b1 100644 (file)
@@ -1627,18 +1627,61 @@ function get_users_from_config($value, $capability, $includeadmins = true) {
 /**
  * Invalidates browser caches and cached data in temp.
  *
- * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
- * {@link phpunit_util::reset_dataroot()}
- *
  * @return void
  */
 function purge_all_caches() {
-    global $CFG, $DB;
+    purge_caches();
+}
 
-    reset_text_filters_cache();
-    js_reset_all_caches();
-    theme_reset_all_caches();
-    get_string_manager()->reset_caches();
+/**
+ * Selectively invalidate different types of cache.
+ *
+ * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
+ * areas alone or in combination.
+ *
+ * @param bool[] $options Specific parts of the cache to purge. Valid options are:
+ *        'muc'    Purge MUC caches?
+ *        'theme'  Purge theme cache?
+ *        'lang'   Purge language string cache?
+ *        'js'     Purge javascript cache?
+ *        'filter' Purge text filter cache?
+ *        'other'  Purge all other caches?
+ */
+function purge_caches($options = []) {
+    $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'filter', 'other'], false);
+    if (empty(array_filter($options))) {
+        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
+    } else {
+        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
+    }
+    if ($options['muc']) {
+        cache_helper::purge_all();
+    }
+    if ($options['theme']) {
+        theme_reset_all_caches();
+    }
+    if ($options['lang']) {
+        get_string_manager()->reset_caches();
+    }
+    if ($options['js']) {
+        js_reset_all_caches();
+    }
+    if ($options['filter']) {
+        reset_text_filters_cache();
+    }
+    if ($options['other']) {
+        purge_other_caches();
+    }
+}
+
+/**
+ * Purge all non-MUC caches not otherwise purged in purge_caches.
+ *
+ * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
+ * {@link phpunit_util::reset_dataroot()}
+ */
+function purge_other_caches() {
+    global $DB, $CFG;
     core_text::reset_caches();
     if (class_exists('core_plugin_manager')) {
         core_plugin_manager::reset_caches();
@@ -1652,7 +1695,6 @@ function purge_all_caches() {
     }
 
     $DB->reset_caches();
-    cache_helper::purge_all();
 
     // Purge all other caches: rss, simplepie, etc.
     clearstatcache();
index 00e9fff..a2ee601 100644 (file)
@@ -245,6 +245,7 @@ function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'l
  */
 function theme_reset_all_caches() {
     global $CFG, $PAGE;
+    require_once("{$CFG->libdir}/filelib.php");
 
     $next = theme_get_next_revision();
     theme_set_revision($next);
@@ -257,6 +258,12 @@ function theme_reset_all_caches() {
     // Purge compiled post processed css.
     cache::make('core', 'postprocessedcss')->purge();
 
+    // Delete all old theme localcaches.
+    $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
+    foreach ($themecachedirs as $localcachedir) {
+        fulldelete($localcachedir);
+    }
+
     if ($PAGE) {
         $PAGE->reload_theme();
     }
@@ -662,6 +669,12 @@ class theme_config {
      */
     public $remapiconcache = [];
 
+    /**
+     * The name of the function to call to get precompiled CSS.
+     * @var string
+     */
+    public $precompiledcsscallback = null;
+
     /**
      * Load the config.php file for a particular theme, and return an instance
      * of this class. (That is, this is a factory method.)
@@ -739,7 +752,8 @@ class theme_config {
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
-            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition', 'iconsystem');
+            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
+            'iconsystem', 'precompiledcsscallback');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -1090,7 +1104,13 @@ class theme_config {
                 } else {
                     if ($type === 'theme' && $identifier === self::SCSS_KEY) {
                         // We need the content from SCSS because this is the SCSS file from the theme.
-                        $csscontent .= $this->get_css_content_from_scss(false);
+                        if ($compiled = $this->get_css_content_from_scss(false)) {
+                            $csscontent .= $compiled;
+                        } else {
+                            // The compiler failed so default back to any precompiled css that might
+                            // exist.
+                            $csscontent .= $this->get_precompiled_css_content();
+                        }
                     } else if ($type === 'theme' && $identifier === $this->lessfile) {
                         // We need the content from LESS because this is the LESS file from the theme.
                         $csscontent .= $this->get_css_content_from_less(false);
@@ -1487,6 +1507,26 @@ class theme_config {
         return $compiled;
     }
 
+    /**
+     * Return the precompiled CSS if the precompiledcsscallback exists.
+     *
+     * @return string Return compiled css.
+     */
+    public function get_precompiled_css_content() {
+        $configs = [$this] + $this->parent_configs;
+        $css = '';
+
+        foreach ($configs as $config) {
+            if (isset($config->precompiledcsscallback)) {
+                $function = $config->precompiledcsscallback;
+                if (function_exists($function)) {
+                    $css .= $function($this);
+                }
+            }
+        }
+        return $css;
+    }
+
     /**
      * Get the icon system to use.
      *
index 5b50374..47d8728 100644 (file)
@@ -81,11 +81,25 @@ class renderer_base {
         global $CFG;
 
         if ($this->mustache === null) {
+            require_once("{$CFG->libdir}/filelib.php");
+
             $themename = $this->page->theme->name;
             $themerev = theme_get_revision();
 
+            // Create new localcache directory.
             $cachedir = make_localcache_directory("mustache/$themerev/$themename");
 
+            // Remove old localcache directories.
+            $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
+            foreach ($mustachecachedirs as $localcachedir) {
+                $cachedrev = [];
+                preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
+                $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
+                if ($cachedrev > 0 && $cachedrev < $themerev) {
+                    fulldelete($localcachedir);
+                }
+            }
+
             $loader = new \core\output\mustache_filesystem_loader();
             $stringhelper = new \core\output\mustache_string_helper();
             $quotehelper = new \core\output\mustache_quote_helper();
diff --git a/lib/tests/behat/behat_filters.php b/lib/tests/behat/behat_filters.php
new file mode 100644 (file)
index 0000000..2b06662
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2018 the Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Note: You cannot use MOODLE_INTERNAL test here, or include files which do so.
+// This file is required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../behat/behat_base.php');
+
+/**
+ * Steps definitions related to filters.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2018 the Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_filters extends behat_base {
+
+    /**
+     * Set the global filter configuration.
+     *
+     * @Given /^the "(?P<filter_name>(?:[^"]|\\")*)" filter is "(on|off|disabled)"$/
+     *
+     * @param string $filtername the name of a filter, e.g. 'glossary'.
+     * @param string $statename 'on', 'off' or 'disabled'.
+     */
+    public function the_filter_is($filtername, $statename) {
+        require_once(__DIR__ . '/../../filterlib.php');
+
+        switch ($statename) {
+            case 'on':
+                $state = TEXTFILTER_ON;
+                break;
+            case 'off':
+                $state = TEXTFILTER_OFF;
+                break;
+            case 'disabled':
+                $state = TEXTFILTER_DISABLED;
+                break;
+            default:
+                throw new coding_exception('Unknown filter state: ' . $statename);
+        }
+        filter_set_global_state($filtername, $state);
+    }
+}
index b8af6ff..c3aab08 100644 (file)
@@ -676,6 +676,17 @@ class core_filterlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('performance_measuring_filter_manager', $filterman);
     }
 
+    public function test_filter_get_globally_enabled_default() {
+        $enabledfilters = filter_get_globally_enabled();
+        $this->assertArrayNotHasKey('glossary', $enabledfilters);
+    }
+
+    public function test_filter_get_globally_enabled_after_change() {
+        filter_set_global_state('glossary', TEXTFILTER_ON);
+        $enabledfilters = filter_get_globally_enabled();
+        $this->assertArrayHasKey('glossary', $enabledfilters);
+    }
+
     public function test_filter_get_globally_enabled_filters_with_config() {
         $this->setup_available_in_context_tests();
 
index 10e54bf..ae19f01 100644 (file)
@@ -40,8 +40,8 @@ $string['refetch'] = 'Re-fetch up to date state from remote hosts';
 $string['privacy:metadata:mnetservice_enrol_enrolments'] = 'Remote enrolment service';
 $string['privacy:metadata:mnetservice_enrol_enrolments:enroltime'] = 'The date/time of when the enrolment was modified.';
 $string['privacy:metadata:mnetservice_enrol_enrolments:enroltype'] = 'The name of the enrol plugin at the remote server that was used to enrol our student into their course.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:hostid'] = 'The Id of the remote MNet host.';
+$string['privacy:metadata:mnetservice_enrol_enrolments:hostid'] = 'The ID of the remote MNet host';
 $string['privacy:metadata:mnetservice_enrol_enrolments:remotecourseid'] = 'ID of the course at  the remote server.';
 $string['privacy:metadata:mnetservice_enrol_enrolments:rolename'] = 'The name of the role at  the remote server.';
 $string['privacy:metadata:mnetservice_enrol_enrolments:tableexplanation'] = 'This table stores the information about enrolments of our local users in courses on remote hosts.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:userid'] = 'The Id of our local user on this server.';
+$string['privacy:metadata:mnetservice_enrol_enrolments:userid'] = 'The ID of our local user on this server';
index 52d8681..b8c7551 100644 (file)
@@ -28,7 +28,7 @@ $string['enabled'] = 'Feedback comments';
 $string['enabled_help'] = 'If enabled, the marker can leave feedback comments for each submission. ';
 $string['pluginname'] = 'Feedback comments';
 $string['privacy:commentpath'] = 'Feedback comments';
-$string['privacy:metadata:assignmentid'] = 'Assignment identifier';
+$string['privacy:metadata:assignmentid'] = 'Assignment ID';
 $string['privacy:metadata:commentpurpose'] = 'The comment text.';
 $string['privacy:metadata:gradepurpose'] = 'The grade ID associated with the comment.';
 $string['privacy:metadata:tablesummary'] = 'This stores comments made by the graders as feedback for the student on their submission.';
index 9ca16b6..882fdc4 100644 (file)
@@ -161,46 +161,46 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
         $navigation3 .= $this->render_toolbar_button('comment_expcol', 'expcolcomments', $this->get_shortcut('expcolcomments'));
         $navigation3 = html_writer::div($navigation3, 'navigation-expcol', array('role' => 'navigation'));
 
-        $toolbar1 = '';
-        $toolbar2 = '';
-        $toolbar3 = '';
-        $toolbar4 = '';
+        $toolbargroup = '';
         $clearfix = html_writer::div('', 'clearfix');
         if (!$widget->readonly) {
-
             // Comments.
+            $toolbar1 = '';
             $toolbar1 .= $this->render_toolbar_button('comment', 'comment', $this->get_shortcut('comment'));
             $toolbar1 .= $this->render_toolbar_button('background_colour_clear', 'commentcolour', $this->get_shortcut('commentcolour'));
-            $toolbar1 = html_writer::div($toolbar1, 'toolbar', array('role'=>'toolbar'));
+            $toolbar1 = html_writer::div($toolbar1, 'toolbar', array('role' => 'toolbar'));
 
             // Select Tool.
+            $toolbar2 = '';
             $toolbar2 .= $this->render_toolbar_button('drag', 'drag', $this->get_shortcut('drag'));
             $toolbar2 .= $this->render_toolbar_button('select', 'select', $this->get_shortcut('select'));
-            $toolbar2 = html_writer::div($toolbar2, 'toolbar', array('role'=>'toolbar'));
+            $toolbar2 = html_writer::div($toolbar2, 'toolbar', array('role' => 'toolbar'));
 
             // Other Tools.
-            $toolbar3 = $this->render_toolbar_button('pen', 'pen', $this->get_shortcut('pen'));
+            $toolbar3 = '';
+            $toolbar3 .= $this->render_toolbar_button('pen', 'pen', $this->get_shortcut('pen'));
             $toolbar3 .= $this->render_toolbar_button('line', 'line', $this->get_shortcut('line'));
             $toolbar3 .= $this->render_toolbar_button('rectangle', 'rectangle', $this->get_shortcut('rectangle'));
             $toolbar3 .= $this->render_toolbar_button('oval', 'oval', $this->get_shortcut('oval'));
             $toolbar3 .= $this->render_toolbar_button('highlight', 'highlight', $this->get_shortcut('highlight'));
             $toolbar3 .= $this->render_toolbar_button('background_colour_clear', 'annotationcolour', $this->get_shortcut('annotationcolour'));
-            $toolbar3 = html_writer::div($toolbar3, 'toolbar', array('role'=>'toolbar'));
+            $toolbar3 = html_writer::div($toolbar3, 'toolbar', array('role' => 'toolbar'));
 
             // Stamps.
-            $toolbar4 .= $this->render_toolbar_button('stamp', 'stamp', 'n');
+            $toolbar4 = '';
+            $toolbar4 .= $this->render_toolbar_button('stamp', 'stamp', $this->get_shortcut('stamp'));
             $toolbar4 .= $this->render_toolbar_button('background_colour_clear', 'currentstamp', $this->get_shortcut('currentstamp'));
             $toolbar4 = html_writer::div($toolbar4, 'toolbar', array('role'=>'toolbar'));
+
+            // Add toolbars to toolbar_group in order of display, and float the toolbar_group right.
+            $toolbars = $toolbar1 . $toolbar2 . $toolbar3 . $toolbar4;
+            $toolbargroup = html_writer::div($toolbars, 'toolbar_group', array('role' => 'toolbar_group'));
         }
 
-        // Toobars written in reverse order because they are floated right.
         $pageheader = html_writer::div($navigation1 .
                                        $navigation2 .
                                        $navigation3 .
-                                       $toolbar4 .
-                                       $toolbar3 .
-                                       $toolbar2 .
-                                       $toolbar1 .
+                                       $toolbargroup .
                                        $clearfix,
                                        'pageheader');
         $body = $pageheader;
index 9d89738..6771820 100644 (file)
@@ -92,20 +92,18 @@ class convert_submissions extends scheduled_task {
             }
 
             mtrace('Convert ' . count($users) . ' submission attempt(s) for assignment ' . $assignmentid);
-            $keepinqueue = false;
-            foreach ($users as $userid) {
-                $combineddocument = document_services::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
-                $status = $combineddocument->get_status();
-
-                switch ($combineddocument->get_status()) {
-                    case combined_document::STATUS_READY:
-                    case combined_document::STATUS_PENDING_INPUT:
-                        // The document has not been converted yet or is somehow still ready.
-                        $keepinqueue = true;
-                        continue;
-                }
 
+            foreach ($users as $userid) {
                 try {
+                    $combineddocument = document_services::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
+                    $status = $combineddocument->get_status();
+
+                    switch ($combineddocument->get_status()) {
+                        case combined_document::STATUS_READY:
+                        case combined_document::STATUS_PENDING_INPUT:
+                            // The document has not been converted yet or is somehow still ready.
+                            continue;
+                    }
                     document_services::get_page_images_for_attempt(
                             $assignment,
                             $userid,
@@ -120,14 +118,12 @@ class convert_submissions extends scheduled_task {
                         );
                 } catch (\moodle_exception $e) {
                     mtrace('Conversion failed with error:' . $e->errorcode);
-                    $keepinqueue = true;
                 }
             }
 
-            if (!$keepinqueue) {
-                // Remove from queue unless requested not to.
-                $DB->delete_records('assignfeedback_editpdf_queue', array('id' => $record->id));
-            }
+            // Remove from queue.
+            $DB->delete_records('assignfeedback_editpdf_queue', array('id' => $record->id));
+
         }
     }
 
index 67c1ab1..0dbffba 100644 (file)
@@ -78,7 +78,7 @@ $string['privacy:metadata:conversionpurpose'] = 'Files are converted to PDFs to
 $string['privacy:metadata:filepurpose'] = 'Stores an annotated PDF with feedback for the user.';
 $string['privacy:metadata:rawtextpurpose'] = 'Stores raw text for the quick data.';
 $string['privacy:metadata:tablepurpose'] = 'Stores teacher specified quicklist comments';
-$string['privacy:metadata:userid'] = 'An identifier for the user.';
+$string['privacy:metadata:userid'] = 'The user ID';
 $string['privacy:path'] = 'PDF Feedback';
 $string['generatingpdf'] = 'Generating the PDF...';
 $string['rectangle'] = 'Rectangle';
index b23aa66..1630264 100644 (file)
     float: left;
 }
 
+.assignfeedback_editpdf_widget .toolbar_group {
+    float: right;
+}
+
 .assignfeedback_editpdf_widget .toolbar button {
     box-shadow: none;
     -moz-box-shadow: none;
 }
 
 .assignfeedback_editpdf_widget .toolbar {
-    float: right;
+    float: left;
 }
 
 .assignfeedback_editpdf_widget .navigation,
     padding: 0;
 }
 
-.assignfeedback_editpdf_dropdown .moodle-dialogue-hd,
-.assignfeedback_editpdf_dropdown .moodle-dialogue-ft {
+.moodle-dialogue-base .assignfeedback_editpdf_dropdown .moodle-dialogue-wrap .moodle-dialogue-hd,
+.moodle-dialogue-base .assignfeedback_editpdf_dropdown .moodle-dialogue-wrap .moodle-dialogue-ft {
     display: none;
 }
 
index 8e606c7..cb0deb3 100644 (file)
@@ -388,14 +388,14 @@ $string['preventsubmissions'] = 'Prevent the user from making any more submissio
 $string['preventsubmissionsshort'] = 'Prevent submission changes';
 $string['previous'] = 'Previous';
 $string['privacy:attemptpath'] = 'attempt {$a}';
-$string['privacy:blindmarkingidentifier'] = 'The identifier used for blind marking.';
+$string['privacy:blindmarkingidentifier'] = 'The identifier used for blind marking';
 $string['privacy:gradepath'] = 'grade';
 $string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders';
 $string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.';
 $string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\'';
 $string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment';
 $string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.';
-$string['privacy:metadata:assignmentid'] = 'Assignment identifier.';
+$string['privacy:metadata:assignmentid'] = 'Assignment ID';
 $string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.';
 $string['privacy:metadata:assignoverrides'] = 'Stores override information for the assignment';
 $string['privacy:metadata:assignperpage'] = 'Number of assignments shown per page.';
@@ -411,7 +411,7 @@ $string['privacy:metadata:groupid'] = 'Group ID that the user is a member of.';
 $string['privacy:metadata:latest'] = 'Greatly simplifies queries wanting to know information about only the latest attempt.';
 $string['privacy:metadata:mailed'] = 'Has this user been mailed yet?';
 $string['privacy:metadata:timecreated'] = 'Time created';
-$string['privacy:metadata:userid'] = 'Identifier for the user.';
+$string['privacy:metadata:userid'] = 'ID of the user';
 $string['privacy:studentpath'] = 'studentsubmissions';
 $string['quickgrading'] = 'Quick grading';
 $string['quickgradingresult'] = 'Quick grading';
index 1b65578..eb91ba5 100644 (file)
@@ -35,7 +35,7 @@ $string['onlinetextsubmission'] = 'Allow online text submission';
 $string['numwords'] = '({$a} words)';
 $string['numwordsforlog'] = 'Submission word count: {$a} words';
 $string['pluginname'] = 'Online text submissions';
-$string['privacy:metadata:assignmentid'] = 'Assignment identifier';
+$string['privacy:metadata:assignmentid'] = 'Assignment ID';
 $string['privacy:metadata:filepurpose'] = 'Files that are embedded in the text submission.';
 $string['privacy:metadata:submissionpurpose'] = 'The submission ID that links to submissions for the user.';
 $string['privacy:metadata:tablepurpose'] = 'Stores the text submission for each attempt.';
index ba28665..0d34150 100644 (file)
@@ -119,12 +119,20 @@ trait mod_assign_test_generator {
      * @param   bool        $changeuser Whether to switch user to the user being submitted as.
      */
     protected function mark_submission($teacher, $assign, $student, $grade = 50.0, $data = [], $attempt = 0) {
+        global $DB;
+
         // Mark the submission.
         $this->setUser($teacher);
         $data = (object) array_merge($data, [
                 'grade' => $grade,
             ]);
 
+        // Bump all timecreated and timemodified for this user back.
+        // The old assign_print_overview function includes submissions which have been graded where the grade modified
+        // date matches the submission modified date.
+        $DB->execute('UPDATE {assign_submission} SET timecreated = timecreated - 1, timemodified = timemodified - 1 WHERE userid = :userid',
+            ['userid' => $student->id]);
+
         $assign->testable_apply_grade_to_user($data, $student->id, $attempt);
     }
 }
index 042ef54..9c758c1 100644 (file)
@@ -120,7 +120,7 @@ class mod_assign_lib_testcase extends advanced_testcase {
         $this->assertEquals(1, count($overview));
         // Submissions without a grade.
         $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
+        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
 
         $this->setUser($teacher);
         $overview = array();
@@ -129,7 +129,7 @@ class mod_assign_lib_testcase extends advanced_testcase {
         $this->assertEquals(1, count($overview));
         // Submissions without a grade.
         $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
+        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
 
         // Let us grade a submission.
         $this->setUser($teacher);
@@ -145,20 +145,16 @@ class mod_assign_lib_testcase extends advanced_testcase {
 
         $overview = array();
         assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
         // Now assignment 4 should not show up.
-        $this->assertNotRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
+        $this->assertDebuggingCalledCount(3);
+        $this->assertEmpty($overview);
 
         $this->setUser($teacher);
         $overview = array();
         assign_print_overview($courses, $overview);
         $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
         // Now assignment 4 should not show up.
-        $this->assertNotRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
+        $this->assertEmpty($overview);
     }
 
     /**
index c454974..80ec0cd 100644 (file)
@@ -1065,10 +1065,6 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         $this->add_submission($student, $assign);
         $this->submit_for_grading($student, $assign);
 
-        // TODO Find a way to kill this waitForSecond
-        // This is to make sure the grade happens after the submission because
-        // we have no control over the timemodified values.
-        $this->waitForSecond();
         $this->mark_submission($teacher, $assign, $student, 50.0);
 
         $data = new stdClass();
@@ -1336,7 +1332,6 @@ class mod_assign_locallib_testcase extends advanced_testcase {
 
         $this->add_submission($student, $assign);
         $this->submit_for_grading($student, $assign);
-        $this->waitForSecond();
         $this->mark_submission($teacher, $assign, $student, 50.0);
 
         // Although it has been graded, it is still marked as submitted.
index 50d75be..82bd0cc 100644 (file)
@@ -125,7 +125,7 @@ $string['privacy:metadata:chat_users:ip'] = 'User IP';
 $string['privacy:metadata:chat_users:lang'] = 'User language';
 $string['privacy:metadata:chat_users:lastmessageping'] = 'Time of the last message in this chat room';
 $string['privacy:metadata:chat_users:lastping'] = 'Time of the last access to chat room';
-$string['privacy:metadata:chat_users:userid'] = 'User id';
+$string['privacy:metadata:chat_users:userid'] = 'The user ID';
 $string['privacy:metadata:chat_users:version'] = 'How user accessed the chat (sockets/basic/ajax/header_js)';
 $string['privacy:metadata:messages'] = 'A record of the messages sent during a chat session';
 $string['privacy:metadata:messages:issystem'] = 'Whether the message is a system-generated message';
index fb5e6de..8b2a52d 100644 (file)
@@ -110,7 +110,7 @@ $string['pluginname'] = 'Choice';
 $string['previewonly'] = 'This is just a preview of the available options for this activity. You will not be able to submit your choice until {$a}.';
 $string['privacy'] = 'Privacy of results';
 $string['privacy:metadata:choice_answers'] = 'Information about the user\'s chosen answer(s) for a given choice activity';
-$string['privacy:metadata:choice_answers:choiceid'] = 'The ID of the choice activity the user is providing answer for';
+$string['privacy:metadata:choice_answers:choiceid'] = 'The ID of the choice activity';
 $string['privacy:metadata:choice_answers:optionid'] = 'The ID of the option that the user selected.';
 $string['privacy:metadata:choice_answers:userid'] = 'The ID of the user answering this choice activity';
 $string['privacy:metadata:choice_answers:timemodified'] = 'The timestamp indicating when the choice was modified by the user';
index 227a9cd..6084a7f 100644 (file)
@@ -187,10 +187,10 @@ $string['fromfile'] = 'Import from zip file';
 $string['fromfile_help'] = 'The import from zip file feature allows you to browse for and upload a preset zip of templates and fields.';
 $string['generateerror'] = 'Not all files generated!';
 $string['header'] = 'Header';
-$string['headeraddtemplate'] = 'Defines the interface when editing entries';
-$string['headerasearchtemplate'] = 'Defines the interface for Advanced Searches';
+$string['headeraddtemplate'] = 'Defines the interface for adding or editing entries';
+$string['headerasearchtemplate'] = 'Defines the interface for the advanced search';
 $string['headercsstemplate'] = 'Defines local CSS styles for the other templates';
-$string['headerjstemplate'] = 'Defines custom Javascript for the other templates';
+$string['headerjstemplate'] = 'Defines custom Javascript for manipulating the way elements are displayed in the List, Single or Add templates';
 $string['headerlisttemplate'] = 'Defines browsing interface for multiple entries';
 $string['headerrsstemplate'] = 'Defines appearance of entries in RSS feeds';
 $string['headersingletemplate'] = 'Defines browsing interface for a single entry';
@@ -304,7 +304,7 @@ $string['presetinfo'] = 'Saving as a preset will publish this template. Other us
 $string['presets'] = 'Presets';
 $string['privacy:metadata:commentpurpose'] = 'Comments on database records';
 $string['privacy:metadata:data_content'] = 'Represents one answer to one field in database activity module';
-$string['privacy:metadata:data_content:fieldid'] = 'Field definition id';
+$string['privacy:metadata:data_content:fieldid'] = 'Field definition ID';
 $string['privacy:metadata:data_content:content'] = 'Content';
 $string['privacy:metadata:data_content:content1'] = 'Additional content 1';
 $string['privacy:metadata:data_content:content2'] = 'Additional content 2';
index bb8a147..f87fb98 100644 (file)
@@ -41,7 +41,7 @@ $string['bold'] = 'Bold';
 $string['calendarend'] = '{$a} closes';
 $string['calendarstart'] = '{$a} opens';
 $string['cannotaccess'] = 'You can only access this feedback from a course';
-$string['cannotsavetempl'] = 'saving templates is not allowed';
+$string['cannotsavetempl'] = 'Saving templates is not allowed';
 $string['captcha'] = 'Captcha';
 $string['captchanotset'] = 'Captcha hasn\'t been set.';
 $string['closebeforeopen'] = 'You have specified an end date before the start date.';
@@ -57,7 +57,7 @@ $string['confirmdeletetemplate'] = 'Are you sure you want to delete this templat
 $string['confirmusetemplate'] = 'Are you sure you want to use this template?';
 $string['continue_the_form'] = 'Continue answering the questions';
 $string['count_of_nums'] = 'Count of numbers';
-$string['courseid'] = 'courseid';
+$string['courseid'] = 'Course ID';
 $string['creating_templates'] = 'Save these questions as a new template';
 $string['delete_entry'] = 'Delete entry';
 $string['delete_item'] = 'Delete question';
@@ -117,7 +117,7 @@ $string['feedback:createpublictemplate'] = 'Create public template';
 $string['feedback:deletesubmissions'] = 'Delete completed submissions';
 $string['feedback:deletetemplate'] = 'Delete template';
 $string['feedback:edititems'] = 'Edit items';
-$string['feedback_is_not_for_anonymous'] = 'feedback is not for anonymous';
+$string['feedback_is_not_for_anonymous'] = 'Feedback is not for anonymous';
 $string['feedback_is_not_open'] = 'The feedback is not open';
 $string['feedback:mapcourse'] = 'Map courses to global feedbacks';
 $string['feedbackopen'] = 'Allow answers from';
@@ -129,12 +129,12 @@ $string['file'] = 'File';
 $string['filter_by_course'] = 'Filter by course';
 $string['handling_error'] = 'Error occurred in feedback module action handling';
 $string['hide_no_select_option'] = 'Hide the "Not selected" option';
-$string['horizontal'] = 'horizontal';
+$string['horizontal'] = 'Horizontal';
 $string['check'] = 'Multiple choice - multiple answers';
 $string['checkbox'] = 'Multiple choice - multiple answers allowed (check boxes)';
 $string['check_values'] = 'Possible responses';
 $string['choosefile'] = 'Choose a file';
-$string['chosen_feedback_response'] = 'chosen feedback response';
+$string['chosen_feedback_response'] = 'Chosen feedback response';
 $string['downloadresponseas'] = 'Download all responses as:';
 $string['importfromthisfile'] = 'Import from this file';
 $string['import_questions'] = 'Import questions';
@@ -160,8 +160,8 @@ and will appear in all courses using the feedback block. You can force the feedb
 $string['mapcourses'] = 'Map feedback to courses';
 $string['mappedcourses'] = 'Mapped courses';
 $string['mappingchanged'] = 'Course mapping has been changed';
-$string['minimal'] = 'minimum';
-$string['maximal'] = 'maximum';
+$string['minimal'] = 'Minimum';
+$string['maximal'] = 'Maximum';
 $string['messageprovider:message'] = 'Feedback reminder';
 $string['messageprovider:submission'] = 'Feedback notifications';
 $string['mode'] = 'Mode';
@@ -250,8 +250,8 @@ $string['search_course'] = 'Search course';
 $string['searchcourses'] = 'Search courses';
 $string['searchcourses_help'] = 'Search for the code or name of the course(s) that you wish to associate with this feedback.';
 $string['selected_dump'] = 'Selected indexes of $SESSION variable are dumped below:';
-$string['send'] = 'send';
-$string['send_message'] = 'send message';
+$string['send'] = 'Send';
+$string['send_message'] = 'Send message';
 $string['show_all'] = 'Show all';
 $string['show_analysepage_after_submit'] = 'Show analysis page';
 $string['show_entries'] = 'Show responses';
@@ -276,14 +276,14 @@ $string['textfield_maxlength'] = 'Maximum characters accepted';
 $string['textfield_size'] = 'Textfield width';
 $string['there_are_no_settings_for_recaptcha'] = 'There are no settings for captcha';
 $string['this_feedback_is_already_submitted'] = 'You\'ve already completed this activity.';
-$string['typemissing'] = 'missing value "type"';
+$string['typemissing'] = 'Missing value "type"';
 $string['update_item'] = 'Save changes to question';
 $string['url_for_continue'] = 'Link to next activity';
 $string['url_for_continue_help'] = 'After submitting the feedback, a continue button is displayed, which links to the course page. Alternatively, it may link to the next activity if the URL of the activity is entered here.';
 $string['use_one_line_for_each_value'] = 'Use one line for each answer!';
 $string['use_this_template'] = 'Use this template';
 $string['using_templates'] = 'Use a template';
-$string['vertical'] = 'vertical';
+$string['vertical'] = 'Vertical';
 // Deprecated since Moodle 3.2.
 $string['start'] = 'Start';
 $string['stop'] = 'End';
index 467609c..28a6711 100644 (file)
@@ -438,11 +438,11 @@ $string['privacy:metadata:forum_digests'] = 'Information about the digest prefer
 $string['privacy:metadata:forum_digests:forum'] = 'The forum subscribed to.';
 $string['privacy:metadata:forum_digests:maildigest'] = 'The digest preference.';
 $string['privacy:metadata:forum_digests:userid'] = 'The ID of the user with the digest preference.';
-$string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions.';
+$string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions';
 $string['privacy:metadata:forum_discussion_subs:discussionid'] = 'The ID of the discussion that was subscribed to.';
 $string['privacy:metadata:forum_discussion_subs:preference'] = 'The start time of the subscription.';
 $string['privacy:metadata:forum_discussion_subs:userid'] = 'The ID of the user with the discussion subscription.';
-$string['privacy:metadata:forum_discussions'] = 'Information about the individual forum discussions that a user has created.';
+$string['privacy:metadata:forum_discussions'] = 'Information about the individual forum discussions that a user has created';
 $string['privacy:metadata:forum_discussions:assessed'] = 'TODOD - what does this field store';
 $string['privacy:metadata:forum_discussions:name'] = 'The name of the discussion, as chosen by the author.';
 $string['privacy:metadata:forum_discussions:timemodified'] = 'The time that the discussion was last modified.';
@@ -458,8 +458,8 @@ $string['privacy:metadata:forum_posts:subject'] = 'The subject of the forum post
 $string['privacy:metadata:forum_posts:totalscore'] = 'The message of the forum post.';
 $string['privacy:metadata:forum_posts:userid'] = 'The ID of the user who authored the forum post.';
 $string['privacy:metadata:forum_queue'] = 'Temporary log of posts that will be mailed in digest form';
-$string['privacy:metadata:forum_queue:discussionid'] = 'Forum discussion id';
-$string['privacy:metadata:forum_queue:postid'] = 'Forum post id';
+$string['privacy:metadata:forum_queue:discussionid'] = 'Forum discussion ID';
+$string['privacy:metadata:forum_queue:postid'] = 'Forum post ID';
 $string['privacy:metadata:forum_queue:timemodified'] = 'The modified time of the original post';
 $string['privacy:metadata:forum_queue:userid'] = 'User who needs to be notified of the post';
 $string['privacy:metadata:forum_read'] = 'Information about which posts have been read by the user.';
@@ -475,7 +475,7 @@ $string['privacy:metadata:forum_track_prefs'] = 'Information about which forums
 $string['privacy:metadata:forum_track_prefs:forumid'] = 'The forum that has read tracking enabled.';
 $string['privacy:metadata:forum_track_prefs:userid'] = 'The ID of the user that this forum tracking preference relates to.';
 $string['privacy:metadata:preference:autosubscribe'] = 'Whether to subscribe to discussions when replying to posts within them.';
-$string['privacy:metadata:preference:maildigest'] = 'The site-wide mail digest preference.';
+$string['privacy:metadata:preference:maildigest'] = 'The site-wide mail digest preference';
 $string['privacy:metadata:preference:markasreadonnotification'] = 'Whether to mark forum posts as read when receiving them as messages.';
 $string['privacy:metadata:preference:trackforums'] = 'Whether to enable read tracking.';
 $string['privacy:postwasread'] = 'This post was first read on {$a->firstread} and most recently read on {$a->lastread}';
index a4c4075..bb08727 100644 (file)
@@ -280,7 +280,7 @@ $string['privacy:metadata:glossary_entries'] = 'Information about the user\'s en
 $string['privacy:metadata:glossary_entries:attachment'] = 'The attachment of the entry the user added';
 $string['privacy:metadata:glossary_entries:concept'] = 'The concept of the entry the user added';
 $string['privacy:metadata:glossary_entries:definition'] = 'The definition of the entry the user added';
-$string['privacy:metadata:glossary_entries:glossaryid'] = 'The ID of the glossary activity the user is providing entry for';
+$string['privacy:metadata:glossary_entries:glossaryid'] = 'The ID of the glossary activity';
 $string['privacy:metadata:glossary_entries:userid'] = 'The ID of the user that is adding this glossary entry';
 $string['privacy:metadata:glossary_entries:timemodified'] = 'The timestamp indicating when the glossary entry was modified by the user';
 $string['question'] = 'Question';
index 66477cb..98756d7 100644 (file)
@@ -1240,10 +1240,14 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h
             array('class' => 'glossary-hidden-note'));
     }
 
-    // Entry link.
-    $return .= '<a class="icon" title="' . get_string('entrylink', 'glossary', $altsuffix) . '" ' .
-               ' href="showentry.php?eid=' . $entry->id . '">' .
-               $OUTPUT->pix_icon('e/insert_edit_link', get_string('entrylink', 'glossary', $altsuffix)) . '</a>';
+    if ($entry->approved || has_capability('mod/glossary:approve', $context)) {
+        $output = true;
+        $return .= \html_writer::link(
+            new \moodle_url('/mod/glossary/showentry.php', ['eid' => $entry->id]),
+            $OUTPUT->pix_icon('fp/link', get_string('entrylink', 'glossary', $altsuffix), 'theme'),
+            ['title' => get_string('entrylink', 'glossary', $altsuffix), 'class' => 'icon']
+        );
+    }
 
     if (has_capability('mod/glossary:approve', $context) && !$glossary->defaultapproval && $entry->approved) {
         $output = true;
index fa458ae..665f2c7 100644 (file)
@@ -23,11 +23,7 @@ Feature: Glossary entries can be organised in categories
     And the following "activities" exist:
       | activity | name       | intro                                                           | course | idnumber  |
       | label    | name       | check autolinking of CategoryAutoLinks and CategoryNoLinks text | C1     | label1    |
-# Log in as admin and enable autolinking filter
-    And I log in as "admin"
-    And I navigate to "Plugins > Filters > Manage filters" in site administration
-    And I click on "On" "option" in the "Glossary auto-linking" "table_row"
-    And I log out
+    And the "glossary" filter is "on"
 # Log in as a teacher and make sure nothing is yet autolinked
     And I log in as "teacher1"
     When I am on "Course 1" course homepage
index 02b58d6..d7c68d6 100644 (file)
@@ -726,8 +726,8 @@ class qformat_default {
         // @@PLUGINFILE@@ with a real URL, but it doesn't matter what.
         // We use http://example.com/.
         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $question->questiontext);
-        return html_to_text(format_text($text,
-                $question->questiontextformat, $formatoptions), 0, false);
+        return s(html_to_text(format_text($text,
+                $question->questiontextformat, $formatoptions), 0, false));
     }
 
     /**
index a5a03c2..cd40e78 100644 (file)
@@ -440,7 +440,7 @@ class mod_lesson_renderer extends plugin_renderer_base {
             foreach ($links as $link) {
                 $options[$link['type']] = $link['name'];
             }
-            $options[0] = get_string('question', 'lesson');
+            $options[0] = get_string('addaquestionpage', 'lesson');
 
             $addpageurl = new moodle_url('/mod/lesson/editpage.php', array('id'=>$this->page->cm->id, 'pageid'=>$page->id, 'sesskey'=>sesskey()));
             $addpageselect = new single_select($addpageurl, 'qtype', $options, null, array(''=>get_string('addanewpage', 'lesson').'...'), 'addpageafter'.$page->id);
index 5ac4b93..8dd0cb3 100644 (file)
@@ -33,7 +33,7 @@ Feature: Teachers can review student progress on all lessons in a course by view
       | id_answer_editor_0 | Next page |
       | id_jumpto_0 | Next page |
     And I press "Save page"
-    And I select "Question" from the "qtype" singleselect
+    And I select "Add a question page" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -60,7 +60,7 @@ Feature: Teachers can review student progress on all lessons in a course by view
       | id_answer_editor_0 | Next page |
       | id_jumpto_0 | Next page |
     And I press "Save page"
-    And I select "Question" from the "qtype" singleselect
+    And I select "