Merge branch 'MDL-62838' of git://github.com/timhunt/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 3 Jul 2018 22:49:23 +0000 (00:49 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 3 Jul 2018 22:49:23 +0000 (00:49 +0200)
105 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/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/tests/behat/consent.feature
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/lang/en/auth_oauth2.php
blocks/recent_activity/lang/en/block_recent_activity.php
calendar/classes/local/event/forms/create.php
calendar/tests/behat/calendar.feature
course/classes/management_renderer.php
course/classes/search/section.php
course/externallib.php
course/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
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/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/db/install.xml
lib/db/upgrade.php
lib/moodlelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/behat/behat_filters.php [new file with mode: 0644]
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/feedback/comments/lang/en/assignfeedback_comments.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/tests/behat/categories.feature
mod/lesson/renderer.php
mod/lesson/tests/behat/lesson_complete_report.feature
mod/lesson/tests/behat/lesson_delete_answers.feature
mod/lesson/tests/behat/lesson_informations_at_end.feature
mod/lesson/tests/behat/lesson_number_of_student_attempts.feature
mod/lesson/tests/behat/lesson_outline_report.feature
mod/lesson/tests/behat/lesson_question_attempts.feature
mod/lesson/tests/behat/lesson_report.feature
mod/lesson/tests/behat/lesson_review.feature
mod/lesson/tests/behat/lesson_student_dashboard.feature
mod/lesson/tests/behat/lesson_student_resume.feature
mod/lesson/tests/behat/questions_images.feature
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/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
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/style/moodle.css [new file with mode: 0644]
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 c451c1a..c556825 100644 (file)
@@ -54,7 +54,7 @@ $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
 $string['consentdetails'] = 'Give consent on behalf of user';
 $string['consentpagetitle'] = 'Consent';
-$string['contactdpo'] = 'For questions regarding the policies please contact the Data Protection Officer.';
+$string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
 $string['dataproc'] = 'Personal data processing';
 $string['deleting'] = 'Deleting a version';
 $string['deleteconfirm'] = '<p>Are you sure you want to delete policy <em>\'{$a->name}\'</em>?</p><p>This operation can not be undone.</p>';
index 3073212..1fbe958 100644 (file)
@@ -83,7 +83,7 @@ function tool_policy_before_standard_html_head() {
     if (!empty($CFG->sitepolicyhandler)
             && $CFG->sitepolicyhandler == 'tool_policy'
             && empty($USER->policyagreed)
-            && isguestuser()) {
+            && (isguestuser() || !isloggedin())) {
         $output = $PAGE->get_renderer('tool_policy');
         $page = new \tool_policy\output\guestconsent();
 
index fde43cc..6c504eb 100644 (file)
@@ -81,12 +81,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     And I press "Next"
     And I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
     And I set the field "I agree to the This site policy" to "1"
@@ -129,22 +129,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     And I press "Next"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     And I press "Next"
     And I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
@@ -495,20 +495,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I am on site homepage
     And I follow "Log in"
     When I press "Create new account"
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I set the field "I agree to the This site policy" to "1"
     And I set the field "I agree to the This privacy policy" to "1"
@@ -528,22 +528,22 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I follow "Log in"
     When I press "Create new account"
     # Confirm that the user can view and accept policies when attempting to create another account.
-    Then I should see "This site policy"
+    Then I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should see "full text2"
     When I press "Next"
-    Then I should see "This privacy policy"
+    Then I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should see "full text3"
     When I press "Next"
     Then I should see "Please agree to the following policies"
-    And I should see "This site policy"
+    And I should see "This site policy" in the "region-main" "region"
     And I should see "short text2"
     And I should not see "full text2"
-    And I should see "This privacy policy"
+    And I should see "This privacy policy" in the "region-main" "region"
     And I should see "short text3"
     And I should not see "full text3"
-    And I should not see "This guests policy"
+    And I should not see "This guests policy" in the "region-main" "region"
     And I should not see "short text4"
     And I should not see "full text4"
     And I set the field "I agree to the This site policy" to "1"
index 024fe2f..4c0ca9e 100644 (file)
@@ -1187,9 +1187,18 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding);
                     empty($nuvalue) ? $nuvalue = array() : $nuvalue;
                     $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
-
                     foreach ($ldapkeys as $ldapkey) {
-                        $ldapkey   = $ldapkey;
+                        // Skip update if $ldapkey does not exist in LDAP.
+                        if (!isset($user_entry[$ldapkey][0])) {
+                            $success = false;
+                            error_log($this->errorlogtag.get_string('updateremfailfield', 'auth_ldap',
+                                                                     array('ldapkey' => $ldapkey,
+                                                                            'key' => $key,
+                                                                            'ouvalue' => $ouvalue,
+                                                                            'nuvalue' => $nuvalue)));
+                            continue;
+                        }
+
                         $ldapvalue = $user_entry[$ldapkey][0];
                         if (!$ambiguous) {
                             // Skip update if the values already match
index 237423e..5040996 100644 (file)
@@ -149,6 +149,7 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 412d35b..6074f71 100644 (file)
@@ -38,7 +38,7 @@ $string['pluginname'] = 'MNet authentication';
 $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
 $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
 $string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user.';
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
 $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
@@ -53,8 +53,8 @@ $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user
 $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
 $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
 $string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
-$string['privacy:metadata:mnet_external:id'] = 'The identifier for the user.';
-$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution.';
+$string['privacy:metadata:mnet_external:id'] = 'The user ID';
+$string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
 $string['privacy:metadata:mnet_external:institution'] = 'The institution that this user is a member of.';
 $string['privacy:metadata:mnet_external:lang'] = 'A user preference for the language shown.';
@@ -64,20 +64,20 @@ $string['privacy:metadata:mnet_external:lastname'] = 'The surname of the user.';
 $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic details about the user\'s surname.';
 $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
-$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user.';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
 $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
 $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The skype identifier of the user.';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
-$string['privacy:metadata:mnet_external:timezone'] = 'The timezone that the user resides in.';
+$string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
 $string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
 $string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The yahoo identifier of the user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
 $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
 $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
@@ -94,7 +94,7 @@ $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried
 $string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
 $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
 $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
-$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier.';
+$string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
 $string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
index d91aaca..ed38e2d 100644 (file)
@@ -93,7 +93,7 @@ $string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connecte
 $string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.';
 $string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.';
 $string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.';
-$string['privacy:metadata:auth_oauth2:issuerid'] = 'The identifier of the OAuth 2 issuer for this OAuth 2 login.';
+$string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login';
 $string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.';
 $string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.';
 $string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.';
index a569a7a..defbc88 100644 (file)
@@ -27,8 +27,8 @@ $string['pluginname'] = 'Recent activity';
 $string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
 $string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days';
 $string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted';
-$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id';
-$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id';
+$string['privacy:metadata:block_recent_activity:cmid'] = 'Course activity ID';
+$string['privacy:metadata:block_recent_activity:courseid'] = 'Course ID';
 $string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)';
 $string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed';
 $string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action';
index 1e36407..50e6d81 100644 (file)
@@ -119,10 +119,9 @@ class create extends \moodleform {
         global $DB, $CFG;
 
         $errors = parent::validation($data, $files);
-        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
         $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
-
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
         if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
         }
index e0e7850..18e41e8 100644 (file)
@@ -154,3 +154,22 @@ Feature: Perform basic calendar functionality
     When I click on "Go to activity" "link"
     And I wait to be redirected
     Then I should see "Test choice"
+
+  @javascript
+  Scenario: Attempt to create event without fill required fields should display validation errors
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "New event" "button"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I am on site homepage
+    And I follow "Calendar"
+    And I click on "New event" "button"
+    And I set the field "Type of event" to "Course"
+    When I click on "Save" "button"
+    Then I should see "Required"
+    And I should see "Select a course"
+    And I set the field "Event title" to "Really awesome event!"
+    When I click on "Save" "button"
+    Then I should see "Select a course"
index 4390d5a..eca4de1 100644 (file)
@@ -860,7 +860,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     protected function detail_pair($key, $value, $class ='') {
         $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
         $html .= html_writer::div(html_writer::span($key), 'pair-key span3 col-md-3 yui3-u-1-4');
-        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 col-md-9 m-b-1 yui3-u-3-4 form-inline');
         $html .= html_writer::end_div();
         return $html;
     }
index 11f6866..ce28664 100644 (file)
@@ -85,6 +85,9 @@ class section extends \core_search\base {
      * @return \core_search\document
      */
     public function get_document($record, $options = array()) {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
+
         // Get the context, modinfo, and section.
         try {
             $context = \context_course::instance($record->course);
index 1f1cb78..07f2d9f 100644 (file)
@@ -1600,9 +1600,7 @@ class core_course_external extends external_api {
                             break;
 
                         case 'visible':
-                            if (has_capability('moodle/category:manage', $context)
-                                or has_capability('moodle/category:viewhiddencategories',
-                                        context_system::instance())) {
+                            if (has_capability('moodle/category:viewhiddencategories', $context)) {
                                 $value = clean_param($crit['value'], PARAM_INT);
                                 $conditions[$key] = $value;
                                 $wheres[] = $key . " = :" . $key;
@@ -1712,9 +1710,7 @@ class core_course_external extends external_api {
             if (!isset($excludedcats[$category->id])) {
 
                 // Final check to see if the category is visible to the user.
-                if ($category->visible
-                        or has_capability('moodle/category:viewhiddencategories', context_system::instance())
-                        or has_capability('moodle/category:manage', $context)) {
+                if ($category->visible or has_capability('moodle/category:viewhiddencategories', $context)) {
 
                     $categoryinfo = array();
                     $categoryinfo['id'] = $category->id;
index 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 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 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 b9b9003..8e4dd62 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20180403" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20180618" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="fk_user" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="ix_concomitem" UNIQUE="false" FIELDS="contextid, commentarea, itemid" COMMENT="Allows the comments API to load comments for a particular area effectively."/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="external_services" COMMENT="built in and custom external services">
       <FIELDS>
index cc7edbd..7ddc876 100644 (file)
@@ -2233,5 +2233,26 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018062800.01) {
+        // Add foreign key fk_user to the comments table.
+        $table = new xmldb_table('comments');
+        $key = new xmldb_key('fk_user', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+        $dbman->add_key($table, $key);
+
+        upgrade_main_savepoint(true, 2018062800.01);
+    }
+
+    if ($oldversion < 2018062800.02) {
+        // Add composite index ix_concomitem to the table comments.
+        $table = new xmldb_table('comments');
+        $index = new xmldb_index('ix_concomitem', XMLDB_INDEX_NOTUNIQUE, array('contextid', 'commentarea', 'itemid'));
+
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        upgrade_main_savepoint(true, 2018062800.02);
+    }
+
     return true;
 }
index 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..ce732d1
--- /dev/null
@@ -0,0 +1,66 @@
+<?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: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../behat/behat_base.php');
+require_once(__DIR__ . '/../../filterlib.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) {
+        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 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 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..67925fa 100644 (file)
     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 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 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 "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:
@@ -106,7 +106,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:
@@ -119,7 +119,7 @@ Feature: Teachers can review student progress on all lessons in a course by view
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
index cafef68..37e9ddf 100644 (file)
@@ -32,7 +32,7 @@ branch table contents
       | id_answer_editor_1 | Previous page |
       | id_jumpto_1 | Previous 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 "Numerical"
     And I press "Add a question page"
     And I set the following fields to these values:
index 0727d30..07a3c7d 100644 (file)
@@ -28,7 +28,7 @@ Feature: In a lesson activity, if custom scoring is not enabled, student should
       | 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 "Numerical"
     And I press "Add a question page"
     And I set the following fields to these values:
index c0ec130..a3a732f 100644 (file)
@@ -49,7 +49,7 @@ Feature: In Dashboard, teacher can see the number of student attempts to lessons
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
index 9182b01..c4eea60 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 "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:
@@ -104,7 +104,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:
index 9abc8c0..9994f8a 100644 (file)
@@ -41,7 +41,7 @@ Feature: In a lesson activity, students can not re-attempt a question more than
       | id_answer_editor_1 | Next page |
       | id_jumpto_1 | 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:
@@ -63,7 +63,7 @@ Feature: In a lesson activity, students can not re-attempt a question more than
       | id_answer_editor_1 | Next page |
       | id_jumpto_1 | 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:
@@ -76,7 +76,7 @@ Feature: In a lesson activity, students can not re-attempt a question more than
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
index 27201f1..c7ff38a 100644 (file)
@@ -34,7 +34,7 @@ Feature: In a lesson activity, teachers can review student attempts
       | 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:
@@ -47,7 +47,7 @@ Feature: In a lesson activity, teachers can review student attempts
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
index 246fffa..39e2731 100644 (file)
@@ -42,7 +42,7 @@ Feature: In a lesson activity, students can review the answers they gave to ques
       | id_response_editor_1 | Incorrect answer |
       | id_jumpto_1 | This 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:
index c21555f..66ff74a 100644 (file)
@@ -37,7 +37,7 @@ Feature: In Dashboard, a student can see their current status on all lessons wit
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -85,7 +85,7 @@ Feature: In Dashboard, a student can see their current status on all lessons wit
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -190,7 +190,7 @@ Feature: In Dashboard, a student can see their current status on all lessons wit
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -256,7 +256,7 @@ Feature: In Dashboard, a student can see their current status on all lessons wit
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -336,7 +336,7 @@ Feature: In Dashboard, a student can see their current status on all lessons wit
       | id_answer_editor_0 | True |
       | id_answer_editor_1 | False |
     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:
index c4b2b40..2a66d6c 100644 (file)
@@ -32,7 +32,7 @@ Feature: In a lesson activity a student should
       | 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:
@@ -45,7 +45,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -194,7 +194,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -214,7 +214,7 @@ Feature: In a lesson activity a student should
       | 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:
@@ -227,7 +227,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -240,7 +240,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -323,7 +323,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -336,7 +336,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -349,7 +349,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
@@ -362,7 +362,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This 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:
index 9659dac..1fbd8c8 100644 (file)
@@ -53,7 +53,7 @@ Feature: In a lesson activity, teacher can add embedded images in questions answ
     And I set the field "Describe this image for someone who cannot see it" to "It's the logo"
     And I click on "Save image" "button"
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I set the field "qtype" to "Add a question page"
     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:
index 8798cab..c077a14 100644 (file)
@@ -34,5 +34,5 @@ $string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI
 $string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI consumer.';
 $string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.';
 $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
-$string['privacy:metadata:useridnumber'] = 'The idnumber of the user using the LTI consumer.';
+$string['privacy:metadata:useridnumber'] = 'The ID number of the user using the LTI consumer';
 $string['servicename'] = 'Memberships';
index 7d566f5..e6c0242 100644 (file)
@@ -644,12 +644,12 @@ $string['privacy:metadata:quiz_grades:grade'] = 'The overall grade for this quiz
 $string['privacy:metadata:quiz_grades:quiz'] = 'The quiz that was graded.';
 $string['privacy:metadata:quiz_grades:timemodified'] = 'The time that the grade was modified.';
 $string['privacy:metadata:quiz_grades:userid'] = 'The user who was graded.';
-$string['privacy:metadata:quiz_overrides'] = 'Details about overrides for this quiz.';
-$string['privacy:metadata:quiz_overrides:quiz'] = 'The quiz with override information.';
+$string['privacy:metadata:quiz_overrides'] = 'Details about overrides for this quiz';
+$string['privacy:metadata:quiz_overrides:quiz'] = 'The quiz with override information';
 $string['privacy:metadata:quiz_overrides:timeclose'] = 'The new close time for the quiz.';
 $string['privacy:metadata:quiz_overrides:timelimit'] = 'The new time limit for the quiz.';
 $string['privacy:metadata:quiz_overrides:timeopen'] = 'The new open time for the quiz.';
-$string['privacy:metadata:quiz_overrides:userid'] = 'The user being overridden.';
+$string['privacy:metadata:quiz_overrides:userid'] = 'The user being overridden';
 $string['privacy:metadata:quizaccess'] = 'The quiz activity makes use of quiz access rules.';
 $string['publish'] = 'Publish';
 $string['publishedit'] = 'You must have permission in the publishing course to add or edit questions in this category';
index 6f53c95..c5e078c 100644 (file)
@@ -36,7 +36,7 @@ $string['cannotloadquestioninfo'] = 'Unable to load questiontype specific questi
 $string['cannotgradethisattempt'] = 'Cannot grade this attempt.';
 $string['changeoptions'] = 'Change options';
 $string['essayonly'] = 'The following questions need to be graded manually';
-$string['invalidquestionid'] = 'Gradeable question with id {$a} not found';
+$string['invalidquestionid'] = 'Gradable question with ID {$a} not found';
 $string['invalidattemptid'] = 'No such attempt ID exists';
 $string['grade'] = 'grade';
 $string['gradeall'] = 'grade all';
index c693dc3..4006ee6 100644 (file)
@@ -211,7 +211,7 @@ $string['masteryoverridedesc'] = 'This preference sets the default for the maste
 $string['myattempts'] = 'My attempts';
 $string['myaiccsessions'] = 'My AICC sessions';
 $string['repositorynotsupported'] = 'This repository does not support linking directly to an imsmanifest.xml file.';
-$string['trackid'] = 'Id';
+$string['trackid'] = 'ID';
 $string['trackid_help'] = 'This is the identifier set by your SCORM package for this question, the SCORM specification doesn\'t allow the full question text to be provided.';
 $string['trackcorrectcount'] = 'Correct count';
 $string['trackcorrectcount_help'] = 'Number of correct results for the question';
index 6c669e5..f1f69aa 100644 (file)
@@ -240,10 +240,10 @@ $string['privacy:metadata:aggregatedgradinggrade'] = 'Aggregated grade for all a
 $string['privacy:metadata:assessmentgrade'] = 'Aggregated grade for the submission suggested by this assessment';
 $string['privacy:metadata:assessmentgradinggrade'] = 'Grade for providing this assessment';
 $string['privacy:metadata:assessmentgradinggradeover'] = 'Manually overridden value of the grade for providing this assessment';
-$string['privacy:metadata:assessmentid'] = 'Identifier of the assessment';
-$string['privacy:metadata:authorid'] = 'Identifier of the submission author';
+$string['privacy:metadata:assessmentid'] = 'ID of the assessment';
+$string['privacy:metadata:authorid'] = 'ID of the submission author';
 $string['privacy:metadata:dimensiongrade'] = 'Grade in the given assessment dimension';
-$string['privacy:metadata:dimensionid'] = 'Identifier of the assessment dimension';
+$string['privacy:metadata:dimensionid'] = 'ID of the assessment dimension';
 $string['privacy:metadata:example'] = 'Whether this record represents an example submission';
 $string['privacy:metadata:feedbackauthor'] = 'Feedback for the author';
 $string['privacy:metadata:feedbackauthorformat'] = 'Text format of the feedback for the author';
@@ -254,25 +254,25 @@ $string['privacy:metadata:peercomment'] = 'Comment on the given grade by the use
 $string['privacy:metadata:peercommentformat'] = 'Text format of the comment on the given grade';
 $string['privacy:metadata:preference:perpage'] = 'Number of submissions the user prefers to see on one page';
 $string['privacy:metadata:published'] = 'Whether the submission should be published to all participants once the workshop is closed';
-$string['privacy:metadata:reviewerid'] = 'Identifier of the user providing the assessment';
+$string['privacy:metadata:reviewerid'] = 'ID of the user providing the assessment';
 $string['privacy:metadata:strategy'] = 'Name of the grading strategy subplugin interpreting the record values';
 $string['privacy:metadata:submissioncontent'] = 'Content of the submission';
 $string['privacy:metadata:submissioncontentformat'] = 'Text format of the submission content';
 $string['privacy:metadata:submissiongrade'] = 'Aggregated grade for the submission written as a decimal number from interval 0..100';
 $string['privacy:metadata:submissiongradeover'] = 'Manually overridden value of the aggregated grade';
-$string['privacy:metadata:submissionid'] = 'Identifier of the submission';
+$string['privacy:metadata:submissionid'] = 'ID of the submission';
 $string['privacy:metadata:submissiontitle'] = 'Title of the submission';
 $string['privacy:metadata:subsystem:corefiles'] = 'Workshop module stores files embedded in / attached to the submission text';
 $string['privacy:metadata:subsystem:coreplagiarism'] = 'Workshop module has inbuilt support for plagiarism prevention systems';
 $string['privacy:metadata:timeaggregated'] = 'When the aggregated grade was last calculated';
 $string['privacy:metadata:timecreated'] = 'When this record was created in the database';
 $string['privacy:metadata:timemodified'] = 'When this record was last modified in the database';
-$string['privacy:metadata:userid'] = 'Identifier of the user for which aggregated grade is calculated';
+$string['privacy:metadata:userid'] = 'ID of the user for which aggregated grade is calculated';
 $string['privacy:metadata:weight'] = 'Weight of the assessment';
 $string['privacy:metadata:workshopaggregations'] = 'Holds aggregated grades for assessment';
 $string['privacy:metadata:workshopassessments'] = 'Holds information about allocated assessments of workshop module submissions';
 $string['privacy:metadata:workshopgrades'] = 'Holds information about how the assessment forms were filled with grades and comments';
-$string['privacy:metadata:workshopid'] = 'Identifier of the workshop activity';
+$string['privacy:metadata:workshopid'] = 'ID of the workshop activity';
 $string['privacy:metadata:workshopsubmissions'] = 'Holds information about workshop module submissions';
 $string['privacy:request:delete:title'] = '[Deleted]';
 $string['privacy:request:delete:content'] = 'The content has been deleted at the request of the user.';
index c7e1d35..d899d00 100644 (file)
       "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
       "dev": true
     },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+      "dev": true
+    },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "dev": true,
+      "requires": {
+        "delegates": "1.0.0",
+        "readable-stream": "2.3.6"
+      }
+    },
     "argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
       "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "assert-plus": {
       "version": "1.0.0",
       "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
       "dev": true
     },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true
+    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "autoprefixer": {
       "version": "6.7.7",
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz",
       "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "babel-code-frame": {
       "version": "6.26.0",
         "tweetnacl": "0.14.5"
       }
     },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
     "body-parser": {
       "version": "1.14.2",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz",
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "center-align": {
       "version": "0.1.3",
       "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
       "dev": true
     },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
     "coffee-script": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz",
         "date-now": "0.1.4"
       }
     },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
     "content-type": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
       "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
       "dev": true,
-      "optional": true,
       "requires": {
         "assert-plus": "1.0.0"
       }
       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
       "dev": true
     },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
         }
       }
     },
+    "each-async": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz",
+      "integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=",
+      "dev": true,
+      "requires": {
+        "onetime": "1.1.0",
+        "set-immediate-shim": "1.0.1"
+      },
+      "dependencies": {
+        "onetime": {
+          "version": "1.1.0",
+          "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+          "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+          "dev": true
+        }
+      }
+    },
     "ecc-jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
       "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "external-editor": {
       "version": "2.2.0",
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
       "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "form-data": {
       "version": "2.3.2",
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
       "dev": true
     },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "inherits": "2.0.3",
+        "mkdirp": "0.5.1",
+        "rimraf": "2.6.2"
+      }
+    },
     "functional-red-black-tree": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
       "integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=",
       "dev": true
     },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "1.2.0",
+        "console-control-strings": "1.1.0",
+        "has-unicode": "2.0.1",
+        "object-assign": "4.1.1",
+        "signal-exit": "3.0.2",
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1",
+        "wide-align": "1.1.3"
+      },
+      "dependencies": {
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "1.0.1"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        }
+      }
+    },
     "gaze": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz",
         }
       }
     },
+    "generate-function": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
+      "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
+      "dev": true
+    },
+    "generate-object-property": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+      "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+      "dev": true,
+      "requires": {
+        "is-property": "1.0.2"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
+      "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
+      "dev": true
+    },
     "get-stdin": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
       "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
       "dev": true,
-      "optional": true,
       "requires": {
         "assert-plus": "1.0.0"
       }
         }
       }
     },
+    "grunt-sass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-2.1.0.tgz",
+      "integrity": "sha512-XkexnQt/9rhReNd+Y7T0n/2g5FqYOQKfi2iSlpwDqvgs7EgEaGTxNhnWzHnbW5oNRvzL9AHopBG3AgRxL0d+DA==",
+      "dev": true,
+      "requires": {
+        "each-async": "1.1.1",
+        "node-sass": "4.9.0",
+        "object-assign": "4.1.1"
+      }
+    },
     "grunt-stylelint": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/grunt-stylelint/-/grunt-stylelint-0.6.0.tgz",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "dev": true
     },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "dev": true
+    },
     "hawk": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
       "dev": true
     },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+      "dev": true
+    },
     "indent-string": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
         "through": "2.3.8"
       }
     },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
     "irregular-plurals": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz",
         "is-extglob": "1.0.0"
       }
     },
+    "is-my-ip-valid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
+      "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
+      "dev": true
+    },
+    "is-my-json-valid": {
+      "version": "2.17.2",
+      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
+      "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
+      "dev": true,
+      "requires": {
+        "generate-function": "2.0.0",
+        "generate-object-property": "1.2.0",
+        "is-my-ip-valid": "1.0.0",
+        "jsonpointer": "4.0.1",
+        "xtend": "4.0.1"
+      }
+    },
     "is-number": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
       "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
       "dev": true
     },
+    "is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+      "dev": true
+    },
     "is-regexp": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "is-utf8": {
       "version": "0.2.1",
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "istanbul": {
       "version": "0.1.37",
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
       "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "json-schema-traverse": {
       "version": "0.3.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
       "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "jsonfilter": {
       "version": "1.1.2",
       "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=",
       "dev": true
     },
+    "jsonpointer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
+      "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
+      "dev": true
+    },
     "jsprim": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
       "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
       "dev": true,
-      "optional": true,
       "requires": {
         "assert-plus": "1.0.0",
         "extsprintf": "1.3.0",
       "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
       "dev": true
     },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "1.0.0"
+      }
+    },
     "ldjson-stream": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ldjson-stream/-/ldjson-stream-1.2.1.tgz",
       "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
       "dev": true
     },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+      "dev": true
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
+    "lodash.mergewith": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+      "dev": true
+    },
     "log-symbols": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
       "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
       "dev": true
     },
+    "nan": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
+      "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
+      "dev": true
+    },
     "natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
       "dev": true
     },
+    "node-gyp": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz",
+      "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==",
+      "dev": true,
+      "requires": {
+        "fstream": "1.0.11",
+        "glob": "7.1.2",
+        "graceful-fs": "4.1.11",
+        "mkdirp": "0.5.1",
+        "nopt": "3.0.6",
+        "npmlog": "4.1.2",
+        "osenv": "0.1.5",
+        "request": "2.81.0",
+        "rimraf": "2.6.2",
+        "semver": "5.3.0",
+        "tar": "2.2.1",
+        "which": "1.3.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "4.11.8",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+          "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+          "dev": true,
+          "requires": {
+            "co": "4.6.0",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "assert-plus": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
+          "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
+          "dev": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
+          "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
+          "dev": true
+        },
+        "boom": {
+          "version": "2.10.1",
+          "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
+          "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
+          "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
+          "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+          "dev": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.6",
+            "mime-types": "2.1.18"
+          }
+        },
+        "har-schema": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
+          "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=",
+          "dev": true
+        },
+        "har-validator": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
+          "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
+          "dev": true,
+          "requires": {
+            "ajv": "4.11.8",
+            "har-schema": "1.0.5"
+          }
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+          "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+          "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+          "dev": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+          "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+          "dev": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.1",
+            "sshpk": "1.14.1"
+          }
+        },
+        "performance-now": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
+          "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.4.0",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
+          "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
+          "dev": true
+        },
+        "request": {
+          "version": "2.81.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
+          "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
+          "dev": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.7.0",
+            "caseless": "0.12.0",
+            "combined-stream": "1.0.6",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "4.2.1",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.18",
+            "oauth-sign": "0.8.2",
+            "performance-now": "0.2.0",
+            "qs": "6.4.0",
+            "safe-buffer": "5.1.2",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.4",
+            "tunnel-agent": "0.6.0",
+            "uuid": "3.2.1"
+          }
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
+          "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        }
+      }
+    },
+    "node-sass": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz",
+      "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==",
+      "dev": true,
+      "requires": {
+        "async-foreach": "0.1.3",
+        "chalk": "1.1.3",
+        "cross-spawn": "3.0.1",
+        "gaze": "1.1.2",
+        "get-stdin": "4.0.1",
+        "glob": "7.1.2",
+        "in-publish": "2.0.0",
+        "lodash.assign": "4.2.0",
+        "lodash.clonedeep": "4.5.0",
+        "lodash.mergewith": "4.6.1",
+        "meow": "3.7.0",
+        "mkdirp": "0.5.1",
+        "nan": "2.10.0",
+        "node-gyp": "3.7.0",
+        "npmlog": "4.1.2",
+        "request": "2.79.0",
+        "sass-graph": "2.2.4",
+        "stdout-stream": "1.4.0",
+        "true-case-path": "1.0.2"
+      },
+      "dependencies": {
+        "assert-plus": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
+          "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
+          "dev": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
+          "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
+          "dev": true
+        },
+        "boom": {
+          "version": "2.10.1",
+          "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
+          "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "caseless": {
+          "version": "0.11.0",
+          "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
+          "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "2.2.1",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "2.0.0",
+            "strip-ansi": "3.0.1",
+            "supports-color": "2.0.0"
+          }
+        },
+        "cross-spawn": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+          "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "4.1.2",
+            "which": "1.3.0"
+          }
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
+          "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
+          "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+          "dev": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.6",
+            "mime-types": "2.1.18"
+          }
+        },
+        "har-validator": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
+          "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
+          "dev": true,
+          "requires": {
+            "chalk": "1.1.3",
+            "commander": "2.9.0",
+            "is-my-json-valid": "2.17.2",
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+          "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+          "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+          "dev": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+          "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+          "dev": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.1",
+            "sshpk": "1.14.1"
+          }
+        },
+        "qs": {
+          "version": "6.3.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
+          "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
+          "dev": true
+        },
+        "request": {
+          "version": "2.79.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
+          "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
+          "dev": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.7.0",
+            "caseless": "0.11.0",
+            "combined-stream": "1.0.6",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "2.0.6",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.18",
+            "oauth-sign": "0.8.2",
+            "qs": "6.3.2",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.4",
+            "tunnel-agent": "0.4.3",
+            "uuid": "3.2.1"
+          }
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
+          "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+          "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+          "dev": true
+        }
+      }
+    },
     "nopt": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
       "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=",
       "dev": true
     },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "dev": true,
+      "requires": {
+        "are-we-there-yet": "1.1.5",
+        "console-control-strings": "1.1.0",
+        "gauge": "2.7.4",
+        "set-blocking": "2.0.0"
+      }
+    },
     "num2fraction": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
       "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "object-assign": {
       "version": "4.1.1",
       "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
       "dev": true
     },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "requires": {
+        "lcid": "1.0.0"
+      }
+    },
     "os-tmpdir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
       "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
       "dev": true
     },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+      "dev": true,
+      "requires": {
+        "os-homedir": "1.0.2",
+        "os-tmpdir": "1.0.2"
+      }
+    },
     "pako": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
         "uuid": "3.2.1"
       }
     },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
     "require-from-string": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz",
       "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
       "dev": true
     },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
     "require-uncached": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
+    "sass-graph": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+      "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "lodash": "4.17.10",
+        "scss-tokenizer": "0.2.3",
+        "yargs": "7.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+          "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+          "dev": true,
+          "requires": {
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "wrap-ansi": "2.1.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "1.0.1"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        },
+        "yargs": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+          "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+          "dev": true,
+          "requires": {
+            "camelcase": "3.0.0",
+            "cliui": "3.2.0",
+            "decamelize": "1.2.0",
+            "get-caller-file": "1.0.2",
+            "os-locale": "1.4.0",
+            "read-pkg-up": "1.0.1",
+            "require-directory": "2.1.1",
+            "require-main-filename": "1.0.1",
+            "set-blocking": "2.0.0",
+            "string-width": "1.0.2",
+            "which-module": "1.0.0",
+            "y18n": "3.2.1",
+            "yargs-parser": "5.0.0"
+          }
+        }
+      }
+    },
     "sax": {
       "version": "0.5.8",
       "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
       "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=",
       "dev": true
     },
+    "scss-tokenizer": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+      "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+      "dev": true,
+      "requires": {
+        "js-base64": "2.4.3",
+        "source-map": "0.4.4"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
     "semver": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
       "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
       "dev": true
     },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
     "shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
       "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=",
       "dev": true,
-      "optional": true,
       "requires": {
         "asn1": "0.2.3",
         "assert-plus": "1.0.0",
       "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
       "dev": true
     },
+    "stdout-stream": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
+      "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.6"
+      }
+    },
     "stream-combiner": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
       "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "strip-ansi": {
       "version": "4.0.0",
         }
       }
     },
+    "tar": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "dev": true,
+      "requires": {
+        "block-stream": "0.0.9",
+        "fstream": "1.0.11",
+        "inherits": "2.0.3"
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
       "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
       "dev": true,
-      "optional": true,
       "requires": {
         "punycode": "1.4.1"
       },
           "version": "1.4.1",
           "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
           "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
-          "dev": true,
-          "optional": true
+          "dev": true
         }
       }
     },
       "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
       "dev": true
     },
+    "true-case-path": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
+      "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
+      "dev": true,
+      "requires": {
+        "glob": "6.0.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "6.0.4",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+          "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+          "dev": true,
+          "requires": {
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
     "tunnel-agent": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
       "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
       "dev": true,
-      "optional": true,
       "requires": {
         "safe-buffer": "5.1.2"
       }
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
       "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "validate-npm-package-license": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
       "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
       "dev": true,
-      "optional": true,
       "requires": {
         "assert-plus": "1.0.0",
         "core-util-is": "1.0.2",
         "isexe": "2.0.0"
       }
     },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "2.1.1"
+      }
+    },
     "window-size": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
       "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
       "dev": true
     },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1"
+      },
+      "dependencies": {
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "1.0.1"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        }
+      }
+    },
     "wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
       "dev": true
     },
+    "y18n": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+      "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+      "dev": true
+    },
     "yallist": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
         }
       }
     },
+    "yargs-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+      "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+      "dev": true,
+      "requires": {
+        "camelcase": "3.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        }
+      }
+    },
     "ycssmin": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ycssmin/-/ycssmin-1.0.1.tgz",
index 0e58fe2..6e20b6c 100644 (file)
@@ -12,6 +12,7 @@
     "grunt-contrib-uglify": "1.0.1",
     "grunt-contrib-watch": "1.0.0",
     "grunt-eslint": "20.1.0",
+    "grunt-sass": "2.1.0",
     "grunt-stylelint": "0.6.0",
     "semver": "5.3.0",
     "shifter": "0.5.0",
index ee050a7..e0c2b15 100644 (file)
@@ -33,7 +33,7 @@ $string['page-report-stats-user'] = 'User course statistics report';
 $string['stats:view'] = 'View course statistics report';
 $string['privacy:metadata'] = 'The Statistics plugin does not store any personal data.';
 
-$string['privacy:metadata:courseid'] = 'An identifier for a course.';
+$string['privacy:metadata:courseid'] = 'An identifier for a course';
 $string['privacy:metadata:userid'] = 'The user ID linked to this table.';
 $string['privacy:metadata:roleid'] = 'The role ID of the user.';
 $string['privacy:metadata:timeend'] = 'End time of logs view';
index 9d54711..f6739f1 100644 (file)
@@ -31,5 +31,5 @@ $string['maxwidth'] = 'Max image width (px)';
 $string['maxheight'] = 'Max image height (px)';
 $string['privacy:metadata:repository_wikimedia'] = 'The Wikimedia repository plugin does store user preferences, and transmits user data from Moodle to the remote system.';
 $string['privacy:metadata:repository_wikimedia:search_text'] = 'The Wikimedia repository user search text query.';
-$string['privacy:metadata:repository_wikimedia:preference:maxwidth'] = 'The user preference Max Width configured for the Wikimedia repository.';
+$string['privacy:metadata:repository_wikimedia:preference:maxwidth'] = 'The user preference max width configured for the Wikimedia repository';
 $string['privacy:metadata:repository_wikimedia:preference:maxheight'] = 'The user preference Max Height configured for the Wikimedia repository.';
index 4b64a8b..ccd8e45 100644 (file)
@@ -36,4 +36,4 @@ $string['sortrating'] = 'Rating';
 $string['sortrelevance'] = 'Relevance';
 $string['sortviewcount'] = 'View Count';
 $string['privacy:metadata:repository_youtube'] = 'The YouTube videos repository plugin does not store any personal data, but does transmit user data from Moodle to the remote system.';
-$string['privacy:metadata:repository_youtube:searchtext'] = 'The YouTube videos repository user search text query.';
+$string['privacy:metadata:repository_youtube:searchtext'] = 'The YouTube videos repository user search text query';
index b363e83..64a784a 100644 (file)
 
 $string['pluginname'] = 'Simple search';
 $string['privacy:metadata:index'] = 'Indexed contents';
-$string['privacy:metadata:index:docid'] = 'Document id (unique)';
+$string['privacy:metadata:index:docid'] = 'Document ID (unique)';
 $string['privacy:metadata:index:itemid'] = 'Item identifier (in search area scope)';
 $string['privacy:metadata:index:title'] = 'Title';
 $string['privacy:metadata:index:content'] = 'Contents';
-$string['privacy:metadata:index:contextid'] = 'Document context id';
-$string['privacy:metadata:index:areaid'] = 'Search area id';
+$string['privacy:metadata:index:contextid'] = 'Document context ID';
+$string['privacy:metadata:index:areaid'] = 'Search area ID';
 $string['privacy:metadata:index:type'] = 'Document type';
-$string['privacy:metadata:index:courseid'] = 'Course id';
-$string['privacy:metadata:index:owneruserid'] = 'Document owner user id';
+$string['privacy:metadata:index:courseid'] = 'Course ID';
+$string['privacy:metadata:index:owneruserid'] = 'Document owner user ID';
 $string['privacy:metadata:index:modified'] = 'Last modification time';
-$string['privacy:metadata:index:userid'] = 'Document user id';
+$string['privacy:metadata:index:userid'] = 'Document user ID';
 $string['privacy:metadata:index:description1'] = 'Extra description field';
 $string['privacy:metadata:index:description2'] = 'Extra description field';
 $string['searchinfo'] = 'Search queries';
index 9f0d404..28a9c94 100644 (file)
@@ -722,7 +722,7 @@ class renderer extends \core_course_management_renderer {
      */
     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 col-md-4 yui3-u-1-4 font-weight-bold');
+        $html .= html_writer::div(html_writer::span($key), 'pair-key col-md-3 yui3-u-1-4 font-weight-bold');
         $html .= html_writer::div(html_writer::span($value), 'pair-value col-md-8 yui3-u-3-4');
         $html .= html_writer::end_div();
         return $html;
index a628ff8..0c260d4 100644 (file)
@@ -150,6 +150,7 @@ $THEME->enable_dock = false;
 $THEME->csstreepostprocessor = 'theme_boost_css_tree_post_processor';
 $THEME->extrascsscallback = 'theme_boost_get_extra_scss';
 $THEME->prescsscallback = 'theme_boost_get_pre_scss';
+$THEME->precompiledcsscallback = 'theme_boost_get_precompiled_css';
 $THEME->yuicssmodules = array();
 $THEME->rendererfactory = 'theme_overridden_renderer_factory';
 $THEME->requiredblocks = '';
index fab20d3..68a20f8 100644 (file)
@@ -109,6 +109,16 @@ function theme_boost_get_main_scss_content($theme) {
     return $scss;
 }
 
+/**
+ * Get compiled css.
+ *
+ * @return string compiled css
+ */
+function theme_boost_get_precompiled_css() {
+    global $CFG;
+    return file_get_contents($CFG->dirroot . '/theme/boost/style/moodle.css');
+}
+
 /**
  * Get SCSS to prepend.
  *
index c620f45..f2812ae 100644 (file)
@@ -4,16 +4,18 @@ Twitter bootstrap
 -----------------
 
 Sass:
-This theme uses the original unmodified version 4.0.0 Twitter bootstrap sass files.
+This theme uses the version 4.0.0 Twitter bootstrap sass files.
 The bootstrap repository is available on:
 
 https://github.com/twitter/bootstrap.git
 
 To update to the latest release of twitter bootstrap:
-* re-apply /* rtl:begin:ignore */ on the top of _popover.scss before .popover rule and /* rtl:end:ignore */ before
-  .popover-arrow::after rule. See MDL-56763 commit (1a4faf9b).
+
 * remove all files from scss/bootstrap,
 * download the new scss files and store them in scss/bootstrap
+* re-apply /* rtl:begin:ignore */ on the top of _popover.scss before .popover rule and /* rtl:end:ignore */ before
+  .popover-arrow::after rule. See MDL-56763 commit (1a4faf9b).
+* comment out all uses of the @supports syntax in SCSS (see https://github.com/sabberworm/PHP-CSS-Parser/issues/127). In Bootstrap 4.0 The @supports rules are used for carousal transitions (nice sliding) and the .sticky-top helper class. The carousel bootstrap component will still be functional.
 * update ./thirdpartylibs.xml
 
 Javascript:
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
new file mode 100644 (file)
index 0000000..b95a1f2
--- /dev/null
@@ -0,0 +1,16969 @@
+@charset "UTF-8";
+@font-face {
+  font-family: 'FontAwesome';
+  src: url("[[font:core|fontawesome-webfont.eot]]?v=4.7.0");
+  src: url("[[font:core|fontawesome-webfont.eot]]?#iefix&v=4.7.0") format("embedded-opentype"), url("[[font:core|fontawesome-webfont.woff2]]?v=4.7.0") format("woff2"), url("[[font:core|fontawesome-webfont.woff]]?v=4.7.0") format("woff"), url("[[font:core|fontawesome-webfont.ttf]]?v=4.7.0") format("truetype"), url("[[font:core|fontawesome-webfont.svg]]?v=4.7.0#fontawesomeregular") format("svg");
+  font-weight: normal;
+  font-style: normal; }
+
+/*!
+ *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+.fa, .block_settings .block_tree [aria-expanded="true"]:before,
+.block_navigation .block_tree [aria-expanded="true"]:before, .block_settings .block_tree [aria-expanded="false"]:before,
+.block_navigation .block_tree [aria-expanded="false"]:before {
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale; }
+
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+  font-size: 1.3333333333em;
+  line-height: 0.75em;
+  vertical-align: -15%; }
+
+.fa-2x {
+  font-size: 2em; }
+
+.fa-3x {
+  font-size: 3em; }
+
+.fa-4x {
+  font-size: 4em; }
+
+.fa-5x {
+  font-size: 5em; }
+
+.fa-fw {
+  width: 1.2857142857em;
+  text-align: center; }
+
+.fa-ul {
+  padding-left: 0;
+  margin-left: 2.1428571429em;
+  list-style-type: none; }
+  .fa-ul > li {
+    position: relative; }
+
+.fa-li {
+  position: absolute;
+  left: -2.1428571429em;
+  width: 2.1428571429em;
+  top: 0.1428571429em;
+  text-align: center; }
+  .fa-li.fa-lg {
+    left: -1.8571428571em; }
+
+.fa-border {
+  padding: .2em .25em .15em;
+  border: solid 0.08em #eee;
+  border-radius: .1em; }
+
+.fa-pull-left {
+  float: left; }
+
+.fa-pull-right {
+  float: right; }
+
+.fa.fa-pull-left, .block_settings .block_tree .fa-pull-left[aria-expanded="true"]:before,
+.block_navigation .block_tree .fa-pull-left[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-left[aria-expanded="false"]:before,
+.block_navigation .block_tree .fa-pull-left[aria-expanded="false"]:before {
+  margin-right: .3em; }
+
+.fa.fa-pull-right, .block_settings .block_tree .fa-pull-right[aria-expanded="true"]:before,
+.block_navigation .block_tree .fa-pull-right[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-right[aria-expanded="false"]:before,
+.block_navigation .block_tree .fa-pull-right[aria-expanded="false"]:before {
+  margin-left: .3em; }
+
+/* Deprecated as of 4.4.0 */
+.pull-right {
+  float: right; }
+
+.pull-left {
+  float: left; }
+
+.fa.pull-left, .block_settings .block_tree .pull-left[aria-expanded="true"]:before,
+.block_navigation .block_tree .pull-left[aria-expanded="true"]:before, .block_settings .block_tree .pull-left[aria-expanded="false"]:before,
+.block_navigation .block_tree .pull-left[aria-expanded="false"]:before {
+  margin-right: .3em; }
+
+.fa.pull-right, .block_settings .block_tree .pull-right[aria-expanded="true"]:before,
+.block_navigation .block_tree .pull-right[aria-expanded="true"]:before, .block_settings .block_tree .pull-right[aria-expanded="false"]:before,
+.block_navigation .block_tree .pull-right[aria-expanded="false"]:before {
+  margin-left: .3em; }
+
+.fa-spin {
+  -webkit-animation: fa-spin 2s infinite linear;
+  animation: fa-spin 2s infinite linear; }
+
+.fa-pulse {
+  -webkit-animation: fa-spin 1s infinite steps(8);
+  animation: fa-spin 1s infinite steps(8); }
+
+@-webkit-keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(359deg);
+    transform: rotate(359deg); } }
+
+@keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(359deg);
+    transform: rotate(359deg); } }
+
+.fa-rotate-90 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+  -webkit-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  transform: rotate(90deg); }
+
+.fa-rotate-180 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+  -webkit-transform: rotate(180deg);
+  -ms-transform: rotate(180deg);
+  transform: rotate(180deg); }
+
+.fa-rotate-270 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+  -webkit-transform: rotate(270deg);
+  -ms-transform: rotate(270deg);
+  transform: rotate(270deg); }
+
+.fa-flip-horizontal {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+  -webkit-transform: scale(-1, 1);
+  -ms-transform: scale(-1, 1);
+  transform: scale(-1, 1); }
+
+.fa-flip-vertical {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+  -webkit-transform: scale(1, -1);
+  -ms-transform: scale(1, -1);
+  transform: scale(1, -1); }
+
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical {
+  filter: none; }
+
+.fa-stack {
+  position: relative;
+  display: inline-block;
+  width: 2em;
+  height: 2em;
+  line-height: 2em;
+  vertical-align: middle; }
+
+.fa-stack-1x, .fa-stack-2x {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  text-align: center; }
+
+.fa-stack-1x {
+  line-height: inherit; }
+
+.fa-stack-2x {
+  font-size: 2em; }
+
+.fa-inverse {
+  color: #fff; }
+
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+   readers do not read off random characters that represent icons */
+.fa-glass:before {
+  content: ""; }
+
+.fa-music:before {
+  content: ""; }
+
+.fa-search:before {
+  content: ""; }
+
+.fa-envelope-o:before {
+  content: ""; }
+
+.fa-heart:before {
+  content: ""; }
+
+.fa-star:before {
+  content: ""; }
+
+.fa-star-o:before {
+  content: ""; }
+
+.fa-user:before {
+  content: ""; }
+
+.fa-film:before {
+  content: ""; }
+
+.fa-th-large:before {
+  content: ""; }
+
+.fa-th:before {
+  content: ""; }
+
+.fa-th-list:before {
+  content: ""; }
+
+.fa-check:before {
+  content: ""; }
+
+.fa-remove:before,
+.fa-close:before,
+.fa-times:before {
+  content: ""; }
+
+.fa-search-plus:before {
+  content: ""; }
+
+.fa-search-minus:before {
+  content: ""; }
+
+.fa-power-off:before {
+  content: ""; }
+
+.fa-signal:before {
+  content: ""; }
+
+.fa-gear:before,
+.fa-cog:before {
+  content: ""; }
+
+.fa-trash-o:before {
+  content: ""; }
+
+.fa-home:before {
+  content: ""; }
+
+.fa-file-o:before {
+  content: ""; }
+
+.fa-clock-o:before {
+  content: ""; }
+
+.fa-road:before {
+  content: ""; }
+
+.fa-download:before {
+  content: ""; }
+
+.fa-arrow-circle-o-down:before {
+  content: ""; }
+
+.fa-arrow-circle-o-up:before {
+  content: ""; }
+
+.fa-inbox:before {
+  content: ""; }
+
+.fa-play-circle-o:before {
+  content: ""; }
+
+.fa-rotate-right:before,
+.fa-repeat:before {
+  content: ""; }
+
+.fa-refresh:before {
+  content: ""; }
+
+.fa-list-alt:before {
+  content: ""; }
+
+.fa-lock:before {
+  content: ""; }
+
+.fa-flag:before {
+  content: ""; }
+
+.fa-headphones:before {
+  content: ""; }
+
+.fa-volume-off:before {
+  content: ""; }
+
+.fa-volume-down:before {
+  content: ""; }
+
+.fa-volume-up:before {
+  content: ""; }
+
+.fa-qrcode:before {
+  content: ""; }
+
+.fa-barcode:before {
+  content: ""; }
+
+.fa-tag:before {
+  content: ""; }
+
+.fa-tags:before {
+  content: ""; }
+
+.fa-book:before {
+  content: ""; }
+
+.fa-bookmark:before {
+  content: ""; }
+
+.fa-print:before {
+  content: ""; }
+
+.fa-camera:before {
+  content: ""; }
+
+.fa-font:before {
+  content: ""; }
+
+.fa-bold:before {
+  content: ""; }
+
+.fa-italic:before {
+  content: ""; }
+
+.fa-text-height:before {
+  content: ""; }
+
+.fa-text-width:before {
+  content: ""; }
+
+.fa-align-left:before {
+  content: ""; }
+
+.fa-align-center:before {
+  content: ""; }
+
+.fa-align-right:before {
+  content: ""; }
+
+.fa-align-justify:before {
+  content: ""; }
+
+.fa-list:before {
+  content: ""; }
+
+.fa-dedent:before,
+.fa-outdent:before {
+  content: ""; }
+
+.fa-indent:before {
+  content: ""; }
+
+.fa-video-camera:before {
+  content: ""; }
+
+.fa-photo:before,
+.fa-image:before,
+.fa-picture-o:before {
+  content: ""; }
+
+.fa-pencil:before {
+  content: ""; }
+
+.fa-map-marker:before {
+  content: ""; }
+
+.fa-adjust:before {
+  content: ""; }
+
+.fa-tint:before {
+  content: ""; }
+
+.fa-edit:before,
+.fa-pencil-square-o:before {
+  content: ""; }
+
+.fa-share-square-o:before {
+  content: ""; }
+
+.fa-check-square-o:before {
+  content: ""; }
+
+.fa-arrows:before {
+  content: ""; }
+
+.fa-step-backward:before {
+  content: ""; }
+
+.fa-fast-backward:before {
+  content: ""; }
+
+.fa-backward:before {
+  content: ""; }
+
+.fa-play:before {
+  content: ""; }
+
+.fa-pause:before {
+  content: ""; }
+
+.fa-stop:before {
+  content: ""; }
+
+.fa-forward:before {
+  content: ""; }
+
+.fa-fast-forward:before {
+  content: ""; }
+
+.fa-step-forward:before {
+  content: ""; }
+
+.fa-eject:before {
+  content: ""; }
+
+.fa-chevron-left:before {
+  content: ""; }
+
+.fa-chevron-right:before {
+  content: ""; }
+
+.fa-plus-circle:before {
+  content: ""; }
+
+.fa-minus-circle:before {
+  content: ""; }
+
+.fa-times-circle:before {
+  content: ""; }
+
+.fa-check-circle:before {
+  content: ""; }
+
+.fa-question-circle:before {
+  content: ""; }
+
+.fa-info-circle:before {
+  content: ""; }
+
+.fa-crosshairs:before {
+  content: ""; }
+
+.fa-times-circle-o:before {
+  content: ""; }
+
+.fa-check-circle-o:before {
+  content: ""; }
+
+.fa-ban:before {
+  content: ""; }
+
+.fa-arrow-left:before {
+  content: ""; }
+
+.fa-arrow-right:before {
+  content: ""; }
+
+.fa-arrow-up:before {
+  content: ""; }
+
+.fa-arrow-down:before {
+  content: ""; }
+
+.fa-mail-forward:before,
+.fa-share:before {
+  content: ""; }
+
+.fa-expand:before {
+  content: ""; }
+
+.fa-compress:before {
+  content: ""; }
+
+.fa-plus:before {
+  content: ""; }
+
+.fa-minus:before {
+  content: ""; }
+
+.fa-asterisk:before {
+  content: ""; }
+
+.fa-exclamation-circle:before {
+  content: ""; }
+
+.fa-gift:before {
+  content: ""; }
+
+.fa-leaf:before {
+  content: ""; }
+
+.fa-fire:before {
+  content: ""; }
+
+.fa-eye:before {
+  content: ""; }
+
+.fa-eye-slash:before {
+  content: ""; }
+
+.fa-warning:before,
+.fa-exclamation-triangle:before {
+  content: ""; }
+
+.fa-plane:before {
+  content: ""; }
+
+.fa-calendar:before {
+  content: ""; }
+
+.fa-random:before {
+  content: ""; }
+
+.fa-comment:before {
+  content: ""; }
+
+.fa-magnet:before {
+  content: ""; }
+
+.fa-chevron-up:before {
+  content: ""; }
+
+.fa-chevron-down:before {
+  content: ""; }
+
+.fa-retweet:before {
+  content: ""; }
+
+.fa-shopping-cart:before {
+  content: ""; }
+
+.fa-folder:before {
+  content: ""; }
+
+.fa-folder-open:before {
+  content: ""; }
+
+.fa-arrows-v:before {
+  content: ""; }
+
+.fa-arrows-h:before {
+  content: ""; }
+
+.fa-bar-chart-o:before,
+.fa-bar-chart:before {
+  content: ""; }
+
+.fa-twitter-square:before {
+  content: ""; }
+
+.fa-facebook-square:before {
+  content: ""; }
+
+.fa-camera-retro:before {
+  content: ""; }
+
+.fa-key:before {
+  content: ""; }
+
+.fa-gears:before,
+.fa-cogs:before {
+  content: ""; }
+
+.fa-comments:before {
+  content: ""; }
+
+.fa-thumbs-o-up:before {
+  content: ""; }
+
+.fa-thumbs-o-down:before {
+  content: ""; }
+
+.fa-star-half:before {
+  content: ""; }
+
+.fa-heart-o:before {
+  content: ""; }
+
+.fa-sign-out:before {
+  content: ""; }
+
+.fa-linkedin-square:before {
+  content: ""; }
+
+.fa-thumb-tack:before {
+  content: ""; }
+
+.fa-external-link:before {
+  content: ""; }
+
+.fa-sign-in:before {
+  content: ""; }
+
+.fa-trophy:before {
+  content: ""; }
+
+.fa-github-square:before {
+  content: ""; }
+
+.fa-upload:before {
+  content: ""; }
+
+.fa-lemon-o:before {
+  content: ""; }
+
+.fa-phone:before {
+  content: ""; }
+
+.fa-square-o:before {
+  content: ""; }
+
+.fa-bookmark-o:before {
+  content: ""; }
+
+.fa-phone-square:before {
+  content: ""; }
+
+.fa-twitter:before {
+  content: ""; }
+
+.fa-facebook-f:before,
+.fa-facebook:before {
+  content: ""; }
+
+.fa-github:before {
+  content: ""; }
+
+.fa-unlock:before {
+  content: ""; }
+
+.fa-credit-card:before {
+  content: ""; }
+
+.fa-feed:before,
+.fa-rss:before {
+  content: ""; }
+
+.fa-hdd-o:before {
+  content: ""; }
+
+.fa-bullhorn:before {
+  content: ""; }
+
+.fa-bell:before {
+  content: ""; }
+
+.fa-certificate:before {
+  content: ""; }
+
+.fa-hand-o-right:before {
+  content: ""; }
+
+.fa-hand-o-left:before {
+  content: ""; }
+
+.fa-hand-o-up:before {
+  content: ""; }
+
+.fa-hand-o-down:before {
+  content: ""; }
+
+.fa-arrow-circle-left:before {
+  content: ""; }
+
+.fa-arrow-circle-right:before {
+  content: ""; }
+
+.fa-arrow-circle-up:before {
+  content: ""; }
+
+.fa-arrow-circle-down:before {
+  content: ""; }
+
+.fa-globe:before {
+  content: ""; }
+
+.fa-wrench:before {
+  content: ""; }
+
+.fa-tasks:before {
+  content: ""; }
+
+.fa-filter:before {
+  content: ""; }
+
+.fa-briefcase:before {
+  content: ""; }
+
+.fa-arrows-alt:before {
+  content: ""; }
+
+.fa-group:before,
+.fa-users:before {
+  content: ""; }
+
+.fa-chain:before,
+.fa-link:before {
+  content: ""; }
+
+.fa-cloud:before {
+  content: ""; }
+
+.fa-flask:before {
+  content: ""; }
+
+.fa-cut:before,
+.fa-scissors:before {
+  content: ""; }
+
+.fa-copy:before,
+.fa-files-o:before {
+  content: ""; }
+
+.fa-paperclip:before {
+  content: ""; }
+
+.fa-save:before,
+.fa-floppy-o:before {
+  content: ""; }
+
+.fa-square:before {
+  content: ""; }
+
+.fa-navicon:before,
+.fa-reorder:before,
+.fa-bars:before {
+  content: ""; }
+
+.fa-list-ul:before {
+  content: ""; }
+
+.fa-list-ol:before {
+  content: ""; }
+
+.fa-strikethrough:before {
+  content: ""; }
+
+.fa-underline:before {
+  content: ""; }
+
+.fa-table:before {
+  content: ""; }
+
+.fa-magic:before {
+  content: ""; }
+
+.fa-truck:before {
+  content: ""; }
+
+.fa-pinterest:before {
+  content: ""; }
+
+.fa-pinterest-square:before {
+  content: ""; }
+
+.fa-google-plus-square:before {
+  content: ""; }
+
+.fa-google-plus:before {
+  content: ""; }
+
+.fa-money:before {
+  content: ""; }
+
+.fa-caret-down:before {
+  content: ""; }
+
+.fa-caret-up:before {
+  content: ""; }
+
+.fa-caret-left:before {
+  content: ""; }
+
+.fa-caret-right:before {
+  content: ""; }
+
+.fa-columns:before {
+  content: ""; }
+
+.fa-unsorted:before,
+.fa-sort:before {
+  content: ""; }
+
+.fa-sort-down:before,
+.fa-sort-desc:before {
+  content: ""; }
+
+.fa-sort-up:before,
+.fa-sort-asc:before {
+  content: ""; }
+
+.fa-envelope:before {
+  content: ""; }
+
+.fa-linkedin:before {
+  content: ""; }
+
+.fa-rotate-left:before,
+.fa-undo:before {
+  content: ""; }
+
+.fa-legal:before,
+.fa-gavel:before {
+  content: ""; }
+
+.fa-dashboard:before,
+.fa-tachometer:before {
+  content: ""; }
+
+.fa-comment-o:before {
+  content: ""; }
+
+.fa-comments-o:before {
+  content: ""; }
+
+.fa-flash:before,
+.fa-bolt:before {
+  content: ""; }
+
+.fa-sitemap:before {
+  content: ""; }
+
+.fa-umbrella:before {
+  content: ""; }
+
+.fa-paste:before,
+.fa-clipboard:before {
+  content: ""; }
+
+.fa-lightbulb-o:before {
+  content: ""; }
+
+.fa-exchange:before {
+  content: ""; }
+
+.fa-cloud-download:before {
+  content: ""; }
+
+.fa-cloud-upload:before {
+  content: ""; }
+
+.fa-user-md:before {
+  content: ""; }
+
+.fa-stethoscope:before {
+  content: ""; }
+
+.fa-suitcase:before {
+  content: ""; }
+
+.fa-bell-o:before {
+  content: ""; }
+
+.fa-coffee:before {
+  content: ""; }
+
+.fa-cutlery:before {
+  content: ""; }
+
+.fa-file-text-o:before {
+  content: ""; }
+
+.fa-building-o:before {
+  content: ""; }
+
+.fa-hospital-o:before {
+  content: ""; }
+
+.fa-ambulance:before {
+  content: ""; }
+
+.fa-medkit:before {
+  content: ""; }
+
+.fa-fighter-jet:before {
+  content: ""; }
+
+.fa-beer:before {
+  content: ""; }