Merge branch 'MDL-65474-master' of https://github.com/ryanwyllie/moodle
authorDavid MonllaĆ³ <davidm@moodle.com>
Mon, 6 May 2019 08:06:41 +0000 (10:06 +0200)
committerAdrian Greeve <abgreeve@gmail.com>
Tue, 7 May 2019 08:24:37 +0000 (16:24 +0800)
289 files changed:
admin/cli/install.php
admin/settings/users.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/tests/behat/restoredefault.feature
admin/tool/log/classes/local/privacy/helper.php
admin/tool/lp/amd/build/competencies.min.js
admin/tool/lp/amd/build/course_competency_settings.min.js
admin/tool/lp/amd/build/module_navigation.min.js [new file with mode: 0644]
admin/tool/lp/amd/src/competencies.js
admin/tool/lp/amd/src/course_competency_settings.js
admin/tool/lp/amd/src/module_navigation.js [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/classes/output/module_navigation.php [new file with mode: 0644]
admin/tool/lp/classes/output/renderer.php
admin/tool/lp/coursecompetencies.php
admin/tool/lp/lang/en/tool_lp.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/module_navigation.mustache [new file with mode: 0644]
admin/tool/lp/templates/user_competency_course_navigation.mustache
admin/tool/lp/tests/behat/course_competencies.feature [new file with mode: 0644]
admin/tool/lp/tests/externallib_test.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/uploaduser/locallib.php
admin/tool/usertours/lang/en/tool_usertours.php
backup/util/settings/setting_dependency.class.php
backup/util/settings/tests/settings_test.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/view-cards.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/navigation/lang/en/block_navigation.php
calendar/amd/build/view_manager.min.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/lib.php
calendar/templates/calendar_day.mustache
calendar/templates/day_detailed.mustache
calendar/templates/event_details.mustache [new file with mode: 0644]
calendar/templates/event_item.mustache
calendar/templates/event_list.mustache
calendar/templates/event_summary_body.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/upcoming_mini.mustache
competency/classes/course_module_competency.php
competency/tests/course_module_competency_test.php [new file with mode: 0644]
course/externallib.php
course/publish/metadata.php
course/request.php
course/templates/coursecard.mustache
course/tests/behat/customfields_locked.feature
course/tests/behat/customfields_visibility.feature
course/tests/externallib_test.php
customfield/field/checkbox/lang/en/customfield_checkbox.php
customfield/field/date/lang/en/customfield_date.php
customfield/field/select/lang/en/customfield_select.php
customfield/field/text/lang/en/customfield_text.php
customfield/field/text/tests/behat/field.feature
customfield/field/textarea/lang/en/customfield_textarea.php
customfield/tests/behat/edit_fields_settings.feature
customfield/tests/behat/required_field.feature
customfield/tests/behat/unique_field.feature
enrol/renderer.php
favourites/classes/local/service/component_favourite_service.php [new file with mode: 0644]
favourites/classes/service_factory.php
favourites/tests/component_favourite_service_test.php [new file with mode: 0644]
favourites/tests/repository_test.php
favourites/tests/user_favourite_service_test.php [moved from favourites/tests/service_test.php with 100% similarity]
filter/mathjaxloader/filter.php
grade/import/direct/lang/en/gradeimport_direct.php
group/tests/privacy_provider_test.php
lang/en/access.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/course.php
lang/en/customfield.php
lang/en/message.php
lang/en/moodle.php
lib/behat/classes/partial_named_selector.php
lib/classes/hub/registration.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/string_manager_standard.php
lib/classes/task/badges_cron_task.php
lib/classes/task/send_failed_login_notifications_task.php
lib/db/caches.php
lib/db/upgrade.php
lib/editor/atto/plugins/emoticon/lib.php
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/moodleemoticon/dialog.php
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/form/cancel.php
lib/form/course.php
lib/form/filemanager.php
lib/form/filepicker.js
lib/form/filepicker.php
lib/form/float.php [new file with mode: 0644]
lib/form/group.php
lib/form/listing.php
lib/form/submit.php
lib/form/templatable_form_element.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-autocomplete-inline.mustache
lib/form/templates/element-autocomplete.mustache
lib/form/templates/element-button-inline.mustache
lib/form/templates/element-button.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-passwordunmask.mustache
lib/form/templates/element-radio-inline.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-select-inline.mustache
lib/form/templates/element-select.mustache
lib/form/templates/element-selectgroups-inline.mustache
lib/form/templates/element-selectgroups.mustache
lib/form/templates/element-selectwithlink.mustache
lib/form/templates/element-submit.mustache
lib/form/templates/element-tags-inline.mustache
lib/form/templates/element-tags.mustache
lib/form/templates/element-template-inline.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-textarea.mustache
lib/form/templates/element-url.mustache
lib/form/tests/float_test.php [new file with mode: 0644]
lib/formslib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/element.php
lib/portfoliolib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/templates/permissionmanager_panelcontent.mustache
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/moodlelib_test.php
lib/tests/string_manager_standard_test.php
lib/upgrade.txt
message/amd/build/message_drawer_router.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/src/message_drawer_router.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_overview_section.js
message/classes/api.php
message/classes/helper.php
message/output/email/classes/event_observers.php [new file with mode: 0644]
message/output/email/db/events.php [new file with mode: 0644]
message/output/email/lang/en/message_email.php
message/output/email/templates/email_digest_html.mustache
message/output/email/tests/event_observers_test.php [new file with mode: 0644]
message/output/email/tests/send_email_task_test.php
message/output/email/version.php
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/favourite_conversations.feature
message/tests/behat/group_conversation.feature
message/tests/behat/message_delete_conversation.feature
message/tests/behat/message_drawer_manage_contacts.feature
message/tests/behat/message_manage_preferences.feature
message/tests/behat/message_send_messages.feature
message/tests/behat/self_conversation.feature [new file with mode: 0644]
message/tests/behat/unread_messages.feature
message/tests/externallib_test.php
message/tests/helper_test.php
message/tests/privacy_provider_test.php
mod/assign/lang/en/assign.php
mod/book/tool/print/classes/output/print_book_page.php
mod/book/tool/print/classes/output/renderer.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/lang/en/data.php
mod/forum/amd/build/inpage_reply.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/inpage_reply.js
mod/forum/amd/src/selectors.js
mod/forum/classes/local/builders/exported_discussion_summaries.php
mod/forum/classes/local/builders/exported_posts.php
mod/forum/classes/local/container.php
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/discussion_summaries.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/exporters/post.php
mod/forum/classes/local/exporters/posts.php
mod/forum/classes/local/factories/exporter.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/classes/local/vaults/author.php
mod/forum/classes/local/vaults/db_table_vault.php
mod/forum/classes/local/vaults/discussion.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/local/vaults/forum.php
mod/forum/classes/local/vaults/post.php
mod/forum/classes/local/vaults/post_read_receipt_collection.php
mod/forum/classes/privacy/provider.php
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/templates/discussion_favourite_toggle.mustache
mod/forum/templates/discussion_list.mustache
mod/forum/templates/discussion_lock_toggle.mustache
mod/forum/templates/discussion_pin_toggle.mustache
mod/forum/templates/forum_action_menu.mustache
mod/forum/templates/forum_discussion_favourite_toggle.mustache
mod/forum/templates/inpage_reply.mustache
mod/forum/tests/behat/discussion_lock.feature
mod/forum/tests/exporters_author_test.php
mod/forum/tests/exporters_forum_test.php
mod/forum/tests/exporters_post_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/vaults_author_test.php
mod/forum/tests/vaults_discussion_list_test.php
mod/forum/tests/vaults_post_test.php
mod/forum/upgrade.txt
mod/forum/view.php
mod/lesson/continue.php
mod/lesson/lang/en/lesson.php
mod/quiz/amd/build/modal_quiz_question_bank.min.js
mod/quiz/amd/src/modal_quiz_question_bank.js
mod/quiz/lang/en/quiz.php
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
question/behaviour/behaviourbase.php
question/engine/questionattempt.php
question/engine/tests/questionattempt_test.php
question/previewlib.php
question/type/calculated/datasetitems_form.php
question/type/calculated/questiontype.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/calculatedsimple/questiontype.php
question/type/calculatedsimple/tests/questiontype_test.php
question/type/edit_question_form.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/add.feature [new file with mode: 0644]
question/type/numerical/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/numerical/tests/behat/edit.feature [new file with mode: 0644]
question/type/numerical/tests/behat/export.feature [new file with mode: 0644]
question/type/numerical/tests/behat/import.feature [new file with mode: 0644]
question/type/numerical/tests/behat/preview.feature [new file with mode: 0644]
question/type/numerical/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/questionbase.php
report/competency/amd/build/grading_popup.min.js
report/competency/amd/build/user_course_navigation.min.js
report/competency/amd/src/grading_popup.js
report/competency/amd/src/user_course_navigation.js
report/competency/classes/external.php
report/competency/classes/output/report.php
report/competency/classes/output/user_course_navigation.php
report/competency/index.php
report/competency/lang/en/report_competency.php
report/competency/lib.php
report/competency/templates/report.mustache
report/competency/templates/user_course_navigation.mustache
report/competency/tests/behat/breakdown_by_activity.feature [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/filesystem/lang/en/repository_filesystem.php
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/pending.min.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/amd/src/pending.js [new file with mode: 0644]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core_form/element-float-inline.mustache [new file with mode: 0644]
theme/boost/templates/core_form/element-float.mustache [new file with mode: 0644]
theme/classic/lang/en/theme_classic.php
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/tests/behat/view_full_profile.feature
version.php

index 1e5837f..e9203ec 100644 (file)
@@ -710,7 +710,7 @@ if ($interactive) {
     cli_separator();
     cli_heading(get_string('cliadminemail', 'install'));
     $prompt = get_string('clitypevaluedefault', 'admin', $options['adminemail']);
-    $options['adminemail'] = cli_input($prompt);
+    $options['adminemail'] = cli_input($prompt, $options['adminemail']);
 }
 
 // Validate that the address provided was an e-mail address.
index 3adbf0b..901a38a 100644 (file)
@@ -174,12 +174,11 @@ if ($hassiteconfig
         // Options include fields from the user table that might be helpful to
         // distinguish when adding or listing users ('I want to add the John
         // Smith from Science faculty').
-        // Username is not included as an option because in some sites, it might
-        // be a security problem to reveal usernames even to trusted staff.
         // Custom user profile fields are not currently supported.
         $temp->add(new admin_setting_configmulticheckbox('showuseridentity',
                 new lang_string('showuseridentity', 'admin'),
                 new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array(
+                    'username'    => new lang_string('username'),
                     'idnumber'    => new lang_string('idnumber'),
                     'email'       => new lang_string('email'),
                     'phone1'      => new lang_string('phone1'),
@@ -255,4 +254,4 @@ if ($hassiteconfig) {
         new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW));
 
     $ADMIN->add('privacy', $temp);
-}
\ No newline at end of file
+}
index 7e4d208..9e7f30d 100644 (file)
@@ -43,7 +43,7 @@ $string['componentselectnone'] = 'Unselect all';
 $string['createmodel'] = 'Create model';
 $string['currenttimesplitting'] = 'Current time-splitting method';
 $string['delete'] = 'Delete';
-$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
+$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes cannot be reverted.';
 $string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
@@ -98,7 +98,7 @@ $string['invalidindicatorsremoved'] = 'A new model has been added. Indicators th
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
-$string['missingmoodleversion'] = 'Imported file does not define a moodle version number';
+$string['missingmoodleversion'] = 'Imported file doesn\'t define a version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelname'] = 'Model name';
@@ -120,7 +120,7 @@ $string['previouspage'] = 'Previous page';
 $string['restoredefault'] = 'Restore default models';
 $string['restoredefaultempty'] = 'Please select models to be restored.';
 $string['restoredefaultinfo'] = 'These default models are missing or have changed since being installed. You can restore selected default models.';
-$string['restoredefaultnone'] = 'All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore.';
+$string['restoredefaultnone'] = 'All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore.';
 $string['restoredefaultsome'] = 'Succesfully re-created {$a->count} new model(s).';
 $string['restoredefaultsubmit'] = 'Restore selected';
 $string['samestartdate'] = 'Current start date is good';
@@ -135,7 +135,7 @@ $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
-$string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
+$string['versionnotsame'] = 'Imported file was from a different version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Evaluation log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index 74ff239..f819e5f 100644 (file)
@@ -65,7 +65,7 @@ Feature: Restoring default models
     And I should see "Analytics models"
     And I should see "No teaching"
     When I click on "Restore default models" "link"
-    Then I should see "All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore."
+    Then I should see "All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore."
     And I click on "Back" "link"
     And I should see "Analytics models"
 
index 95c09e9..a90f1fc 100644 (file)
@@ -85,7 +85,7 @@ class helper {
         } else {
             $name = $record->eventname;
             $description = "Unknown event ({$name})";
-            $other = unserialize($record->other);
+            $other = \tool_log\helper\reader::decode_other($record->other);
         }
 
         $realuserid = $record->realuserid;
index 1f5ddbe..6d24fd8 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencies.min.js and b/admin/tool/lp/amd/build/competencies.min.js differ
index 5fe246e..fe53dc6 100644 (file)
Binary files a/admin/tool/lp/amd/build/course_competency_settings.min.js and b/admin/tool/lp/amd/build/course_competency_settings.min.js differ
diff --git a/admin/tool/lp/amd/build/module_navigation.min.js b/admin/tool/lp/amd/build/module_navigation.min.js
new file mode 100644 (file)
index 0000000..db9d241
Binary files /dev/null and b/admin/tool/lp/amd/build/module_navigation.min.js differ
index 1b82116..e3a5176 100644 (file)
@@ -144,7 +144,7 @@ define(['jquery',
                     });
                     requests.push({
                         methodname: 'tool_lp_data_for_course_competencies_page',
-                        args: {courseid: self.itemid}
+                        args: {courseid: self.itemid, moduleid: 0}
                     });
 
                     pagerender = 'tool_lp/course_competencies_page';
@@ -212,7 +212,7 @@ define(['jquery',
                 {methodname: 'core_competency_remove_competency_from_course',
                     args: {courseid: localthis.itemid, competencyid: deleteid}},
                 {methodname: 'tool_lp_data_for_course_competencies_page',
-                    args: {courseid: localthis.itemid}}
+                    args: {courseid: localthis.itemid, moduleid: 0}}
             ]);
             pagerender = 'tool_lp/course_competencies_page';
             pageregion = 'coursecompetenciespage';
@@ -311,7 +311,7 @@ define(['jquery',
                     {methodname: 'core_competency_set_course_competency_ruleoutcome',
                       args: {coursecompetencyid: coursecompetencyid, ruleoutcome: ruleoutcome}},
                     {methodname: 'tool_lp_data_for_course_competencies_page',
-                      args: {courseid: localthis.itemid}}
+                      args: {courseid: localthis.itemid, moduleid: 0}}
                 ]);
 
                 requests[1].done(function(context) {
index 06af055..ff21ade 100644 (file)
@@ -134,7 +134,7 @@ define(['jquery',
 
         ajax.call([
             {methodname: 'tool_lp_data_for_course_competencies_page',
-              args: {courseid: courseId}}
+              args: {courseid: courseId, moduleid: 0}}
         ])[0].done(function(context) {
             templates.render('tool_lp/course_competencies_page', context).done(function(html, js) {
                 $('[data-region="coursecompetenciespage"]').replaceWith(html);
diff --git a/admin/tool/lp/amd/src/module_navigation.js b/admin/tool/lp/amd/src/module_navigation.js
new file mode 100644 (file)
index 0000000..d08a550
--- /dev/null
@@ -0,0 +1,62 @@
+// 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/>.
+
+/**
+ * Module to navigation between users in a course.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+
+    /**
+     * ModuleNavigation
+     *
+     * @param {String} moduleSelector The selector of the module element.
+     * @param {String} baseUrl The base url for the page (no params).
+     * @param {Number} courseId The course id
+     * @param {Number} moduleId The activity module (filter)
+     */
+    var ModuleNavigation = function(moduleSelector, baseUrl, courseId, moduleId) {
+        this._baseUrl = baseUrl;
+        this._moduleId = moduleId;
+        this._courseId = courseId;
+
+        $(moduleSelector).on('change', this._moduleChanged.bind(this));
+    };
+
+    /**
+     * The module was changed in the select list.
+     *
+     * @method _moduleChanged
+     * @param {Event} e the event
+     */
+    ModuleNavigation.prototype._moduleChanged = function(e) {
+        var newModuleId = $(e.target).val();
+        var queryStr = '?mod=' + newModuleId + '&courseid=' + this._courseId;
+        document.location = this._baseUrl + queryStr;
+    };
+
+    /** @type {Number} The id of the course. */
+    ModuleNavigation.prototype._courseId = null;
+    /** @type {Number} The id of the module. */
+    ModuleNavigation.prototype._moduleId = null;
+    /** @type {String} Plugin base url. */
+    ModuleNavigation.prototype._baseUrl = null;
+
+    return /** @alias module:tool_lp/module_navigation */ ModuleNavigation;
+});
index d79317c..1929036 100644 (file)
@@ -363,7 +363,13 @@ class external extends external_api {
             'The course id',
             VALUE_REQUIRED
         );
-        $params = array('courseid' => $courseid);
+        $moduleid = new external_value(
+            PARAM_INT,
+            'The module id',
+            VALUE_DEFAULT,
+            0
+        );
+        $params = array('courseid' => $courseid, 'moduleid' => $moduleid);
         return new external_function_parameters($params);
     }
 
@@ -371,16 +377,18 @@ class external extends external_api {
      * Loads the data required to render the course_competencies_page template.
      *
      * @param int $courseid The course id to check.
+     * @param int $moduleid The module id to check (0 for no filter).
      * @return boolean
      */
-    public static function data_for_course_competencies_page($courseid) {
+    public static function data_for_course_competencies_page($courseid, $moduleid) {
         global $PAGE;
         $params = self::validate_parameters(self::data_for_course_competencies_page_parameters(), array(
             'courseid' => $courseid,
+            'moduleid' => $moduleid,
         ));
         self::validate_context(context_course::instance($params['courseid']));
 
-        $renderable = new output\course_competencies_page($params['courseid']);
+        $renderable = new output\course_competencies_page($params['courseid'], $params['moduleid']);
         $renderer = $PAGE->get_renderer('tool_lp');
 
         $data = $renderable->export_for_template($renderer);
index 218d8a5..830cf8a 100644 (file)
@@ -57,6 +57,9 @@ class course_competencies_page implements renderable, templatable {
     /** @var int $courseid Course id for this page. */
     protected $courseid = null;
 
+    /** @var int $moduleid Module id for this page. */
+    protected $moduleid = null;
+
     /** @var context $context The context for this page. */
     protected $context = null;
 
@@ -76,10 +79,31 @@ class course_competencies_page implements renderable, templatable {
      * Construct this renderable.
      * @param int $courseid The course record for this page.
      */
-    public function __construct($courseid) {
+    public function __construct($courseid, $moduleid) {
         $this->context = context_course::instance($courseid);
         $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
         $this->coursecompetencylist = api::list_course_competencies($courseid);
+
+        if ($this->moduleid > 0) {
+            $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+            foreach ($this->coursecompetencylist as $ccid => $coursecompetency) {
+                $coursecompetency = $coursecompetency['coursecompetency'];
+                $found = false;
+                foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                    if ($modulecompetency->get('competencyid') == $coursecompetency->get('competencyid')) {
+                        $found = true;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    // We need to filter out this competency.
+                    unset($this->coursecompetencylist[$ccid]);
+                }
+            }
+        }
+
         $this->canmanagecoursecompetencies = has_capability('moodle/competency:coursecompetencymanage', $this->context);
         $this->canconfigurecoursecompetencies = has_capability('moodle/competency:coursecompetencyconfigure', $this->context);
         $this->cangradecompetencies = has_capability('moodle/competency:competencygrade', $this->context);
@@ -112,6 +136,7 @@ class course_competencies_page implements renderable, templatable {
 
         $data = new stdClass();
         $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
         $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
@@ -120,6 +145,24 @@ class course_competencies_page implements renderable, templatable {
         if ($gradable) {
             $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $USER->id);
             $data->gradableuserid = $USER->id;
+
+            if ($this->moduleid > 0) {
+                $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+                foreach ($usercompetencycourses as $ucid => $usercoursecompetency) {
+                    $found = false;
+                    foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                        if ($modulecompetency->get('competencyid') == $usercoursecompetency->get('competencyid')) {
+                            $found = true;
+                            break;
+                        }
+                    }
+
+                    if (!$found) {
+                        // We need to filter out this competency.
+                        unset($usercompetencycourses[$ucid]);
+                    }
+                }
+            }
         }
 
         $ruleoutcomelist = course_competency::get_ruleoutcome_list();
diff --git a/admin/tool/lp/classes/output/module_navigation.php b/admin/tool/lp/classes/output/module_navigation.php
new file mode 100644 (file)
index 0000000..68ff9b0
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * User navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_lp\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+use context_course;
+use core_course\external\course_module_summary_exporter;
+use stdClass;
+
+/**
+ * User course navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class module_navigation implements renderable, templatable {
+
+    /** @var courseid */
+    protected $courseid;
+
+    /** @var moduleid */
+    protected $moduleid;
+
+    /** @var baseurl */
+    protected $baseurl;
+
+    /**
+     * Construct.
+     *
+     * @param int $courseid
+     * @param int $moduleid
+     * @param string $baseurl
+     */
+    public function __construct($courseid, $moduleid, $baseurl) {
+        $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
+        $this->baseurl = $baseurl;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+
+        $context = context_course::instance($this->courseid);
+
+        $data = new stdClass();
+        $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
+        $data->baseurl = $this->baseurl;
+        $data->hasmodules = false;
+        $data->modules = array();
+
+        $data->hasmodules = true;
+        $data->modules = array();
+        $empty = (object)['id' => 0, 'name' => get_string('nofiltersapplied')];
+        $data->modules[] = $empty;
+
+        $modinfo = get_fast_modinfo($this->courseid);
+        foreach ($modinfo->get_cms() as $cm) {
+            if ($cm->uservisible) {
+                $exporter = new course_module_summary_exporter(null, ['cm' => $cm]);
+                $module = $exporter->export($output);
+                if ($module->id == $this->moduleid) {
+                    $module->selected = true;
+                }
+                $data->modules[] = $module;
+                $data->hasmodules = true;
+            }
+        }
+
+        return $data;
+    }
+}
index ca45e29..3907215 100644 (file)
@@ -263,4 +263,16 @@ class renderer extends plugin_renderer_base {
         $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
         return $this->render($n);
     }
+
+    /**
+     * Defer to template.
+     *
+     * @param module_navigation $nav
+     * @return string
+     */
+    public function render_module_navigation(module_navigation $nav) {
+        $data = $nav->export_for_template($this);
+        return parent::render_from_template('tool_lp/module_navigation', $data);
+    }
+
 }
index e700a17..f3e65a7 100644 (file)
 require_once(__DIR__ . '/../../../config.php');
 
 $id = required_param('courseid', PARAM_INT);
+$currentmodule = optional_param('mod', null, PARAM_INT);
+if ($currentmodule > 0) {
+    $cm = get_coursemodule_from_id('', $currentmodule, 0, false, MUST_EXIST);
+}
 
 $params = array('id' => $id);
 $course = $DB->get_record('course', $params, '*', MUST_EXIST);
@@ -33,16 +37,22 @@ require_login($course);
 \core_competency\api::require_enabled();
 
 $context = context_course::instance($course->id);
-$urlparams = array('courseid' => $id);
+$urlparams = array('courseid' => $id, 'mod' => $currentmodule);
 
 $url = new moodle_url('/admin/tool/lp/coursecompetencies.php', $urlparams);
 
 list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course);
+if ($currentmodule > 0) {
+    $title = get_string('filtermodule', 'report_competency', format_string($cm->name));
+}
 
 $output = $PAGE->get_renderer('tool_lp');
-$page = new \tool_lp\output\course_competencies_page($course->id);
+$page = new \tool_lp\output\course_competencies_page($course->id, $currentmodule);
 
 echo $output->header();
+$baseurl = new moodle_url('/admin/tool/lp/coursecompetencies.php');
+$nav = new \tool_lp\output\module_navigation($course->id, $currentmodule, $baseurl);
+echo $output->render($nav);
 echo $output->heading($title);
 
 echo $output->render($page);
index 156299c..c4db7ba 100644 (file)
@@ -107,6 +107,7 @@ $string['editthisuserevidence'] = 'Edit this evidence';
 $string['edituserevidence'] = 'Edit evidence';
 $string['evidence'] = 'Evidence';
 $string['findcourses'] = 'Find courses';
+$string['filterbyactivity'] = 'Filter competencies by resource or activity';
 $string['frameworkcannotbedeleted'] = 'The competency framework \'{$a}\' cannot be deleted';
 $string['hidden'] = 'Hidden';
 $string['hiddenhint'] = '(hidden)';
@@ -145,6 +146,7 @@ $string['nfiles'] = '{$a} file(s)';
 $string['noactivities'] = 'No activities';
 $string['nocompetencies'] = 'No competencies have been created in this framework.';
 $string['nocompetenciesincourse'] = 'No competencies have been linked to this course.';
+$string['nocompetenciesinactivity'] = 'No competencies have been linked to this activity or resource.';
 $string['nocompetenciesinevidence'] = 'No competencies have been linked to this evidence.';
 $string['nocompetenciesinlearningplan'] = 'No competencies have been linked to this learning plan.';
 $string['nocompetenciesintemplate'] = 'No competencies have been linked to this learning plan template.';
index b41af9b..8de78a0 100644 (file)
 </table>
 {{^competencies}}
 <p class="alert alert-info">
-    {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{#moduleid}}
+        {{#str}}nocompetenciesinactivity, tool_lp{{/str}}
+    {{/moduleid}}
+    {{^moduleid}}
+        {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{/moduleid}}
 </p>
 {{/competencies}}
 </div>
diff --git a/admin/tool/lp/templates/module_navigation.mustache b/admin/tool/lp/templates/module_navigation.mustache
new file mode 100644 (file)
index 0000000..aa10239
--- /dev/null
@@ -0,0 +1,52 @@
+{{!
+    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_lp/module_navigation
+
+    Show an auto-complete for filtering by competencies linked to a module.
+
+    Context variables required for this template:
+    * hasmodules
+    * modules - array
+      * id
+      * selected
+      * name
+
+    // No example context because the JS is connected to webservices
+}}
+<div class="float-right card p-2">
+<form class="user-competency-course-navigation">
+{{#hasmodules}}
+<span>
+<label for="module-nav-{{uniqid}}" class="accesshide">{{#str}}filterbyactivity, tool_lp{{/str}}</label>
+<select id="module-nav-{{uniqid}}">
+{{#modules}}
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{name}}</option>
+{{/modules}}
+</select>
+</span>
+{{/hasmodules}}
+</form>
+</div>
+{{#js}}
+require(['core/form-autocomplete', 'tool_lp/module_navigation'], function(autocomplete, nav) {
+    (new nav('#module-nav-{{uniqid}}', '{{baseurl}}', {{courseid}}, {{moduleid}}));
+{{#hasmodules}}
+    autocomplete.enhance('#module-nav-{{uniqid}}', false, false, {{# quote }}{{# str }}filterbyactivity, tool_lp{{/ str }}{{/ quote }});
+{{/hasmodules}}
+});
+{{/js}}
index 9ffc29a..331c3e1 100644 (file)
@@ -35,7 +35,7 @@
 
     // No example context because the JS is connected to webservices
 }}
-<div class="float-sm-right card card-block">
+<div class="float-sm-right card card-block p-x-1 p-b-1">
 <p>{{{groupselector}}}</p>
 <form class="user-competency-course-navigation">
 {{#hasusers}}
diff --git a/admin/tool/lp/tests/behat/course_competencies.feature b/admin/tool/lp/tests/behat/course_competencies.feature
new file mode 100644 (file)
index 0000000..f765213
--- /dev/null
@@ -0,0 +1,63 @@
+@report @javascript @tool_lp
+Feature: See the competencies for an activity on the course competencies page.
+  As a student
+  In order to see only the competencies for an activity in the course competencies page.
+
+  Background:
+    Given the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Test-Framework | ID-FW1 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | Test-Comp1 | ID-FW1 |
+      | Test-Comp2 | ID-FW1 |
+    Given the following "courses" exist:
+      | shortname | fullname   |
+      | C1        | Course 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | name       | intro      | course | idnumber |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
+      | page     | PageName2  | PageDesc2  | C1     | PAGE2    |
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp2" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I am on "Course 1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Expand all"
+    And I set the field "Course competencies" to "Test-Comp1"
+    And I press "Save and return to course"
+    And I log out
+
+  @javascript
+  Scenario: Go to the competency course competencies page.
+    When I log in as "student1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    Then I should see "Test-Comp1"
+    And I should see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName1"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName2"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should not see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I should see "No competencies have been linked to this activity or resource."
index 44c9521..450570a 100644 (file)
@@ -461,4 +461,33 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('A', $summary->evidence[1]->gradename);
     }
 
+    public function test_data_for_course_competency_page() {
+        $this->setAdminUser();
+
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+        $f1 = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $course1 = $dg->create_course(array('category' => $this->category->id));
+        $cc = api::add_competency_to_course($course1->id, $c1->get('id'));
+
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 1, true);
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 2, true);
+
+        $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page');
+        $page = $pagegenerator->create_instance(array('course' => $course1->id));
+        $page2 = $pagegenerator->create_instance(array('course' => $course1->id));
+
+        $cm = get_coursemodule_from_instance('page', $page->id);
+        $cm2 = get_coursemodule_from_instance('page', $page2->id);
+        // Add the competency to the course module.
+        $ccm = api::add_competency_to_course_module($cm, $c1->get('id'));
+        $summary = external::data_for_course_competencies_page($course1->id, 0);
+        $summary2 = external::data_for_course_competencies_page($course1->id, $cm->id);
+        $summary3 = external::data_for_course_competencies_page($course1->id, $cm2->id);
+
+        $this->assertEquals(count($summary->competencies), 1);
+        $this->assertEquals(count($summary->competencies), count($summary2->competencies));
+        $this->assertEquals(count($summary3->competencies), 0);
+    }
 }
index 1ce5887..0b6ba81 100644 (file)
@@ -87,7 +87,7 @@ $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
-$string['readingthisemailgettheapp'] = 'Reading this in your e-mail? <a href="{$a}">Download the mobile app and receive notifications on your mobile devices</a>.';
+$string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
 $string['setuplink'] = 'App download page';
index ae3dfe3..f6eecce 100644 (file)
@@ -183,6 +183,7 @@ function uu_validate_user_upload_columns(csv_import_reader $cir, $stdfields, $pr
     $processed = array();
     foreach ($columns as $key=>$unused) {
         $field = $columns[$key];
+        $field = trim($field);
         $lcfield = core_text::strtolower($field);
         if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) {
             // standard fields are only lowercase
index 8ebce53..f61d13c 100644 (file)
@@ -208,7 +208,7 @@ You can also choose to display the courses in a list, with summary information,
 
 // 3.6 Messaging tour.
 $string['tour4_title_messaging'] = 'New messaging interface';
-$string['tour4_content_messaging'] = 'Moodle 3.6 provides a new interface to messaging, ability for group messaging within a course, along with better control over who can message you.';
+$string['tour4_content_messaging'] = 'New messaging features include group messaging within a course and better control over who can message you.';
 $string['tour4_title_icon'] = 'Messaging';
 $string['tour4_content_icon'] = 'You can access your messages from any page using this icon.
 
index 079537f..22ac0ed 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -82,7 +81,7 @@ abstract class setting_dependency {
      * Destroy all circular references. It helps PHP 5.2 a lot!
      */
     public function destroy() {
-        // No need to destroy anything recursively here, direct reset
+        // No need to destroy anything recursively here, direct reset.
         $this->setting = null;
         $this->dependentsetting = null;
     }
@@ -94,16 +93,19 @@ abstract class setting_dependency {
      * @return bool
      */
     final public function process_change($changetype, $oldvalue) {
-        // Check the type of change requested
+        // Check the type of change requested.
         switch ($changetype) {
-            // Process a status change
-            case base_setting::CHANGED_STATUS: return $this->process_status_change($oldvalue);
-            // Process a visibility change
-            case base_setting::CHANGED_VISIBILITY: return $this->process_visibility_change($oldvalue);
-            // Process a value change
-            case base_setting::CHANGED_VALUE: return $this->process_value_change($oldvalue);
+            // Process a status change.
+            case base_setting::CHANGED_STATUS:
+                return $this->process_status_change($oldvalue);
+            // Process a visibility change.
+            case base_setting::CHANGED_VISIBILITY:
+                return $this->process_visibility_change($oldvalue);
+            // Process a value change.
+            case base_setting::CHANGED_VALUE:
+                return $this->process_value_change($oldvalue);
         }
-        // Throw an exception if we get this far
+        // Throw an exception if we get this far.
         throw new backup_ui_exception('unknownchangetype');
     }
     /**
@@ -112,11 +114,11 @@ abstract class setting_dependency {
      * @return bool
      */
     protected function process_visibility_change($oldvisibility) {
-        // Store the current dependent settings visibility for comparison
+        // Store the current dependent settings visibility for comparison.
         $prevalue = $this->dependentsetting->get_visibility();
-        // Set it regardless of whether we need to
+        // Set it regardless of whether we need to.
         $this->dependentsetting->set_visibility($this->setting->get_visibility());
-        // Return true if it changed
+        // Return true if it changed.
         return ($prevalue != $this->dependentsetting->get_visibility());
     }
     /**
@@ -182,15 +184,16 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function __construct(base_setting $setting, base_setting $dependentsetting, $value, $defaultvalue = false) {
         parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = ($value)?(string)$value:0;
+        $this->value = ($value) ? (string)$value : 0;
     }
     /**
      * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) {
+        // If the setting is locked or the dependent setting should be locked then return true.
+        if ($this->setting->get_status() !== base_setting::NOT_LOCKED ||
+                $this->evaluate_disabled_condition($this->setting->get_value())) {
             return true;
         }
         // Else the dependent setting is not locked by this setting_dependency.
@@ -208,17 +211,25 @@ class setting_dependency_disabledif_equals extends setting_dependency {
             return false;
         }
         $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if ($this->setting->get_value() == $this->value) {
+        // If the setting is the desired value enact the dependency.
+        $settingvalue = $this->setting->get_value();
+        if ($this->evaluate_disabled_condition($settingvalue)) {
             // The dependent setting needs to be locked by hierachy and set to the
             // default value.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
+
+            // For checkboxes the default value is false, but when the setting is
+            // locked, the value should inherit from the parent setting.
+            if ($this->defaultvalue === false) {
+                $this->dependentsetting->set_value($settingvalue);
+            } else {
+                $this->dependentsetting->set_value($this->defaultvalue);
+            }
         } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
+            // We can unlock the dependent setting.
             $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
         }
-        // Return true if the value has changed for the dependent setting
+        // Return true if the value has changed for the dependent setting.
         return ($prevalue != $this->dependentsetting->get_value());
     }
     /**
@@ -227,17 +238,18 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool
      */
     protected function process_status_change($oldstatus) {
-        // Store the dependent status
+        // Store the dependent status.
         $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
+        // Store the current status.
         $currentstatus = $this->setting->get_status();
         if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && $this->setting->get_value() != $this->value) {
-                // Dependency has changes, is not fine, unlock the dependent setting
+            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY &&
+                    !$this->evaluate_disabled_condition($this->setting->get_value())) {
+                // Dependency has changes, is not fine, unlock the dependent setting.
                 $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
             }
         } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
+            // Make sure the dependent setting is also locked, in this case by hierarchy.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
         }
         // Return true if the dependent setting has changed.
@@ -248,17 +260,17 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool True if there were changes
      */
     public function enforce() {
-        // This will be set to true if ANYTHING changes
+        // This will be set to true if ANYTHING changes.
         $changes = false;
-        // First process any value changes
+        // First process any value changes.
         if ($this->process_value_change($this->setting->get_value())) {
             $changes = true;
         }
-        // Second process any status changes
+        // Second process any status changes.
         if ($this->process_status_change($this->setting->get_status())) {
             $changes = true;
         }
-        // Finally process visibility changes
+        // Finally process visibility changes.
         if ($this->process_visibility_change($this->setting->get_visibility())) {
             $changes = true;
         }
@@ -271,152 +283,72 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * This function should be abstract, but there will probably be existing sub-classes so we must provide a default
+     * implementation.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value == $this->value;
+    }
 }
 
 /**
-* A dependency that disables the secondary setting if the primary setting is
-* not equal to the provided value
-*
-* @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
-* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-*/
+ * A dependency that disables the secondary setting if the primary setting is
+ * not equal to the provided value
+ *
+ * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class setting_dependency_disabledif_not_equals extends setting_dependency_disabledif_equals {
+
     /**
-    * Enforces the dependency if required.
-    * @return bool True if there were changes
-    */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if (!$this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value != $this->value;
     }
+
     /**
-    * Returns an array of properties suitable to be used to define a moodleforms
-    * disabled command
-    * @return array
-    */
+     * Returns an array of properties suitable to be used to define a moodleforms
+     * disabled command
+     * @return array
+     */
     public function get_moodleform_properties() {
         return array(
-                'setting'=>$this->dependentsetting->get_ui_name(),
-                'dependenton'=>$this->setting->get_ui_name(),
-                'condition'=>'notequal',
-                'value'=>$this->value
+                'setting' => $this->dependentsetting->get_ui_name(),
+                'dependenton' => $this->setting->get_ui_name(),
+                'condition' => 'notequal',
+                'value' => $this->value
         );
     }
 }
 
-//with array
-class setting_dependency_disabledif_equals2 extends setting_dependency {
-    /**
-     * The value to compare to
-     * @var mixed
-     */
-    protected $value;
-    /**
-     * Creates the dependency
-     *
-     * @param base_setting $setting
-     * @param base_setting $dependentsetting
-     * @param mixed $value
-     * @param mixed $defaultvalue
-     */
-    public function __construct(base_setting $setting, base_setting $dependentsetting, array $value, $defaultvalue = false) {
-        parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = $value;
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || in_array($this->setting->get_value(), $this->value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if (in_array($this->setting->get_value(), $this->value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
+/**
+ * Disable if a value is in a list.
+ */
+class setting_dependency_disabledif_in_array extends setting_dependency_disabledif_equals {
+
     /**
-     * Processes a status change in the primary setting
-     * @param mixed $oldstatus
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
      * @return bool
      */
-    protected function process_status_change($oldstatus) {
-        // Store the dependent status
-        $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
-        $currentstatus = $this->setting->get_status();
-        if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && !in_array($this->setting->get_value(), $this->value)) {
-                // Dependency has changes, is not fine, unlock the dependent setting
-                $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-            }
-        } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-        }
-        // Return true if the dependent setting has changed.
-        return ($prevalue != $this->dependentsetting->get_status());
-    }
-    /**
-     * Enforces the dependency if required.
-     * @return bool True if there were changes
-     */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if ($this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+    protected function evaluate_disabled_condition($value) {
+        return in_array($value, $this->value);
     }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -424,14 +356,19 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
 }
 
+/**
+ * This class is here for backwards compatibility (terrible name).
+ */
+class setting_dependency_disabledif_equals2 extends setting_dependency_disabledif_in_array {
+}
 
 /**
  * A dependency that disables the secondary element if the primary element is
@@ -452,9 +389,9 @@ class setting_dependency_disabledif_checked extends setting_dependency_disabledi
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'checked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'checked'
         );
     }
 }
@@ -478,9 +415,9 @@ class setting_dependency_disabledif_not_checked extends setting_dependency_disab
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notchecked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notchecked'
         );
     }
 }
@@ -497,6 +434,16 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return !empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -504,50 +451,12 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (!empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
 
 /**
@@ -562,6 +471,16 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -569,47 +488,10 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
index 45bc4ec..63999fe 100644 (file)
@@ -45,7 +45,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test base_setting class
      */
-    function test_base_setting() {
+    public function test_base_setting() {
         // Instantiate base_setting and check everything
         $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN);
         $this->assertTrue($bs instanceof base_setting);
@@ -290,10 +290,54 @@ class backp_settings_testcase extends basic_testcase {
         $this->assertEquals($ubs3->get_status(), $ubs1->get_status());
     }
 
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do not flow from the parent to the child setting when the setting is locked by permissions.
+     */
+    public function test_dependency_empty_locked_by_permission_child_is_not_unlocked() {
+        // Check dependencies are working ok.
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 2);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 2);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+        $bs2->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+
+        // Unlocking the parent should NOT unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs2->get_status());
+    }
+
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do flow from the parent to the child setting when the setting is locked by config.
+     */
+    public function test_dependency_not_empty_locked_by_config_parent_is_unlocked() {
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 0);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 0);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_CONFIG);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_CONFIG, $bs1->get_status());
+
+        // Unlocking the parent should unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+        $this->assertEquals(base_setting::NOT_LOCKED, $bs2->get_status());
+    }
+
     /**
      * test backup_setting class
      */
-    function test_backup_setting() {
+    public function test_backup_setting() {
         // Instantiate backup_setting class and set level
         $bs = new mock_backup_setting('test', base_setting::IS_INTEGER, null);
         $bs->set_level(1);
@@ -340,7 +384,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test activity_backup_setting class
      */
-    function test_activity_backup_setting() {
+    public function test_activity_backup_setting() {
         $bs = new mock_activity_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::ACTIVITY_LEVEL);
 
@@ -355,7 +399,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test section_backup_setting class
      */
-    function test_section_backup_setting() {
+    public function test_section_backup_setting() {
         $bs = new mock_section_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::SECTION_LEVEL);
 
@@ -370,7 +414,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test course_backup_setting class
      */
-    function test_course_backup_setting() {
+    public function test_course_backup_setting() {
         $bs = new mock_course_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::COURSE_LEVEL);
 
index b6f51b8..79a0b78 100644 (file)
@@ -49,8 +49,8 @@ $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
 $string['completepercent'] = '{$a}% complete';
-$string['displaycategories'] = 'Display Categories';
-$string['displaycategories_help'] = 'Display the Course Category on dashboard course items including cards, list items and summary items';
+$string['displaycategories'] = 'Display categories';
+$string['displaycategories_help'] = 'Display the course category on dashboard course items including cards, list items and summary items.';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
index ff2e63c..b940a1e 100644 (file)
@@ -55,7 +55,9 @@
         <span class="sr-only">
             {{#str}}aria:coursecategory, core_course{{/str}}
         </span>
-        <div>{{{coursecategory}}}</div>
+        <span class="categoryname">
+            {{{coursecategory}}}
+        </span>
     {{/coursecategory}}
     {{$divider}}
         <div class="pl-1 pr-1">|</div>
index eafb72e..ab98fcb 100644 (file)
         data-course-id="{{{id}}}">
         <div class="row-fluid">
             <div class="{{#hasprogress}}col-md-6{{/hasprogress}}{{^hasprogress}}col-md-11 col-md-11{{/hasprogress}} d-flex align-items-center">
-                <a href="{{viewurl}}" class="coursename">
-                    <div class="text-muted muted d-flex" style="flex-flow:wrap;">
+                <div>
+                    <div class="text-muted muted d-flex flex-wrap">
                         <span class="sr-only">
                             {{#str}}aria:coursecategory, core_course{{/str}}
                         </span>
-                        {{$coursecategory}}
-                            <div>{{{coursecategory}}}</div>
-                        {{/coursecategory}}
+                        <span class="categoryname">
+                            {{{coursecategory}}}
+                        </span>
                         {{#showshortname}}
                         <div class="pl-1 pr-1">|</div>
                         <span class="sr-only">
                         <div>{{{shortname}}}</div>
                         {{/showshortname}}
                     </div>
-                    {{> core_course/favouriteicon }}
-                    <span class="sr-only">
-                        {{#str}}aria:coursename, core_course{{/str}}
-                    </span>
-                    {{{fullname}}}
-                </a>
+                    <a href="{{viewurl}}" class="coursename">
+                        {{> core_course/favouriteicon }}
+                        <span class="sr-only">
+                            {{#str}}aria:coursename, core_course{{/str}}
+                        </span>
+                        {{{fullname}}}
+                    </a>
+                </div>
             </div>
             {{#hasprogress}}
             <div class="col-md-5 pt-1">
index 8b35555..a57c690 100644 (file)
             </a>
 
             <div class="align-self-stretch d-flex flex-column w-100">
+                <div class="text-muted muted mb-1 d-flex flex-wrap">
+                    <span class="sr-only">
+                        {{#str}}aria:coursecategory, core_course{{/str}}
+                    </span>
+                    {{$coursecategory}}
+                    <span class="categoryname">
+                        {{{coursecategory}}}
+                    </span>
+                    {{/coursecategory}}
+                    {{#showshortname}}
+                    <div class="pl-1 pr-1">|</div>
+                    <span class="sr-only">
+                        {{#str}}aria:courseshortname, core_course{{/str}}
+                    </span>
+                    <div>{{{shortname}}}</div>
+                    {{/showshortname}}
+                </div>
                 <div class="d-flex mb-1">
                     <a href="{{viewurl}}" class="coursename">
-                        <div class="text-muted muted mb-1 d-flex" style="flex-flow:wrap;">
-                            <span class="sr-only">
-                                {{#str}}aria:coursecategory, core_course{{/str}}
-                            </span>
-                            {{$coursecategory}}
-                                <div>{{{coursecategory}}}</div>
-                            {{/coursecategory}}
-                            {{#showshortname}}
-                            <div class="pl-1 pr-1">|</div>
-                            <span class="sr-only">
-                                {{#str}}aria:courseshortname, core_course{{/str}}
-                            </span>
-                            <div>{{{shortname}}}</div>
-                            {{/showshortname}}
-                        </div>
                         {{> core_course/favouriteicon }}
                         <span class="sr-only">
                             {{#str}}aria:coursename, core_course{{/str}}
index 89c95e6..94e10ac 100644 (file)
@@ -27,7 +27,7 @@
 $string['everything'] = 'Everything';
 $string['courses'] = 'Categories and courses';
 $string['coursestructures'] = 'Categories, courses, and course structures';
-$string['courseactivities'] = 'Categories, courses, and course Activities';
+$string['courseactivities'] = 'Categories, courses, and course activities';
 $string['enabledockdesc'] = 'Allow the user to dock this block';
 $string['expansionlimit'] = 'Generate navigation for the following';
 $string['linkcategoriesdesc'] = 'Display categories as links';
index dd946d4..e615028 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 2c59a72..7043f75 100644 (file)
@@ -339,28 +339,6 @@ define([
                 .fail(Notification.exception);
         };
 
-        /**
-         * Convert the given event type into one of either user, site,
-         * group, category, or course.
-         *
-         * @param {String} eventType The calendar event type
-         * @return {String}
-         */
-        var normaliseEventType = function(eventType) {
-            switch (eventType) {
-                case 'user':
-                    return 'user';
-                case 'site':
-                    return 'site';
-                case 'group':
-                    return 'group';
-                case 'category':
-                    return 'category';
-                default:
-                    return 'course';
-            }
-        };
-
         /**
          * Get the CSS class to apply for the given event type.
          *
@@ -368,7 +346,7 @@ define([
          * @return {String}
          */
         var getEventTypeClassFromType = function(eventType) {
-            return 'calendar_event_' + normaliseEventType(eventType);
+            return 'calendar_event_' + eventType;
         };
 
         /**
@@ -385,12 +363,9 @@ define([
                     throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
                 }
                 var eventData = getEventResponse.event;
-                typeClass = getEventTypeClassFromType(eventData.eventtype);
+                typeClass = getEventTypeClassFromType(eventData.normalisedeventtype);
 
-                return getEventType(eventData.eventtype).then(function(eventType) {
-                    eventData.eventtype = eventType;
-                    return eventData;
-                });
+                return eventData;
             }).then(function(eventData) {
                 // Build the modal parameters from the event data.
                 var modalParams = {
@@ -422,19 +397,6 @@ define([
             }).fail(Notification.exception);
         };
 
-        /**
-         * Get the event type lang string.
-         *
-         * @param {String} eventType The event type.
-         * @return {promise} The lang string promise.
-         */
-        var getEventType = function(eventType) {
-            var lang = 'type' + normaliseEventType(eventType);
-            return Str.get_string(lang, 'core_calendar').then(function(langStr) {
-                return langStr;
-            });
-        };
-
         return {
             init: function(root) {
                 registerEventListeners(root);
index f4b629f..ea32de3 100644 (file)
@@ -52,9 +52,6 @@ class calendar_event_exporter extends event_exporter_base {
             'type' => PARAM_BOOL,
             'default' => false,
         ];
-        $values['calendareventtype'] = [
-            'type' => PARAM_TEXT,
-        ];
         $values['popupname'] = [
             'type' => PARAM_RAW,
         ];
@@ -172,8 +169,6 @@ class calendar_event_exporter extends event_exporter_base {
             $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
         }
 
-        $values['calendareventtype'] = $this->get_calendar_event_type();
-
         if ($event->get_course_module()) {
             $values = array_merge($values, $this->get_module_timestamp_limits($event));
         } else if ($hascourse && $course->id != SITEID && empty($event->get_group())) {
index 51ee3e4..015d71a 100644 (file)
@@ -55,7 +55,6 @@ class event_exporter extends event_exporter_base {
             'type' => event_action_exporter::read_properties_definition(),
             'optional' => true,
         ];
-
         return $values;
     }
 
index d20cfb3..08d14ac 100644 (file)
@@ -125,7 +125,7 @@ class event_exporter_base extends exporter {
                 'null' => NULL_ALLOWED
             ],
             'location' => [
-                'type' => PARAM_RAW_TRIMMED,
+                'type' => PARAM_RAW,
                 'optional' => true,
                 'default' => null,
                 'null' => NULL_ALLOWED
@@ -236,6 +236,12 @@ class event_exporter_base extends exporter {
                 'default' => null,
                 'null' => NULL_ALLOWED
             ],
+            'normalisedeventtype' => [
+                'type' => PARAM_TEXT
+            ],
+            'normalisedeventtypetext' => [
+                'type' => PARAM_TEXT
+            ],
         ];
     }
 
@@ -254,11 +260,14 @@ class event_exporter_base extends exporter {
         $values['isactionevent'] = false;
         $values['iscourseevent'] = false;
         $values['iscategoryevent'] = false;
+        $values['normalisedeventtype'] = $event->get_type();
         if ($moduleproxy = $event->get_course_module()) {
             // We need a separate property to flag if an event is action event.
             // That's required because canedit return true but action action events cannot be edited on the calendar UI.
             // But they are considered editable because you can drag and drop the event on the month view.
             $values['isactionevent'] = true;
+            // Activity events are normalised to "look" like course events.
+            $values['normalisedeventtype'] = 'course';
         } else if ($event->get_type() == 'course') {
             $values['iscourseevent'] = true;
         } else if ($event->get_type() == 'category') {
@@ -266,6 +275,7 @@ class event_exporter_base extends exporter {
         }
         $timesort = $event->get_times()->get_sort_time()->getTimestamp();
         $iconexporter = new event_icon_exporter($event, ['context' => $context]);
+        $values['normalisedeventtypetext'] = get_string('type' . $values['normalisedeventtype'], 'calendar');
 
         $values['icon'] = $iconexporter->export($output);
 
index 6ef434f..02c2d97 100644 (file)
@@ -2849,7 +2849,7 @@ function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $
     }
 
     $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
-            str_replace('\\', '', $event->properties['LOCATION'][0]->value);
+            trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value));
     $eventrecord->uuid = $event->properties['UID'][0]->value;
     $eventrecord->timemodified = time();
 
index 7d75b57..e82e42a 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/calendar_day
+    @template core_calendar/calendar_day
 
     Calendar day view.
 
index 74d48bb..fde6b2f 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/day_detailed
+    @template core_calendar/day_detailed
 
     Calendar day view.
 
diff --git a/calendar/templates/event_details.mustache b/calendar/templates/event_details.mustache
new file mode 100644 (file)
index 0000000..86fcda4
--- /dev/null
@@ -0,0 +1,120 @@
+{{!
+    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 core_calendar/event_details
+
+    Calendar event details.
+
+    The purpose of this template is to render the event details.
+
+    This template is used in the summary modal, day and upcoming views to output event information consistently
+    across the calendar.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+        "formattedtime": "Wednesday, 17 April, 9:27 AM",
+        "normalisedeventtype": "Group",
+        "description": "An random event description",
+        "location": "Moodle HQ",
+        "isactionevent": "true",
+        "course": {
+            "viewurl": "http://mymoodlesite/course/view.php?id=1",
+            "fullname": "Course name"
+        },
+        "source": "Ical imported",
+        "groupname": "Group 1",
+        "iscategoryevent": "true",
+        "category": {
+            "nestedname": "Category name"
+        },
+        "iscourseevent": "true",
+        "groupname": "Group name",
+        "subscription": "true",
+        "displayeventsource": "true",
+        "subscriptionname": "Subscription name",
+        "subscriptionurl": "http://subscriptionurl"
+    }
+}}
+<div class="row">
+    <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
+    <div class="col-xs-11">{{{formattedtime}}}</div>
+</div>
+<div class="row mt-1">
+    <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
+    <div class="col-xs-11">{{normalisedeventtypetext}}</div>
+</div>
+{{#description}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
+        <div class="description-content col-xs-11">{{{.}}}</div>
+    </div>
+{{/description}}
+{{#location}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
+        <div class="location-content col-xs-11">{{{.}}}</div>
+    </div>
+{{/location}}
+{{#isactionevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
+    </div>
+{{/isactionevent}}
+{{#iscategoryevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
+        <div class="col-xs-11">{{{category.nestedname}}}</div>
+    </div>
+{{/iscategoryevent}}
+{{#iscourseevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+    </div>
+{{/iscourseevent}}
+{{#groupname}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+    </div>
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
+        <div class="col-xs-11">{{{groupname}}}</div>
+    </div>
+{{/groupname}}
+{{#subscription}}
+    {{#displayeventsource}}
+        <div class="row mt-1">
+            <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
+            <div class="col-xs-11">
+                {{#subscriptionurl}}
+                    <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
+                {{/subscriptionurl}}
+                {{^subscriptionurl}}
+                    <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
+                {{/subscriptionurl}}
+            </div>
+        </div>
+    {{/displayeventsource}}
+{{/subscription}}
index 98431ea..ccbf773 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/event_item
+    @template core_calendar/event_item
 
     Calendar event item.
 
 
     Example context (json):
     {
+        "id": 1,
+        "name": "Sample event name",
+        "normalisedeventtype": "course",
+        "course": {
+            "id": 1
+        },
+        "canedit": true,
+        "candelete": true,
+        "isactionevent": true,
+        "icon": {
+            "key": "i/courseevent",
+            "component": "core",
+            "alttext": "Some course event"
+        },
+        "editurl": "#",
+        "url": "#"
     }
 }}
 <div{{!
     }} data-type="event"{{!
     }} data-course-id="{{course.id}}"{{!
     }} data-event-id="{{id}}"{{!
-    }} class="event"{{!
-    }} data-eventtype-{{calendareventtype}}="1"{{!
+    }} class="event m-t-1"{{!
+    }} data-eventtype-{{normalisedeventtype}}="1"{{!
     }} data-event-title="{{name}}"{{!
     }} data-event-count="{{eventcount}}"{{!
     }}>
-    <div class="card">
-        <div class="box card-header clearfix p-y-1">
+    <div class="card rounded">
+        <div class="box card-header clearfix calendar_event_{{normalisedeventtype}}">
             <div class="commands float-sm-right">
                 {{#canedit}}
                     {{#candelete}}
             {{#icon}}<div class="d-inline-block mt-1 align-top">{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}</div>{{/icon}}
             <div class="d-inline-block">
                 <h3 class="name d-inline-block">{{{name}}}</h3>
-                <span class="date float-sm-right mr-1">{{{formattedtime}}}</span>
-                <div class="location">{{#location}}{{{location}}}{{/location}}</div>
             </div>
         </div>
-        <div class="description card-block calendar_event_{{eventtype}}">
-            <p>{{{description}}}</p>
-            {{#iscourseevent}}
-                <div><a href="{{url}}">{{course.fullname}}</a></div>
-            {{/iscourseevent}}
-            {{> core_calendar/event_subscription}}
-            {{#isactionevent}}
-                <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
-            {{/isactionevent}}
-            {{#groupname}}
-                <div><a href="{{url}}">{{{course.fullname}}}</a></div>
-                <div>{{{groupname}}}</div>
-            {{/groupname}}
+        <div class="description card-body">
+            {{> core_calendar/event_details }}
         </div>
+        {{#isactionevent}}
+            <div class="card-footer text-right bg-transparent">
+                <a href="{{url}}" class="card-link">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+            </div>
+        {{/isactionevent}}
     </div>
 </div>
index f75157a..ae276ae 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/event_list
+    @template core_calendar/event_list
 
     Calendar event list.
 
index 22d8d27..2d333b4 100644 (file)
     }} data-edit-url="{{editurl}}"{{!
     }}>
     <div class="container-fluid">
-        <div class="row">
-            <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{formattedtime}}}</div>
-        </div>
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{eventtype}}</div>
-        </div>
-        {{#description}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
-            <div class="description-content col-xs-11">{{{.}}}</div>
-        </div>
-        {{/description}}
-        {{#location}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
-            <div class="location-content col-xs-11">{{{.}}}</div>
-        </div>
-        {{/location}}
-        {{#isactionevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
-        </div>
-        {{/isactionevent}}
-        {{#iscategoryevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{category.nestedname}}}</div>
-        </div>
-        {{/iscategoryevent}}
-        {{#iscourseevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
-        </div>
-        {{/iscourseevent}}
-        {{#groupname}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
-        </div>
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{groupname}}}</div>
-        </div>
-        {{/groupname}}
-        {{#subscription}}
-            {{#displayeventsource}}
-                <div class="row mt-1">
-                    <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
-                    <div class="col-xs-11">
-                        {{#subscriptionurl}}
-                            <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
-                        {{/subscriptionurl}}
-                        {{^subscriptionurl}}
-                            <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
-                        {{/subscriptionurl}}
-                    </div>
-                </div>
-            {{/displayeventsource}}
-        {{/subscription}}
+        {{> core_calendar/event_details }}
     </div>
 </div>
index 5041ca2..84ce421 100644 (file)
@@ -87,7 +87,7 @@
                                         {{/underway}}
                                         {{^underway}}
                                             <li data-region="event-item"
-                                                data-eventtype-{{calendareventtype}}="1"
+                                                data-eventtype-{{normalisedeventtype}}="1"
                                                 {{#draggable}}
                                                     draggable="true"
                                                     data-drag-type="move"
                                                 {{/draggable}}>
 
                                                 <a data-action="view-event" data-event-id="{{id}}" href="{{url}}" title="{{name}}">
-                                                    <span class="badge badge-circle calendar_event_{{calendareventtype}}">
+                                                    <span class="badge badge-circle calendar_event_{{normalisedeventtype}}">
                                                         &nbsp;
                                                     </span>
                                                     {{> core_calendar/event_icon}}
index 2a34335..b1aa06f 100644 (file)
                                 {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}}
                                 {{$content}}
                                     {{#events}}
-                                        <div data-popover-eventtype-{{calendareventtype}}="1">
+                                        <div data-popover-eventtype-{{normalisedeventtype}}="1">
                                             {{#modulename}}
                                                 {{#pix}} icon, {{modulename}} {{/pix}}
                                             {{/modulename}}
index 082d5bc..c952877 100644 (file)
@@ -41,7 +41,7 @@
     {{#events}}
         <div{{!
             }} class="event"{{!
-            }} data-eventtype-{{calendareventtype}}="1"{{!
+            }} data-eventtype-{{normalisedeventtype}}="1"{{!
             }} data-region="event-item"{{!
         }}>
             <span>{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}</span>
index 405b495..026ac44 100644 (file)
@@ -192,8 +192,8 @@ class course_module_competency extends persistent {
         $sql = 'SELECT COUNT(comp.id)
                   FROM {' . self::TABLE . '} coursemodulecomp
                   JOIN {' . competency::TABLE . '} comp
-                    ON coursecomp.competencyid = comp.id
-                 WHERE coursecomp.cmid = ? ';
+                    ON coursemodulecomp.competencyid = comp.id
+                 WHERE coursemodulecomp.cmid = ? ';
         $params = array($cmid);
 
         $results = $DB->count_records_sql($sql, $params);
diff --git a/competency/tests/course_module_competency_test.php b/competency/tests/course_module_competency_test.php
new file mode 100644 (file)
index 0000000..8d951dc
--- /dev/null
@@ -0,0 +1,72 @@
+<?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/>.
+
+/**
+ * Course module competency persistent class tests.
+ *
+ * @package    core_competency
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+use core_competency\course_module_competency;
+
+/**
+ * Course module competency persistent testcase.
+ *
+ * @package    core_competency
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_competency_course_module_competency_testcase extends advanced_testcase {
+
+    public function test_count_competencies() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+
+        $c1 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+
+        $framework = $lpg->create_framework();
+        $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));   // In C1, and C2.
+        $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));   // In C2.
+        $lpg->create_course_competency(array('competencyid' => $comp1->get('id'), 'courseid' => $c1->id));
+        $lpg->create_course_competency(array('competencyid' => $comp2->get('id'), 'courseid' => $c1->id));
+
+        $assign1a = $dg->create_module('assign', ['course' => $c1]);
+        $assign1b = $dg->create_module('assign', ['course' => $c1]);
+        $cmc1a = $lpg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1a->cmid]);
+        $cmc1b = $lpg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1b->cmid]);
+        $cmc2b = $lpg->create_course_module_competency(['competencyid' => $comp2->get('id'), 'cmid' => $assign1b->cmid]);
+
+        // Enrol the user 1 in C1.
+        $dg->enrol_user($u1->id, $c1->id);
+
+        $all = course_module_competency::list_course_module_competencies($assign1a->cmid);
+        $this->assertEquals(course_module_competency::count_competencies($assign1a->cmid), count($all));
+
+        $all = course_module_competency::list_course_module_competencies($assign1b->cmid);
+        $this->assertEquals(course_module_competency::count_competencies($assign1b->cmid), count($all));
+    }
+
+}
index 0c6456b..bd44260 100644 (file)
@@ -2463,6 +2463,20 @@ class core_course_external extends external_api {
         $coursereturns['contacts']          = $coursecontacts;
         $coursereturns['enrollmentmethods'] = $enroltypes;
         $coursereturns['sortorder']         = $course->sortorder;
+
+        $handler = core_course\customfield\course_handler::create();
+        if ($customfields = $handler->export_instance_data($course->id)) {
+            $coursereturns['customfields'] = [];
+            foreach ($customfields as $data) {
+                $coursereturns['customfields'][] = [
+                    'type' => $data->get_type(),
+                    'value' => $data->get_value(),
+                    'name' => $data->get_name(),
+                    'shortname' => $data->get_shortname()
+                ];
+            }
+        }
+
         return $coursereturns;
     }
 
index dbe7ce2..3042864 100644 (file)
@@ -46,7 +46,7 @@ $PAGE->set_pagelayout('incourse');
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($course->fullname);
 
-$context = context_course::instance($courseid);
+$context = context_course::instance($course->id);
 if (empty($CFG->enablecoursepublishing) || !has_capability('moodle/course:publish', $context)) {
     throw new moodle_exception('nopermission');
 }
index 34d02d3..e330e1d 100644 (file)
@@ -53,7 +53,7 @@ require_capability('moodle/course:request', $context);
 
 // Set up the form.
 $data = course_request::prepare();
-$requestform = new course_request_form($url, compact('editoroptions'));
+$requestform = new course_request_form($url);
 $requestform->set_data($data);
 
 $strtitle = get_string('courserequest');
index 0ea7e8f..1aba15c 100644 (file)
@@ -43,8 +43,8 @@
     </a>
     <div class="card-body pr-1 course-info-container" id="course-info-container-{{id}}-{{uniqid}}">
         <div class="d-flex align-items-start">
-            <a href="{{viewurl}}" class="coursename mr-2 text-truncate">
-                <div class="text-muted muted d-flex w-100 mb-1 text-truncate" style="flex-flow:wrap;">
+            <div>
+                <div class="text-muted muted d-flex w-100 mb-1 text-truncate flex-wrap">
                     {{$coursecategory}}{{/coursecategory}}
                     {{#showshortname}}
                     {{$divider}}{{/divider}}
                     </div>
                     {{/showshortname}}
                 </div>
-                {{> core_course/favouriteicon }}
-                <span class="sr-only">
-                    {{#str}}aria:coursename, core_course{{/str}}
-                </span>
-                {{$coursename}}{{/coursename}}
-            </a>
+                <a href="{{viewurl}}" class="coursename mr-2 text-truncate">
+                    {{> core_course/favouriteicon }}
+                    <span class="sr-only">
+                            {{#str}}aria:coursename, core_course{{/str}}
+                        </span>
+                    {{$coursename}}{{/coursename}}
+                </a>
+            </div>
             {{$menu}}{{/menu}}
         </div>
     </div>
index a3c73b6..45d55dd 100644 (file)
@@ -23,7 +23,7 @@ Feature: Fields locked control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
index e8650a3..802f6fa 100644 (file)
@@ -23,7 +23,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -43,7 +43,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field  |
       | Short name | testfield   |
@@ -63,7 +63,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field     |
       | Short name | testfield      |
index 5cba1fa..8aeb6c8 100644 (file)
@@ -2370,7 +2370,13 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
         $course1 = self::getDataGenerator()->create_course(
             array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
-        $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2'));
+
+        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
+        $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
+            'categoryid' => $fieldcategory->get('id')];
+        $field = self::getDataGenerator()->create_custom_field($customfield);
+        $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
+        $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2', 'customfields' => [$customfieldvalue]));
 
         $student1 = self::getDataGenerator()->create_user();
         $user1 = self::getDataGenerator()->create_user();
@@ -2384,16 +2390,16 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(3, $result['courses']);
         // Expect to receive all the fields.
-        $this->assertCount(37, $result['courses'][0]);
-        $this->assertCount(38, $result['courses'][1]);  // One more field because is not the site course.
-        $this->assertCount(38, $result['courses'][2]);  // One more field because is not the site course.
+        $this->assertCount(38, $result['courses'][0]);
+        $this->assertCount(39, $result['courses'][1]);  // One more field because is not the site course.
+        $this->assertCount(39, $result['courses'][2]);  // One more field because is not the site course.
 
         $result = core_course_external::get_courses_by_field('id', $course1->id);
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(1, $result['courses']);
         $this->assertEquals($course1->id, $result['courses'][0]['id']);
         // Expect to receive all the fields.
-        $this->assertCount(38, $result['courses'][0]);
+        $this->assertCount(39, $result['courses'][0]);
         // Check default values for course format topics.
         $this->assertCount(2, $result['courses'][0]['courseformatoptions']);
         foreach ($result['courses'][0]['courseformatoptions'] as $option) {
@@ -2409,6 +2415,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(1, $result['courses']);
         $this->assertEquals($course2->id, $result['courses'][0]['id']);
+        // Check custom fields properly returned.
+        unset($customfield['categoryid']);
+        $this->assertEquals([array_merge($customfield, $customfieldvalue)], $result['courses'][0]['customfields']);
 
         $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
@@ -2446,15 +2455,15 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $result = core_course_external::get_courses_by_field();
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(2, $result['courses']);
-        $this->assertCount(30, $result['courses'][0]);
-        $this->assertCount(31, $result['courses'][1]);  // One field more (course format options), not present in site course.
+        $this->assertCount(31, $result['courses'][0]);
+        $this->assertCount(32, $result['courses'][1]);  // One field more (course format options), not present in site course.
 
         $result = core_course_external::get_courses_by_field('id', $course1->id);
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(1, $result['courses']);
         $this->assertEquals($course1->id, $result['courses'][0]['id']);
         // Expect to receive all the files that a student can see.
-        $this->assertCount(31, $result['courses'][0]);
+        $this->assertCount(32, $result['courses'][0]);
 
         // Check default filters.
         $filters = $result['courses'][0]['filters'];
@@ -2499,15 +2508,15 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $result = core_course_external::get_courses_by_field();
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(2, $result['courses']);
-        $this->assertCount(30, $result['courses'][0]);  // Site course.
-        $this->assertCount(13, $result['courses'][1]);  // Only public information, not enrolled.
+        $this->assertCount(31, $result['courses'][0]);  // Site course.
+        $this->assertCount(14, $result['courses'][1]);  // Only public information, not enrolled.
 
         $result = core_course_external::get_courses_by_field('id', $course1->id);
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
         $this->assertCount(1, $result['courses']);
         $this->assertEquals($course1->id, $result['courses'][0]['id']);
         // Expect to receive all the files that a authenticated can see.
-        $this->assertCount(13, $result['courses'][0]);
+        $this->assertCount(14, $result['courses'][0]);
 
         // Course 2 is not visible.
         $result = core_course_external::get_courses_by_field('id', $course2->id);
index 74a41d9..cffdc85 100644 (file)
@@ -27,4 +27,4 @@ $string['checkedbydefault'] = 'Checked by default';
 $string['errorconfigunique'] = 'The checkbox field cannot be defined as unique.';
 $string['pluginname'] = 'Checkbox';
 $string['privacy:metadata'] = 'The Checkbox field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the checkbox field';
+$string['specificsettings'] = 'Checkbox field settings';
index fd5b3cc..d23c2e2 100644 (file)
@@ -32,4 +32,4 @@ $string['mindate'] = 'Minimum value';
 $string['mindateaftermax'] = 'The minimum value cannot be bigger than the maximum value.';
 $string['pluginname'] = 'Date and time';
 $string['privacy:metadata'] = 'The Date and time field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the date and time field';
+$string['specificsettings'] = 'Date and time field settings';
index 81dc118..52f9809 100644 (file)
@@ -30,4 +30,4 @@ $string['invalidoption'] = 'Invalid option selected';
 $string['menuoptions'] = 'Menu options (one per line)';
 $string['pluginname'] = 'Dropdown menu';
 $string['privacy:metadata'] = 'The Dropdown menu field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the dropdown menu field';
+$string['specificsettings'] = 'Dropdown menu field settings';
index c9e47a7..de273bf 100644 (file)
@@ -28,17 +28,17 @@ $string['displaysize'] = 'Form input size';
 $string['errorconfigdisplaysize'] = 'The form input size must be between 1 and 200 characters.';
 $string['errorconfiglinkplaceholder'] = 'The link must contain a placeholder $$.';
 $string['errorconfiglinksyntax'] = 'The link must be a valid URL starting with either http:// or https://.';
-$string['errorconfigmaxlen'] = 'The maximum length must be between 1 and 1333.';
-$string['errormaxlength'] = 'This field maximum length is {$a}.';
+$string['errorconfigmaxlen'] = 'The maximum number of characters allowed must be between 1 and 1333.';
+$string['errormaxlength'] = 'The maximum number of characters allowed in this field is {$a}.';
 $string['islink'] = 'Link field';
 $string['islink_help'] = 'To transform the text into a link, enter a URL containing $$ as a placeholder, where $$ will be replaced with the text. For example, to transform a Twitter ID to a link, enter http://twitter.com/$$.';
 $string['ispassword'] = 'Password field';
 $string['linktarget'] = 'Link target';
-$string['maxlength'] = 'Maximum length';
+$string['maxlength'] = 'Maximum number of characters';
 $string['newwindow'] = 'New window';
 $string['none'] = 'None';
-$string['pluginname'] = 'Text field';
-$string['privacy:metadata'] = 'The Text field field type plugin doesn\'t store any personal data; it uses tables defined in core.';
+$string['pluginname'] = 'Short text';
+$string['privacy:metadata'] = 'The Short text field type plugin doesn\'t store any personal data; it uses tables defined in core.';
 $string['sameframe'] = 'Same frame';
 $string['samewindow'] = 'Same window';
-$string['specificsettings'] = 'Settings for the text field';
+$string['specificsettings'] = 'Short text field settings';
index e1bed82..da4ad41 100644 (file)
@@ -13,7 +13,7 @@ Feature: Managers can manage course custom fields text
 
   Scenario: Create a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -23,7 +23,7 @@ Feature: Managers can manage course custom fields text
 
   Scenario: Edit a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -40,7 +40,7 @@ Feature: Managers can manage course custom fields text
   @javascript
   Scenario: Delete a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -64,7 +64,7 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | See more on website       |
       | Short name | testfield                 |
@@ -94,11 +94,11 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
-      | Maximum length | 3          |
+      | Maximum number of characters | 3          |
     And I press "Save changes"
     And I log out
     Then I log in as "teacher1"
@@ -107,7 +107,7 @@ Feature: Managers can manage course custom fields text
     And I set the following fields to these values:
       | Test field | 1234 |
     And I press "Save and display"
-    Then I should see "This field maximum length is 3"
+    Then I should see "The maximum number of characters allowed in this field is 3."
 
   Scenario: A text field with a default value must be shown on listing but allow empty values that will not be shown
     Given the following "users" exist:
@@ -121,7 +121,7 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name          | Test field  |
       | Short name    | testfield   |
index 7149604..ca601f4 100644 (file)
@@ -26,4 +26,4 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'Text area';
 $string['privacy:metadata'] = 'The Text area field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the text area field';
+$string['specificsettings'] = 'Text area field settings';
index 247f5ce..90bfacd 100644 (file)
@@ -101,7 +101,7 @@ Feature: Teachers can edit course custom fields
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
     And I press "Save changes"
index d0aea96..56c0f23 100644 (file)
@@ -22,7 +22,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -43,7 +43,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
index 8fe7063..e834eaa 100644 (file)
@@ -22,7 +22,7 @@ Feature: Uniqueness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name        | Test field |
       | Short name  | testfield  |
index 9bf0182..bb88d11 100644 (file)
@@ -353,7 +353,8 @@ class course_enrolment_table extends html_table implements renderable {
      * @var array
      */
     protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'lastaccess', 'lastcourseaccess' );
+            'alternatename', 'username', 'idnumber', 'email', 'phone1', 'phone2',
+            'institution', 'department', 'lastaccess', 'lastcourseaccess');
 
     /**
      * Constructs the table
diff --git a/favourites/classes/local/service/component_favourite_service.php b/favourites/classes/local/service/component_favourite_service.php
new file mode 100644 (file)
index 0000000..84fea93
--- /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/>.
+
+/**
+ * Contains the component_favourite_service class, part of the service layer for the favourites subsystem.
+ *
+ * @package   core_favourites
+ * @copyright 2019 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_favourites\local\service;
+use \core_favourites\local\repository\favourite_repository_interface;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class service, providing an single API for interacting with the favourites subsystem, for all favourites of a specific component.
+ *
+ * This class provides operations which can be applied to favourites within a component, based on type and context identifiers.
+ *
+ * All object persistence is delegated to the favourite_repository_interface object.
+ *
+ * @copyright 2019 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class component_favourite_service {
+
+    /** @var favourite_repository_interface $repo the favourite repository object. */
+    protected $repo;
+
+    /** @var int $component the frankenstyle component name to which this favourites service is scoped. */
+    protected $component;
+
+    /**
+     * The component_favourite_service constructor.
+     *
+     * @param string $component The frankenstyle name of the component to which this service operations are scoped.
+     * @param \core_favourites\local\repository\favourite_repository_interface $repository a favourites repository.
+     * @throws \moodle_exception if the component name is invalid.
+     */
+    public function __construct(string $component, favourite_repository_interface $repository) {
+        if (!in_array($component, \core_component::get_component_names())) {
+            throw new \moodle_exception("Invalid component name '$component'");
+        }
+        $this->repo = $repository;
+        $this->component = $component;
+    }
+
+
+    /**
+     * Delete a collection of favourites by type, and optionally for a given context.
+     *
+     * E.g. delete all favourites of type 'message_conversations' and for a specific CONTEXT_COURSE context.
+     *
+     * @param string $itemtype the type of the favourited items.
+     * @param \context $context the context of the items which were favourited.
+     */
+    public function delete_favourites_by_type(string $itemtype, \context $context = null) {
+        $criteria = ['component' => $this->component, 'itemtype' => $itemtype] + ($context ? ['contextid' => $context->id] : []);
+        $this->repo->delete_by($criteria);
+    }
+}
index f158003..c9143b1 100644 (file)
@@ -45,5 +45,15 @@ class service_factory {
     public static function get_service_for_user_context(\context_user $context) : local\service\user_favourite_service {
         return new local\service\user_favourite_service($context, new local\repository\favourite_repository());
     }
+
+    /**
+     * Returns a basic service object providing operations for favourites belonging to a given component.
+     *
+     * @param string $component frankenstyle component name.
+     * @return local\service\component_favourite_service the service object.
+     */
+    public static function get_service_for_component(string $component) : local\service\component_favourite_service {
+        return new local\service\component_favourite_service($component, new local\repository\favourite_repository());
+    }
 }
 
diff --git a/favourites/tests/component_favourite_service_test.php b/favourites/tests/component_favourite_service_test.php
new file mode 100644 (file)
index 0000000..2a6ef4a
--- /dev/null
@@ -0,0 +1,273 @@
+<?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/>.
+
+/**
+ * Testing the service layer within core_favourites.
+ *
+ * @package    core_favourites
+ * @category   test
+ * @copyright  2019 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+use \core_favourites\local\entity\favourite;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test class covering the component_favourite_service within the service layer of favourites.
+ *
+ * @copyright  2019 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class component_favourite_service_testcase extends advanced_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    // Basic setup stuff to be reused in most tests.
+    protected function setup_users_and_courses() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user1context = \context_user::instance($user1->id);
+        $user2 = self::getDataGenerator()->create_user();
+        $user2context = \context_user::instance($user2->id);
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+        $course1context = context_course::instance($course1->id);
+        $course2context = context_course::instance($course2->id);
+        return [$user1context, $user2context, $course1context, $course2context];
+    }
+
+    /**
+     * Generates an in-memory repository for testing, using an array store for CRUD stuff.
+     *
+     * @param array $mockstore
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    protected function get_mock_repository(array $mockstore) {
+        // This mock will just store data in an array.
+        $mockrepo = $this->getMockBuilder(\core_favourites\local\repository\favourite_repository_interface::class)
+            ->setMethods([])
+            ->getMock();
+        $mockrepo->expects($this->any())
+            ->method('add')
+            ->will($this->returnCallback(function(favourite $favourite) use (&$mockstore) {
+                // Mock implementation of repository->add(), where an array is used instead of the DB.
+                // Duplicates are confirmed via the unique key, and exceptions thrown just like a real repo.
+                $key = $favourite->userid . $favourite->component . $favourite->itemtype . $favourite->itemid
+                    . $favourite->contextid;
+
+                // Check the objects for the unique key.
+                foreach ($mockstore as $item) {
+                    if ($item->uniquekey == $key) {
+                        throw new \moodle_exception('Favourite already exists');
+                    }
+                }
+                $index = count($mockstore);     // Integer index.
+                $favourite->uniquekey = $key;   // Simulate the unique key constraint.
+                $favourite->id = $index;
+                $mockstore[$index] = $favourite;
+                return $mockstore[$index];
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('find_by')
+            ->will($this->returnCallback(function(array $criteria, int $limitfrom = 0, int $limitnum = 0) use (&$mockstore) {
+                // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
+                foreach ($mockstore as $index => $mockrow) {
+                    $mockrowarr = (array)$mockrow;
+                    if (array_diff($criteria, $mockrowarr) == []) {
+                        $returns[$index] = $mockrow;
+                    }
+                }
+                // Return a subset of the records, according to the paging options, if set.
+                if ($limitnum != 0) {
+                    return array_slice($returns, $limitfrom, $limitnum);
+                }
+                // Otherwise, just return the full set.
+                return $returns;
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('find_favourite')
+            ->will($this->returnCallback(function(int $userid, string $comp, string $type, int $id, int $ctxid) use (&$mockstore) {
+                // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
+                $crit = ['userid' => $userid, 'component' => $comp, 'itemtype' => $type, 'itemid' => $id, 'contextid' => $ctxid];
+                foreach ($mockstore as $fakerow) {
+                    $fakerowarr = (array)$fakerow;
+                    if (array_diff($crit, $fakerowarr) == []) {
+                        return $fakerow;
+                    }
+                }
+                throw new \dml_missing_record_exception("Item not found");
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('find')
+            ->will($this->returnCallback(function(int $id) use (&$mockstore) {
+                return $mockstore[$id];
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('exists')
+            ->will($this->returnCallback(function(int $id) use (&$mockstore) {
+                return array_key_exists($id, $mockstore);
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('count_by')
+            ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
+                $count = 0;
+                // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
+                foreach ($mockstore as $index => $mockrow) {
+                    $mockrowarr = (array)$mockrow;
+                    if (array_diff($criteria, $mockrowarr) == []) {
+                        $count++;
+                    }
+                }
+                return $count;
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('delete')
+            ->will($this->returnCallback(function(int $id) use (&$mockstore) {
+                foreach ($mockstore as $mockrow) {
+                    if ($mockrow->id == $id) {
+                        unset($mockstore[$id]);
+                    }
+                }
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('delete_by')
+            ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
+                // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
+                foreach ($mockstore as $index => $mockrow) {
+                    $mockrowarr = (array)$mockrow;
+                    if (array_diff($criteria, $mockrowarr) == []) {
+                        unset($mockstore[$index]);
+                    }
+                }
+            })
+        );
+        $mockrepo->expects($this->any())
+            ->method('exists_by')
+            ->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
+                // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
+                foreach ($mockstore as $index => $mockrow) {
+                    $mockrowarr = (array)$mockrow;
+                    echo "Here";
+                    if (array_diff($criteria, $mockrowarr) == []) {
+                        return true;
+                    }
+                }
+                return false;
+            })
+        );
+        return $mockrepo;
+    }
+
+    /**
+     * Test confirming the deletion of favourites by type, but with no optional context filter provided.
+     */
+    public function test_delete_favourites_by_type() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Get a user_favourite_service for each user.
+        $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
+        $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
+        $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
+
+        // Favourite both courses for both users.
+        $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
+        $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
+        $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
+        $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
+        $this->assertTrue($repo->exists($fav1->id));
+        $this->assertTrue($repo->exists($fav2->id));
+        $this->assertTrue($repo->exists($fav3->id));
+        $this->assertTrue($repo->exists($fav4->id));
+
+        // Favourite something else arbitrarily.
+        $fav5 = $user2service->create_favourite('core_user', 'course', $course2context->instanceid, $course2context);
+        $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course2context->instanceid, $course2context);
+
+        // Get a component_favourite_service to perform the type based deletion.
+        $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
+
+        // Delete all 'course' type favourites (for all users at ANY context).
+        $service->delete_favourites_by_type('course');
+
+        // Verify the favourites don't exist.
+        $this->assertFalse($repo->exists($fav1->id));
+        $this->assertFalse($repo->exists($fav2->id));
+        $this->assertFalse($repo->exists($fav3->id));
+        $this->assertFalse($repo->exists($fav4->id));
+
+        // Verify favourites of other types or for other components are not affected.
+        $this->assertTrue($repo->exists($fav5->id));
+        $this->assertTrue($repo->exists($fav6->id));
+
+        // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
+        $this->assertNull($service->delete_favourites_by_type('course'));
+    }
+
+    /**
+     * Test confirming the deletion of favourites by type and with the optional context filter provided.
+     */
+    public function test_delete_favourites_by_type_with_context() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Get a user_favourite_service for each user.
+        $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
+        $user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
+        $user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo);
+
+        // Favourite both courses for both users.
+        $fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
+        $fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
+        $fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
+        $fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
+        $this->assertTrue($repo->exists($fav1->id));
+        $this->assertTrue($repo->exists($fav2->id));
+        $this->assertTrue($repo->exists($fav3->id));
+        $this->assertTrue($repo->exists($fav4->id));
+
+        // Favourite something else arbitrarily.
+        $fav5 = $user2service->create_favourite('core_user', 'course', $course1context->instanceid, $course1context);
+        $fav6 = $user2service->create_favourite('core_course', 'whatnow', $course1context->instanceid, $course1context);
+
+        // Get a component_favourite_service to perform the type based deletion.
+        $service = new \core_favourites\local\service\component_favourite_service('core_course', $repo);
+
+        // Delete all 'course' type favourites (for all users at ONLY the course 1 context).
+        $service->delete_favourites_by_type('course', $course1context);
+
+        // Verify the favourites for course 1 context don't exist.
+        $this->assertFalse($repo->exists($fav1->id));
+        $this->assertFalse($repo->exists($fav2->id));
+
+        // Verify the favourites for the same component and type, but NOT for the same contextid and unaffected.
+        $this->assertTrue($repo->exists($fav3->id));
+        $this->assertTrue($repo->exists($fav4->id));
+
+        // Verify favourites of other types or for other components are not affected.
+        $this->assertTrue($repo->exists($fav5->id));
+        $this->assertTrue($repo->exists($fav6->id));
+
+        // Try to delete favourites for a type which we know doesn't exist. Verify no exception.
+        $this->assertNull($service->delete_favourites_by_type('course', $course1context));
+    }
+}
index a99f30f..fd6276c 100644 (file)
@@ -212,8 +212,8 @@ class favourite_repository_testcase extends advanced_testcase {
 
         $favouritesrepo = new favourite_repository($user1context);
 
-        // Verify that for an empty repository, find_all returns an empty array.
-        $this->assertEquals([], $favouritesrepo->find_all());
+        // Verify that only two self-conversations are found.
+        $this->assertCount(2, $favouritesrepo->find_all());
 
         // Save a favourite for 2 courses, in different areas.
         $favourite = new favourite(
@@ -233,9 +233,9 @@ class favourite_repository_testcase extends advanced_testcase {
         $favouritesrepo->add($favourite);
         $favouritesrepo->add($favourite2);
 
-        // Verify that find_all returns both of our favourites.
+        // Verify that find_all returns both of our favourites + two self-conversations.
         $favourites = $favouritesrepo->find_all();
-        $this->assertCount(2, $favourites);
+        $this->assertCount(4, $favourites);
         foreach ($favourites as $fav) {
             $this->assertInstanceOf(favourite::class, $fav);
             $this->assertObjectHasAttribute('id', $fav);
@@ -251,11 +251,11 @@ class favourite_repository_testcase extends advanced_testcase {
 
         $favouritesrepo = new favourite_repository($user1context);
 
-        // Verify that for an empty repository, find_all with any combination of page options returns an empty array.
-        $this->assertEquals([], $favouritesrepo->find_all(0, 0));
-        $this->assertEquals([], $favouritesrepo->find_all(0, 10));
-        $this->assertEquals([], $favouritesrepo->find_all(1, 0));
-        $this->assertEquals([], $favouritesrepo->find_all(1, 10));
+        // Verify that for an empty repository, find_all with any combination of page options returns only self-conversations.
+        $this->assertCount(2, $favouritesrepo->find_all(0, 0));
+        $this->assertCount(2, $favouritesrepo->find_all(0, 10));
+        $this->assertCount(1, $favouritesrepo->find_all(1, 0));
+        $this->assertCount(1, $favouritesrepo->find_all(1, 10));
 
         // Save 10 arbitrary favourites to the repo.
         foreach (range(1, 10) as $i) {
@@ -269,19 +269,19 @@ class favourite_repository_testcase extends advanced_testcase {
             $favouritesrepo->add($favourite);
         }
 
-        // Verify we have 10 favourites.
-        $this->assertEquals(10, $favouritesrepo->count());
+        // Verify we have 10 favourites + 2 self-conversations.
+        $this->assertEquals(12, $favouritesrepo->count());
 
-        // Verify we can fetch the first page of 5 records.
-        $favourites = $favouritesrepo->find_all(0, 5);
-        $this->assertCount(5, $favourites);
+        // Verify we can fetch the first page of 5 records+ 2 self-conversations.
+        $favourites = $favouritesrepo->find_all(0, 6);
+        $this->assertCount(6, $favourites);
 
         // Verify we can fetch the second page.
-        $favourites = $favouritesrepo->find_all(5, 5);
-        $this->assertCount(5, $favourites);
+        $favourites = $favouritesrepo->find_all(6, 6);
+        $this->assertCount(6, $favourites);
 
         // Verify the third page request ends with an empty array.
-        $favourites = $favouritesrepo->find_all(10, 5);
+        $favourites = $favouritesrepo->find_all(12, 6);
         $this->assertCount(0, $favourites);
     }
 
@@ -321,11 +321,11 @@ class favourite_repository_testcase extends advanced_testcase {
 
         $favouritesrepo = new favourite_repository($user1context);
 
-        // Verify that for an empty repository, find_all with any combination of page options returns an empty array.
-        $this->assertEquals([], $favouritesrepo->find_by([], 0, 0));
-        $this->assertEquals([], $favouritesrepo->find_by([], 0, 10));
-        $this->assertEquals([], $favouritesrepo->find_by([], 1, 0));
-        $this->assertEquals([], $favouritesrepo->find_by([], 1, 10));
+        // Verify that by default, find_all with any combination of page options returns only self-conversations.
+        $this->assertCount(2, $favouritesrepo->find_by([], 0, 0));
+        $this->assertCount(2, $favouritesrepo->find_by([], 0, 10));
+        $this->assertCount(1, $favouritesrepo->find_by([], 1, 0));
+        $this->assertCount(1, $favouritesrepo->find_by([], 1, 10));
 
         // Save 10 arbitrary favourites to the repo.
         foreach (range(1, 10) as $i) {
@@ -339,12 +339,12 @@ class favourite_repository_testcase extends advanced_testcase {
             $favouritesrepo->add($favourite);
         }
 
-        // Verify we have 10 favourites.
-        $this->assertEquals(10, $favouritesrepo->count());
+        // Verify we have 10 favourites + 2 self-conversations.
+        $this->assertEquals(12, $favouritesrepo->count());
 
-        // Verify a request for a page, when no criteria match, results in an empty array.
+        // Verify a request for a page, when no criteria match, results in 2 self-conversations array.
         $favourites = $favouritesrepo->find_by(['component' => 'core_message'], 0, 5);
-        $this->assertCount(0, $favourites);
+        $this->assertCount(2, $favourites);
 
         // Verify we can fetch a the first page of 5 records.
         $favourites = $favouritesrepo->find_by(['component' => 'core_course'], 0, 5);
@@ -546,8 +546,8 @@ class favourite_repository_testcase extends advanced_testcase {
         $favourite1 = $favouritesrepo->add($favourite);
         $favourite2 = $favouritesrepo->add($favourite2);
 
-        // Verify we have 2 items in the repo.
-        $this->assertEquals(2, $favouritesrepo->count());
+        // Verify we have 2 items in the repo + 2 self-conversations.
+        $this->assertEquals(4, $favouritesrepo->count());
 
         // Try to delete by a non-existent area, and confirm it doesn't remove anything.
         $favouritesrepo->delete_by(
@@ -557,7 +557,7 @@ class favourite_repository_testcase extends advanced_testcase {
                 'itemtype' => 'donaldduck'
             ]
         );
-        $this->assertEquals(2, $favouritesrepo->count());
+        $this->assertEquals(4, $favouritesrepo->count());
 
         // Try to delete by a non-existent area, and confirm it doesn't remove anything.
         $favouritesrepo->delete_by(
@@ -567,7 +567,7 @@ class favourite_repository_testcase extends advanced_testcase {
                 'itemtype' => 'cat'
             ]
         );
-        $this->assertEquals(2, $favouritesrepo->count());
+        $this->assertEquals(4, $favouritesrepo->count());
 
         // Delete by area, and confirm we have one record left, from the 'core_course/anothertype' area.
         $favouritesrepo->delete_by(
@@ -577,7 +577,7 @@ class favourite_repository_testcase extends advanced_testcase {
                 'itemtype' => 'course'
             ]
         );
-        $this->assertEquals(1, $favouritesrepo->count());
+        $this->assertEquals(3, $favouritesrepo->count());
         $this->assertFalse($favouritesrepo->exists($favourite1->id));
         $this->assertTrue($favouritesrepo->exists($favourite2->id));
     }
index 0906e18..9fab93b 100644 (file)
@@ -140,7 +140,13 @@ class filter_mathjaxloader extends moodle_text_filter {
         if ($hasextra) {
             // If custom dilimeters are used, wrap whole text to prevent autolinking.
             $text = '<span class="nolink">' . $text . '</span>';
-        } else {
+        } else if (preg_match('/\\\\[[(]/', $text) || preg_match('/\$\$/', $text)) {
+            // Only parse the text if there are mathjax symbols in it. The recognized
+            // math environments are \[ \] and $$ $$ for display mathematics and \( \)
+            // for inline mathematics.
+            // Note: 2 separate regexes seems to perform better here than using a single
+            // regex with groupings.
+
             // Wrap display and inline math environments in nolink spans.
             // Do not wrap nested environments, i.e., if inline math is nested
             // inside display math, only the outer display math is wrapped in
index 516c5fc..4ed05d1 100644 (file)
@@ -22,7 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['direct:view'] = 'Import grades from CSV';
+$string['direct:view'] = 'Import grades from spreadsheet';
 $string['pluginname'] = 'Paste from spreadsheet';
 $string['privacy:metadata'] = 'The import grades from copied spreadsheed plugin does not store any personal data.';
 $string['userdata'] = 'Help copying data into this form.';
index 6fd01fa..33c8aaf 100644 (file)
@@ -714,12 +714,15 @@ class core_group_privacy_provider_testcase extends provider_testcase {
         $coursecontext1 = context_course::instance($course1->id);
         $coursecontext2 = context_course::instance($course2->id);
 
-        // User1 is member of some groups in course1 and course2.
+        // User1 is member of some groups in course1 and course2 + self-conversation.
         $contextlist = provider::get_contexts_for_userid($user1->id);
-        $this->assertCount(2, $contextlist);
+        $contextids = $contextlist->get_contextids();
+        // First user context is the one related to self-conversation. Let's test group contexts.
+        array_pop($contextids);
+        $this->assertCount(3, $contextlist);
         $this->assertEquals(
                 [$coursecontext1->id, $coursecontext2->id],
-                $contextlist->get_contextids(),
+                $contextids,
                 '', 0.0, 10, true);
     }
 
@@ -757,7 +760,7 @@ class core_group_privacy_provider_testcase extends provider_testcase {
         // User is member of some groups in course1 and course2,
         // but only the membership in course1 is directly managed by core_group.
         $contextlist = provider::get_contexts_for_userid($user->id);
-        $this->assertEquals([$coursecontext1->id], $contextlist->get_contextids());
+        $this->assertEquals($coursecontext1->id, $contextlist->get_contextids()[0]);
     }
 
     /**
index 9eb6d21..6fe3616 100644 (file)
@@ -28,7 +28,7 @@ $string['accesskey'] = 'Access key, {$a}';
 $string['accessstatement'] = 'Accessibility statement';
 $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
-$string['breadcrumb'] = 'Breadcrumb trail';
+$string['breadcrumb'] = 'Navigation bar';
 $string['hideblocka'] = 'Hide {$a} block';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
index 9b43f46..26c8961 100644 (file)
@@ -265,7 +265,7 @@ $string['configiplookup'] = 'When you click on an IP address (such as 34.12.222.
 $string['configkeeptagnamecase'] = 'Check this if you want tag names to keep the original casing as entered by users who created them';
 $string['configlang'] = 'Choose a default language for the whole site. Users can override this setting using the language menu or the setting in their personal profile.';
 $string['configlangstringcache'] = 'Caches all the language strings into compiled files in the data directory.  If you are translating Moodle or changing strings in the Moodle source code then you may want to switch this off.  Otherwise leave it on to see performance benefits.';
-$string['configlanglist'] = 'Leave this blank to allow users to choose from any language you have in this installation of Moodle.  However, you can shorten the language menu by entering a comma-separated list of language codes that you want.  For example:  en,es_es,fr,it';
+$string['configlanglist'] = 'If left blank, all languages installed on the site will be displayed in the language menu. Alternatively, the language menu may be shortened by entering a list of language codes separated by commas e.g. en,de,fr. If desired, a different name for the language than the language pack name may be specified using the format: language code|language name e.g. en_kids|English,de_kids|Deutsch.';
 $string['configlangmenu'] = 'Choose whether or not you want to display the general-purpose language menu on the home page, login page etc.  This does not affect the user\'s ability to set the preferred language in their own profile.';
 $string['configlatinexcelexport'] = 'Choose the encoding for Excel exports.';
 $string['configlocale'] = 'Choose a sitewide locale - this will override the format and language of dates for all language packs (though names of days in calendar are not affected). You need to have this locale data installed on your operating system (eg for linux en_US.UTF-8 or es_ES.UTF-8). In most cases this field should be left blank.';
@@ -1111,7 +1111,7 @@ $string['sessioncookiedomain'] = 'Cookie domain';
 $string['sessioncookiepath'] = 'Cookie path';
 $string['sessionhandling'] = 'Session handling';
 $string['sessiontimeout'] = 'Timeout';
-$string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>';
+$string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>.';
 $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
 $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
 $string['settingsafemode'] = 'Moodle is not fully compatible with safe mode, please ask server administrator to turn it off. Running Moodle under safe mode is not supported, please expect various problems if you do so.';
@@ -1237,7 +1237,7 @@ $string['taskfiletrashcleanup'] = 'Cleanup files in trash';
 $string['taskglobalsearchindex'] = 'Global search indexing';
 $string['taskglobalsearchoptimize'] = 'Global search index optimization';
 $string['taskgradecron'] = 'Background processing for gradebook';
-$string['taskgradehistorycleanup'] = 'Background processing for clean grade history tables';
+$string['taskgradehistorycleanup'] = 'Background processing for cleaning grade history tables';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
 $string['tasklogcleanup'] = 'Cleanup of task logs';
 $string['tasklogs'] = 'Task logs';
@@ -1246,8 +1246,8 @@ $string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts';
 $string['taskplagiarismcron'] = 'Background processing for legacy cron in plagiarism plugins';
 $string['taskportfoliocron'] = 'Background processing for portfolio plugins';
 $string['taskprocessing'] = 'Task processing';
-$string['taskquestioncron'] = 'Background processing for cleaning up the old question previews';
-$string['taskquestionstatscleanupcron'] = 'Background processing for cleaning up the old question statistics cache';
+$string['taskquestioncron'] = 'Background processing for cleaning up question previews';
+$string['taskquestionstatscleanupcron'] = 'Background processing for cleaning up question statistics caches';
 $string['taskrefreshsystemtokens'] = 'Refresh OAuth tokens for service accounts';
 $string['taskregistrationcron'] = 'Site registration';
 $string['tasksendfailedloginnotifications'] = 'Send failed login notifications';
@@ -1258,11 +1258,11 @@ $string['tasktagcron'] = 'Background processing for tags';
 $string['tasktempfilecleanup'] = 'Delete stale temp files';
 $string['tempdatafoldercleanup'] = 'Clean up temporary data files older than';
 $string['testoutgoingmailconf'] = 'Test outgoing mail configuration';
-$string['testoutgoingmaildetail'] = 'Before testing you have to save the configuration.<br />{$a}';
-$string['testoutgoingmailconf_message'] = 'This is a test message. Please disregard. If you received this email, it means that you have successfully configured your Moodle site\'s email settings.';
-$string['testoutgoingmailconf_errorcommunications'] = 'Moodle could not communicate with your mail server. Start by checking your Moodle Outgoing mail configuration.';
+$string['testoutgoingmaildetail'] = 'Note: Before testing, please save your configuration.<br />{$a}';
+$string['testoutgoingmailconf_message'] = 'This is a test message to confirm that you have successfully configured your site\'s outgoing mail.';
+$string['testoutgoingmailconf_errorcommunications'] = 'Your site couldn\'t communicate with your mail server. Please check your outgoing mail configuration.';
 $string['testoutgoingmailconf_sendtest'] = 'Send a test message';
-$string['testoutgoingmailconf_sentmail'] = 'Moodle successfully delivered the test message to the mail server.<br />From: {$a->fromemail}<br />To: {$a->toemail}';
+$string['testoutgoingmailconf_sentmail'] = 'This site has successfully sent a test message to the mail server.<br />From: {$a->fromemail}<br />To: {$a->toemail}';
 $string['testoutgoingmailconf_subject'] = '{$a}: test message';
 $string['testoutgoingmailconf_toemail'] = 'To email address';
 $string['themedesignermode'] = 'Theme designer mode';
index fd830f6..b0f5244 100644 (file)
@@ -39,15 +39,15 @@ $string['erroralreadypredict'] = 'File {$a} has already been used to generate pr
 $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
 $string['errorcannotwritedataset'] = 'Dataset file {$a} cannot be written';
 $string['errorexportmodelresult'] = 'The machine learning model can not be exported.';
-$string['errorimport'] = 'Error importing the provided json file.';
-$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed in your system. To install the same or a newer version of the plugin should be enough in most cases.';
-$string['errorimportversionmismatches'] = 'The version of the following components differ from the version installed in this site: {$a}. You can use "Ignore version mismatches" option to ignore these differences.';
+$string['errorimport'] = 'Error importing the provided JSON file.';
+$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed on your site. Installing the same or a newer version of the plugin should be fine in most cases.';
+$string['errorimportversionmismatches'] = 'The version of the following components differs from the version installed on this site: {$a}. You can use the option \'Ignore version mismatches\' to ignore these differences.';
 $string['errorimportmissingclasses'] = 'The following analytics components are not available in this site: {$a->missingclasses}. ';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
 $string['errorinvalidtarget'] = 'Invalid {$a} target';
-$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the class fully qualified class name.';
+$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the fully qualified class name.';
 $string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
-$string['errornoexportconfigrequirements'] = 'Only non static models with timeplitting methods can be exported.';
+$string['errornoexportconfigrequirements'] = 'Only non-static models with time-splitting methods can be exported.';
 $string['errornoindicators'] = 'This model does not have any indicators.';
 $string['errornopredictresults'] = 'No results returned from the predictions processor. Check the output directory contents for more information.';
 $string['errornotimesplittings'] = 'This model does not have any time-splitting method.';
@@ -123,7 +123,7 @@ $string['privacy:metadata:analytics:predictions'] = 'Predictions';
 $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:rangeindex'] = 'The index of the time splitting method';
+$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';
 $string['privacy:metadata:analytics:predictions:calculations'] = 'Indicator calculations';
index c518dab..de681c6 100644 (file)
 
 $string['asyncbackupcomplete'] = 'The backup process has completed';
 $string['asyncbackupcompletebutton'] = 'Continue';
-$string['asyncbackupcompletedetail'] = 'The backup process has completed successfully completed. <br/> You can access the backup in the <a href="{$a}">restore page.</a>';
+$string['asyncbackupcompletedetail'] = 'The backup process has completed successfully. <br/> You can access the backup on the <a href="{$a}">restore page.</a>';
 $string['asyncbackuperror'] = 'The backup process has failed';
 $string['asyncbackuperrordetail'] = 'The backup process has failed. Please contact your system administrator.';
 $string['asyncbackuppending'] = 'The backup process is pending';
 $string['asyncbackupprocessing'] = 'The backup is in progress';
-$string['asyncbadexecution'] = 'Bad backup controller execution, is {$a} should be 2';
-$string['asynccheckprogress'] = ' You can check the progress at anytime at the <a href="{$a}">restore page.</a>';
-$string['asyncgeneralsettings'] = 'Asynchronous backup/restore general settings';
+$string['asyncbadexecution'] = 'Bad backup controller execution. It is {$a} and should be 2.';
+$string['asynccheckprogress'] = 'You can check the progress at any time on the <a href="{$a}">restore page</a>.';
+$string['asyncgeneralsettings'] = 'Asynchronous backup/restore';
 $string['asyncemailenable'] = 'Enable message notifications';
-$string['asyncemailenabledetail'] = 'When enabled users will receive a message when an asynchronous restore/backup completes';
+$string['asyncemailenabledetail'] = 'If enabled, users will receive a message when an asynchronous backup or restore completes.';
 $string['asyncmessagebody'] = 'Message';
-$string['asyncmessagebodydetail'] = 'Message to send when an asynchronous restore/backup completes';
-$string['asyncmessagebodydefault'] = 'Dear {user_firstname} {user_lastname}, <br/> Your {operation} (ID: {backupid}) has completed successfully! <br/><br/>You can view it here {link}.<br/>Kind Regards,<br/>Your Moodle Administrator.';
+$string['asyncmessagebodydetail'] = 'Message to send when an asynchronous backup or restore completes.';
+$string['asyncmessagebodydefault'] = 'Hi {user_firstname},<br/> Your {operation} (ID: {backupid}) has completed successfully. <br/><br/>You can access it here: {link}.';
 $string['asyncmessagesubject'] = 'Subject';
 $string['asyncmessagesubjectdetail'] = 'Message subject';
 $string['asyncmessagesubjectdefault'] = 'Moodle {operation} completed successfully';
-$string['asyncnowait'] = 'You don\'t need to wait here, the process will continue in the background.';
+$string['asyncnowait'] = 'You don\'t need to wait here, as the process will continue in the background.';
 $string['asyncprocesspending'] = 'Process pending';
 $string['asyncrestorecomplete'] = 'The restore process has completed';
 $string['asyncrestorecompletebutton'] = 'Continue';
-$string['asyncrestorecompletedetail'] = 'The restore process has completed successfully completed. Clicking continue will take you to the <a href="{$a}">course for the restored item.</a>';
+$string['asyncrestorecompletedetail'] = 'The restore process has completed successfully. Clicking continue will take you to the <a href="{$a}">course for the restored item.</a>';
 $string['asyncrestoreerror'] = 'The restore process has failed';
 $string['asyncrestoreerrordetail'] = 'The restore process has failed. Please contact your system administrator.';
 $string['asyncrestorepending'] = 'The restore process is pending';
@@ -169,7 +169,7 @@ $string['currentstage4'] = 'Confirmation and review';
 $string['currentstage8'] = 'Perform backup';
 $string['currentstage16'] = 'Complete';
 $string['enableasyncbackup'] = 'Enable asynchronous backups';
-$string['enableasyncbackup_help'] = 'If enabled, all backup and restore operations will be done asynchronously. This does not effect imports and exports. Asynchronous backups and restores allow users to do other operations while a backup or restore is in progress.';
+$string['enableasyncbackup_help'] = 'If enabled, all backup and restore operations will be done asynchronously. This does not affect imports and exports. Asynchronous backups and restores allow users to do other operations while a backup or restore is in progress.';
 $string['enterasearch'] = 'Enter a search';
 $string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->bid}) for course module (id: {$a->mid}) found. This block will not be backed up';
 $string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
@@ -258,7 +258,7 @@ $string['nomatchingcourses'] = 'There are no courses to display';
 $string['norestoreoptions'] = 'There are no categories or existing courses you can restore to.';
 $string['originalwwwroot'] = 'URL of backup';
 $string['overwrite'] = 'Overwrite';
-$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Muliple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
+$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Multiple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
 $string['pendingasyncdeletedetail'] = 'This course has an asynchronous backup pending. <br/> Courses can\'t be deleted until this backup finishes.';
 $string['pendingasyncedit'] = 'There is a pending asynchronous backup for this course. Please do not edit this course until backup is complete.';
 $string['pendingasyncerror'] = 'Backup pending for this resource';
@@ -374,4 +374,4 @@ $string['unnamedsection'] = 'Unnamed section';
 $string['userinfo'] = 'Userinfo';
 $string['module'] = 'Module';
 $string['morecoursesearchresults'] = 'More than {$a} courses found, showing first {$a} results';
-$string['recyclebin_desc'] = 'These settings will be also applied to recycle bin';
+$string['recyclebin_desc'] = 'Note that these settings will be also be used for the recycle bin.';
index 8e1c503..7ee2620 100644 (file)
@@ -245,7 +245,7 @@ $string['criteria_7_help'] = 'Allows a badge to be awarded to users based on the
 $string['criteria_8'] = 'Cohort membership';
 $string['criteria_8_help'] = 'Allows a badge to be awarded to users based on cohort membership.';
 $string['criteria_9'] = 'Competencies';
-$string['criteria_9_help'] = 'Allows a badge to be awarded to users based on the competencies thay have completed.';
+$string['criteria_9_help'] = 'Allows a badge to be awarded to users based on the competencies they have completed.';
 $string['criterror'] = 'Current parameters issues';
 $string['criterror_help'] = 'This fieldset shows all parameters that were initially added to this badge requirement but are no longer available. It is recommended that you un-check such parameters to make sure that users can earn this badge in the future.';
 $string['currentimage'] = 'Current image';
index 441b191..73acbeb 100644 (file)
@@ -65,6 +65,7 @@ $string['cachedef_observers'] = 'Event observers';
 $string['cachedef_plugin_functions'] = 'Plugins available callbacks';
 $string['cachedef_plugin_manager'] = 'Plugin info manager';
 $string['cachedef_presignup'] = 'Pre sign-up data for particular unregistered user';
+$string['cachedef_portfolio_add_button_portfolio_instances'] = 'Portfolio instances for portfolio_add_button class';
 $string['cachedef_postprocessedcss'] = 'Post processed CSS';
 $string['cachedef_tagindexbuilder'] = 'Search results for tagged items';
 $string['cachedef_questiondata'] = 'Question definitions';
index d7930bb..d33e396 100644 (file)
@@ -38,7 +38,7 @@ $string['customfield_visibility'] = 'Visible to';
 $string['customfield_visibility_help'] = 'This setting determines who can view the custom field name and value in the list of courses.';
 $string['customfield_visibletoall'] = 'Everyone';
 $string['customfield_visibletoteachers'] = 'Teachers';
-$string['customfieldsettings'] = 'Settings for course custom fields';
+$string['customfieldsettings'] = 'Common course custom fields settings';
 $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
 $string['favourite'] = 'Starred course';
 $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).';
@@ -58,8 +58,8 @@ $string['target:coursecompetencies'] = 'Students at risk of not achieving the co
 $string['target:coursecompetencies_help'] = 'This target describes whether a student is at risk of not achieving the competencies assigned to a course. This target considers that all competencies assigned to the course must be achieved by the end of the course.';
 $string['target:coursedropout'] = 'Students at risk of dropping out';
 $string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.';
-$string['target:coursegradetopass'] = 'Students at risk of not getting the minimum grade to pass the course.';
-$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not getting the minimum grade to pass the course.';
+$string['target:coursegradetopass'] = 'Students at risk of not achieving the minimum grade to pass the course';
+$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not achieving the minimum grade to pass the course.';
 $string['target:noteachingactivity'] = 'No teaching';
 $string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.';
 $string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions';
@@ -70,5 +70,5 @@ $string['targetlabelstudentdropoutyes'] = 'Student at risk of dropping out';
 $string['targetlabelstudentdropoutno'] = 'Not at risk';
 $string['targetlabelstudentgradetopassno'] = 'Student who is likely to meet the minimum grade to pass the course.';
 $string['targetlabelstudentgradetopassyes'] = 'Student at risk of not meeting the minimum grade to pass the course.';
-$string['targetlabelteachingyes'] = 'Users with teaching capabilities have access to the course';
+$string['targetlabelteachingyes'] = 'Users with teaching capabilities who have access to the course';
 $string['targetlabelteachingno'] = 'No teaching';
index a916c31..698680d 100644 (file)
@@ -28,7 +28,7 @@ $string['addnewcategory'] = 'Add a new category';
 $string['afterfield'] = 'After field {$a}';
 $string['categorynotfound'] = 'Category not found';
 $string['checked'] = 'Checked';
-$string['commonsettings'] = 'Common settings';
+$string['commonsettings'] = 'General';
 $string['componentsettings'] = 'Component settings';
 $string['confirmdeletecategory'] = 'Are you sure you want to delete this category? All fields inside the category will also be deleted and all data associated with them. This action cannot be undone.';
 $string['confirmdeletefield'] = 'Are you sure you want to delete this field and all associated data? This action cannot be undone.';
index 8c18ed7..5451eda 100644 (file)
@@ -157,7 +157,7 @@ $string['privacy:metadata:messages:useridfrom'] = 'The ID of the user who sent t
 $string['privacy:metadata:messages:smallmessage'] = 'A small version of the message';
 $string['privacy:metadata:messages:subject'] = 'The subject of the message';
 $string['privacy:metadata:messages:timecreated'] = 'The time when the message was created';
-$string['privacy:metadata:messages:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender image (user or group).';
+$string['privacy:metadata:messages:customdata'] = 'Custom data, usually containing internal IDs and a public URL of the sender image (user or group)';
 $string['privacy:metadata:message_contacts'] = 'The list of contacts';
 $string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
 $string['privacy:metadata:message_contacts:timecreated'] = 'The time when the contact was created';
@@ -198,7 +198,7 @@ $string['privacy:metadata:notifications:timeread'] = 'The time when the notifica
 $string['privacy:metadata:notifications:timecreated'] = 'The time when the notification was created';
 $string['privacy:metadata:notifications:useridfrom'] = 'The ID of the user who sent the notification';
 $string['privacy:metadata:notifications:useridto'] = 'The ID of the user who received the notification';
-$string['privacy:metadata:notifications:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender picture (if any).';
+$string['privacy:metadata:notifications:customdata'] = 'Custom data, usually containing internal IDs and a public URL of the sender picture (if any)';
 $string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging';
 $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
 $string['privacy:export:conversationprefix'] = 'Conversation: ';
index 1635df1..df841d6 100644 (file)
@@ -1807,7 +1807,7 @@ $string['separateandconnected'] = 'Separate and Connected ways of knowing';
 $string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
 $string['servererror'] = 'An error occurred whilst communicating with the server';
 $string['serverlocaltime'] = 'Server\'s local time';
-$string['sessionforceclean'] = 'As a security precaution, user generated scripts have been disabled within this session';
+$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session.';
 $string['setcategorytheme'] = 'Set category theme';
 $string['setpassword'] = 'Set password';
 $string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
index 6d73f4a..b9233ef 100644 (file)
@@ -72,6 +72,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'table_row' => 'table_row',
         'xpath_element' => 'xpath_element',
         'form_row' => 'form_row',
+        'group_message_header' => 'group_message_header',
     );
 
     /**
@@ -93,6 +94,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'group_message_header' => 'group_message_header',
         'group_message_member' => 'group_message_member',
         'group_message_tab' => 'group_message_tab',
+        'group_message_list_area' => 'group_message_list_area',
         'icon' => 'icon',
         'link' => 'link',
         'link_or_button' => 'link_or_button',
@@ -159,7 +161,7 @@ XPATH
             .//*[@data-region='message-drawer' and contains(., %locator%)]//div[@data-region='content-message-container']
 XPATH
     , 'group_message_header' => <<<XPATH
-        .//*[@data-region='message-drawer']//div[@data-region='header-container']//*[text()[contains(., %locator%)]]
+        .//*[@data-region='message-drawer']//div[@data-region='header-container' and contains(., %locator%)]
 XPATH
     , 'group_message_member' => <<<XPATH
         .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
@@ -169,6 +171,9 @@ XPATH
 XPATH
     , 'group_message_tab' => <<<XPATH
         .//*[@data-region='message-drawer']//button[@data-toggle='collapse' and contains(string(), %locator%)]
+XPATH
+    , 'group_message_list_area' => <<<XPATH
+        .//*[@data-region='message-drawer']//*[contains(@data-region, concat('view-overview-', %locator%))]
 XPATH
         , 'icon' => <<<XPATH
 .//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
index d2a8672..9865782 100644 (file)
@@ -82,7 +82,7 @@ class registration {
         global $DB;
 
         if (self::$registration === null) {
-            self::$registration = $DB->get_record('registration_hubs', ['huburl' => HUB_MOODLEORGHUBURL]);
+            self::$registration = $DB->get_record('registration_hubs', ['huburl' => HUB_MOODLEORGHUBURL]) ?: null;
         }
 
         if (self::$registration && (bool)self::$registration->confirmed == (bool)$confirmed) {
@@ -578,4 +578,4 @@ class registration {
             redirect(new moodle_url('/admin/registration/index.php', ['returnurl' => $returnurl->out_as_local_url(false)]));
         }
     }
-}
\ No newline at end of file
+}
index aa138d6..7d7e7c1 100644 (file)
@@ -349,6 +349,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/dock_to_block' => 'fa-chevron-left',
             'core:t/download' => 'fa-download',
             'core:t/down' => 'fa-arrow-down',
+            'core:t/downlong' => 'fa-long-arrow-down',
             'core:t/dropdown' => 'fa-cog',
             'core:t/editinline' => 'fa-pencil',
             'core:t/edit_menu' => 'fa-cog',
@@ -402,6 +403,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/unlocked' => 'fa-unlock-alt',
             'core:t/unlock' => 'fa-lock',
             'core:t/up' => 'fa-arrow-up',
+            'core:t/uplong' => 'fa-long-arrow-up',
             'core:t/user' => 'fa-user',
             'core:t/viewdetails' => 'fa-list',
         ];
index 4d6666f..d34c09e 100644 (file)
@@ -45,6 +45,8 @@ class core_string_manager_standard implements core_string_manager {
     protected $countgetstring = 0;
     /** @var bool use disk cache */
     protected $translist;
+    /** @var array language aliases to use in the language selector */
+    protected $transaliases = [];
     /** @var cache stores list of available translations */
     protected $menucache;
     /** @var array list of cached deprecated strings */
@@ -56,12 +58,14 @@ class core_string_manager_standard implements core_string_manager {
      * @param string $otherroot location of downloaded lang packs - usually $CFG->dataroot/lang
      * @param string $localroot usually the same as $otherroot
      * @param array $translist limit list of visible translations
+     * @param array $transaliases aliases to use for the languages in the language selector
      */
-    public function __construct($otherroot, $localroot, $translist) {
+    public function __construct($otherroot, $localroot, $translist, $transaliases = []) {
         $this->otherroot    = $otherroot;
         $this->localroot    = $localroot;
         if ($translist) {
             $this->translist = array_combine($translist, $translist);
+            $this->transaliases = $transaliases;
         } else {
             $this->translist = array();
         }
@@ -521,8 +525,8 @@ class core_string_manager_standard implements core_string_manager {
             }
             // Return only enabled translations.
             foreach ($cachedlist as $langcode => $langname) {
-                if (isset($this->translist[$langcode])) {
-                    $languages[$langcode] = $langname;
+                if (array_key_exists($langcode, $this->translist)) {
+                    $languages[$langcode] = !empty($this->transaliases[$langcode]) ? $this->transaliases[$langcode] : $langname;
                 }
             }
             return $languages;
@@ -572,7 +576,7 @@ class core_string_manager_standard implements core_string_manager {
         $languages = array();
         foreach ($cachedlist as $langcode => $langname) {
             if (isset($this->translist[$langcode])) {
-                $languages[$langcode] = $langname;
+                $languages[$langcode] = !empty($this->transaliases[$langcode]) ? $this->transaliases[$langcode] : $langname;
             }
         }
 
index 54e71c8..6e591a5 100644 (file)
@@ -53,14 +53,15 @@ class badges_cron_task extends scheduled_task {
             if (empty($CFG->badges_allowcoursebadges)) {
                 $coursesql = '';
             } else {
-                $coursesql = ' OR EXISTS (SELECT id FROM {course} WHERE visible = :visible AND startdate < :current) ';
+                $coursesql = ' OR EXISTS (SELECT c.id FROM {course} c WHERE c.visible = :visible AND c.startdate < :current'
+                        . '     AND c.id = b.courseid) ';
                 $courseparams = array('visible' => true, 'current' => time());
             }
 
-            $sql = 'SELECT id
-                      FROM {badge}
-                     WHERE (status = :active OR status = :activelocked)
-                       AND (type = :site ' . $coursesql . ')';
+            $sql = 'SELECT b.id
+                      FROM {badge} b
+                     WHERE (b.status = :active OR b.status = :activelocked)
+                       AND (b.type = :site ' . $coursesql . ')';
             $badgeparams = [
                 'active' => BADGE_STATUS_ACTIVE,
                 'activelocked' => BADGE_STATUS_ACTIVE_LOCKED,
index 6f7e123..7af752e 100644 (file)
@@ -145,7 +145,7 @@ class send_failed_login_notifications_task extends scheduled_task {
             $a->time = userdate($log->timecreated);
             if (empty($log->username)) {
                 // Entries with no valid username. We get attempted username from the event's other field.
-                $other = unserialize($log->other);
+                $other = \tool_log\helper\reader::decode_other($log->other);
                 $a->info = empty($other['username']) ? '' : $other['username'];
                 $a->name = get_string('unknownuser');
             } else {
index 71ebe33..7247f54 100644 (file)
@@ -391,4 +391,13 @@ $definitions = array(
         'simplekeys' => true,
         'simpledata' => true,
     ),
+
+    // Cache the list of portfolio instances for the logged in user
+    // in the portfolio_add_button constructor to avoid loading the
+    // same data multiple times.
+    'portfolio_add_button_portfolio_instances' => [
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
+        'staticacceleration' => true
+    ],
 );
index 744561c..b3e24ea 100644 (file)
@@ -3301,5 +3301,21 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019042700.01);
     }
 
+    if ($oldversion < 2019050300.01) {
+        // Delete all stale favourite records which were left behind when a course was deleted.
+        $select = 'id IN (
+            SELECT fav.id as id
+              FROM {favourite} fav
+         LEFT JOIN {context} ctx ON (ctx.id = fav.contextid)
+             WHERE fav.component = :component
+               AND fav.itemtype = :itemtype
+               AND ctx.id IS NULL
+               )';
+        $params = ['component' => 'core_message', 'itemtype' => 'message_conversations'];
+        $DB->delete_records_select('favourite', $select, $params);
+
+        upgrade_main_savepoint(true, 2019050300.01);
+    }
+
     return true;
 }
index 8be6243..50d0860 100644 (file)
@@ -36,7 +36,7 @@ function atto_emoticon_strings_for_js() {
 
     // Load the strings required by the emotes.
     $manager = get_emoticon_manager();
-    foreach ($manager->get_emoticons() as $emote) {
+    foreach ($manager->get_emoticons(true) as $emote) {
         $PAGE->requires->string_for_js($emote->altidentifier, $emote->altcomponent);
     }
 }
@@ -49,6 +49,6 @@ function atto_emoticon_strings_for_js() {
 function atto_emoticon_params_for_js($elementid, $options, $fpoptions) {
     $manager = get_emoticon_manager();
     return array(
-        'emoticons' => $manager->get_emoticons()
+        'emoticons' => $manager->get_emoticons(true)
     );
 }
index ee8e4d9..e5eb76c 100644 (file)
@@ -68,9 +68,11 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
         });
         Y.use('event', 'moodle-core-event', function(Y) {
             var form = Y.one(document.getElementById(editor.id)).ancestor('form');
-            form.on(M.core.event.FORM_SUBMIT_AJAX, function() {
-                editor.save();
-            }, this)
+            if (form) {
+                form.on(M.core.event.FORM_SUBMIT_AJAX, function() {
+                    editor.save();
+                }, this);
+            }
         });
     };
 
index 5987a0e..619dcc5 100644 (file)
@@ -51,7 +51,7 @@ header('X-UA-Compatible: IE=edge');
     <table border="0" align="center" style="width:100%;">
 <?php
 
-$emoticons = $emoticonmanager->get_emoticons();
+$emoticons = $emoticonmanager->get_emoticons(true);
 // This is tricky - we must somehow include the information about the original
 // emoticon text so that we can replace the image back with it on editor save.
 // so we are going to encode the index of the emoticon. this will break when the
index 047196a..3cb9b0f 100644 (file)
@@ -52,7 +52,7 @@ class tinymce_moodleemoticon extends editor_tinymce_plugin {
 
         // Extra params specifically for emoticon plugin.
         $manager = get_emoticon_manager();
-        $emoticons = $manager->get_emoticons();
+        $emoticons = $manager->get_emoticons(true);
         $imgs = array();
         // See the TinyMCE plugin moodleemoticon for how the emoticon index is (ab)used.
         $index = 0;
index aebf1c0..b0465fc 100644 (file)
@@ -61,7 +61,8 @@ class MoodleQuickForm_cancel extends MoodleQuickForm_submit
             $value=get_string('cancel');
         }
         parent::__construct($elementName, $value, $attributes);
-        $this->updateAttributes(array('onclick'=>'skipClientValidation = true; return true;'));
+        $this->updateAttributes(array('data-skip-validation' => 1, 'data-cancel' => 1,
+            'onclick' => 'skipClientValidation = true; return true;'));
 
         // Add the class btn-cancel.
         $class = $this->getAttribute('class');
@@ -93,7 +94,7 @@ class MoodleQuickForm_cancel extends MoodleQuickForm_submit
     {
         switch ($event) {
             case 'createElement':
-                static::__construct($arg[0], $arg[1], $arg[2]);
+                parent::onQuickFormEvent($event, $arg, $caller);
                 $caller->_registerCancelButton($this->getName());
                 return true;
                 break;
index b75d7d8..f651398 100644 (file)
@@ -64,8 +64,7 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
      *
      * @param string $elementname Element name
      * @param mixed $elementlabel Label(s) for an element
-     * @param array $options Options to control the element's display
-     *                       Valid options are:
+     * @param mixed $attributes Array of typical HTML attributes plus additional options, such as:
      *                       'multiple' - boolean multi select
      *                       'exclude' - array or int, list of course ids to never show
      *                       'requiredcapabilities' - array of capabilities. Uses ANY to combine them.
@@ -73,46 +72,44 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
      *                       'includefrontpage' - boolean Enables the frontpage to be selected.
      *                       'onlywithcompletion' - only courses where completion is enabled
      */
-    public function __construct($elementname = null, $elementlabel = null, $options = array()) {
-        if (isset($options['multiple'])) {
-            $this->multiple = $options['multiple'];
+    public function __construct($elementname = null, $elementlabel = null, $attributes = array()) {
+        if (!is_array($attributes)) {
+            $attributes = [];
         }
-        if (isset($options['exclude'])) {
-            $this->exclude = $options['exclude'];
+        if (isset($attributes['multiple'])) {
+            $this->multiple = $attributes['multiple'];
+        }
+        if (isset($attributes['exclude'])) {
+            $this->exclude = $attributes['exclude'];
             if (!is_array($this->exclude)) {
                 $this->exclude = array($this->exclude);
             }
+            unset($attributes['exclude']);
         }
-        if (isset($options['requiredcapabilities'])) {
-            $this->requiredcapabilities = $options['requiredcapabilities'];
+        if (isset($attributes['requiredcapabilities'])) {
+            $this->requiredcapabilities = $attributes['requiredcapabilities'];
+            unset($attributes['requiredcapabilities']);
         }
-        if (isset($options['limittoenrolled'])) {
-            $this->limittoenrolled = $options['limittoenrolled'];
+        if (isset($attributes['limittoenrolled'])) {
+            $this->limittoenrolled = $attributes['limittoenrolled'];
+            unset($attributes['limittoenrolled']);
         }
 
-        $validattributes = array(
+        $attributes += array(
             'ajax' => 'core/form-course-selector',
             'data-requiredcapabilities' => implode(',', $this->requiredcapabilities),
             'data-exclude' => implode(',', $this->exclude),
             'data-limittoenrolled' => (int)$this->limittoenrolled
         );
-        if ($this->multiple) {
-            $validattributes['multiple'] = 'multiple';
-        }
-        if (isset($options['noselectionstring'])) {
-            $validattributes['noselectionstring'] = $options['noselectionstring'];
-        }
-        if (isset($options['placeholder'])) {
-            $validattributes['placeholder'] = $options['placeholder'];
-        }
-        if (!empty($options['includefrontpage'])) {
-            $validattributes['data-includefrontpage'] = SITEID;
+        if (!empty($attributes['includefrontpage'])) {
+            $attributes['data-includefrontpage'] = SITEID;
+            unset($attributes['includefrontpage']);
         }
         if (!empty($options['onlywithcompletion'])) {
             $validattributes['data-onlywithcompletion'] = 1;
         }
 
-        parent::__construct($elementname, $elementlabel, array(), $validattributes);
+        parent::__construct($elementname, $elementlabel, array(), $attributes);
     }
 
     /**
index af191a0..62670e5 100644 (file)
@@ -296,9 +296,7 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         $output = $PAGE->get_renderer('core', 'files');
         $html .= $output->render($fm);
 
-        $html .= html_writer::empty_tag('input', array('value' => $draftitemid, 'name' => $elname, 'type' => 'hidden'));
-        // label element needs 'for' attribute work
-        $html .= html_writer::empty_tag('input', array('value' => '', 'id' => 'id_'.$elname, 'type' => 'hidden'));
+        $html .= html_writer::empty_tag('input', array('value' => $draftitemid, 'name' => $elname, 'type' => 'hidden', 'id' => $id));
 
         if (!empty($options->accepted_types) && $options->accepted_types != '*') {
             $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
index d13a9cd..fa48a7a 100644 (file)
@@ -8,10 +8,10 @@ M.form_filepicker.callback = function(params) {
     html += '<div class="dndupload-progressbars"></div>';
     M.form_filepicker.Y.one('#file_info_'+params['client_id'] + ' .filepicker-filename').setContent(html);
     //When file is added then set status of global variable to true
-    var elementname = M.core_filepicker.instances[params['client_id']].options.elementname;
-    M.form_filepicker.instances[elementname].fileadded = true;
+    var elementid = M.core_filepicker.instances[params['client_id']].options.elementid;
+    M.form_filepicker.instances[elementid].fileadded = true;
     //generate event to indicate changes which will be used by disable if or validation code
-    M.form_filepicker.Y.one('#id_'+elementname).simulate('change');
+    M.form_filepicker.Y.one('#'+elementid).simulate('change');
 };
 
 /**
@@ -22,8 +22,8 @@ M.form_filepicker.init = function(Y, options) {
     M.form_filepicker.Y = Y;
 
     //For client side validation, initialize file status for this filepicker
-    M.form_filepicker.instances[options.elementname] = {};
-    M.form_filepicker.instances[options.elementname].fileadded = false;
+    M.form_filepicker.instances[options.elementid] = {};
+    M.form_filepicker.instances[options.elementid].fileadded = false;
 
     //Set filepicker callback
     options.formcallback = M.form_filepicker.callback;
index a376459..35b08fe 100644 (file)
@@ -155,7 +155,7 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
         $args->maxbytes = $this->_options['maxbytes'];
         $args->context = $PAGE->context;
         $args->buttonname = $elname.'choose';
-        $args->elementname = $elname;
+        $args->elementid = $id;
 
         $html = $this->_getTabs();
         $fp = new file_picker($args);
diff --git a/lib/form/float.php b/lib/form/float.php
new file mode 100644 (file)
index 0000000..8f375c2
--- /dev/null
@@ -0,0 +1,187 @@
+<?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/>.
+
+
+/**
+ * Float type form element
+ *
+ * Contains HTML class for a float type element
+ *
+ * @package   core_form
+ * @category  form
+ * @copyright 2019 Shamim Rezaie <shamim@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/form/text.php');
+
+/**
+ * Float type form element.
+ *
+ * This is preferred over the text element when working with float numbers, and takes care of the fact that different languages
+ * may use different symbols as the decimal separator.
+ * Using this element, submitted float numbers will be automatically translated from the localised format into the computer format,
+ * and vice versa when they are being displayed.
+ *
+ * @copyright 2019 Shamim Rezaie <shamim@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class MoodleQuickForm_float extends MoodleQuickForm_text {
+
+    /**
+     * MoodleQuickForm_float constructor.
+     *
+     * @param string $elementName (optional) name of the float field
+     * @param string $elementLabel (optional) float field label
+     * @param string $attributes (optional) Either a typical HTML attribute string or an associative array
+     */
+    public function __construct($elementName = null, $elementLabel = null, $attributes = null) {
+        parent::__construct($elementName, $elementLabel, $attributes);
+        $this->_type = 'float';
+    }
+
+    /**
+     * Called by HTML_QuickForm whenever form event is made on this element.
+     *
+     * @param string $event Name of event
+     * @param mixed $arg event arguments
+     * @param object $caller calling object
+     * @return bool
+     */
+    public function onQuickFormEvent($event, $arg, &$caller) {
+        switch ($event) {
+            case 'updateValue':
+                if ($value = $this->_findValue($caller->_constantValues)) {
+                    $value = $this->format_float($value);
+                }
+                if (null === $value) {
+                    $value = $this->_findValue($caller->_submitValues);
+                    if (null === $value) {
+                        if ($value = $this->_findValue($caller->_defaultValues)) {
+                            $value = $this->format_float($value);
+                        }
+                    }
+                }
+                if (null !== $value) {
+                    parent::setValue($value);
+                }
+                return true;
+            case 'createElement':
+                $caller->setType($arg[0], PARAM_RAW_TRIMMED);
+            default:
+                return parent::onQuickFormEvent($event, $arg, $caller);
+        }
+    }
+
+    /**
+     * Checks that the submitted value is a valid float number.
+     *
+     * @param string $value The localised float number that is submitted.
+     * @return string|null Validation error message or null.
+     */
+    public function validateSubmitValue($value) {
+        if (false === unformat_float($value, true)) {
+            return get_string('err_numeric', 'core_form');
+        }
+    }
+
+    /**
+     * Sets the value of the form element.
+     *
+     * @param string $value Default value of the form element
+     */
+    public function setValue($value) {
+        $value = $this->format_float($value);
+        parent::setValue($value);
+    }
+
+    /**
+     * Returns the value of the form element.
+     *
+     * @return false|float
+     */
+    public function getValue() {
+        $value = parent::getValue();
+        if ($value) {
+            $value = unformat_float($value, true);
+        }
+        return $value;
+    }
+
+    /**
+     * Returns a 'safe' element's value.
+     *
+     * @param  array   $submitValues array of submitted values to search
+     * @param  bool    $assoc whether to return the value as associative array
+     * @return mixed
+     */
+    public function exportValue(&$submitValues, $assoc = false) {
+        $value = $this->_findValue($submitValues);
+        if (null === $value) {
+            $value = $this->getValue();
+        } else if ($value) {
+            $value = unformat_float($value, true);
+        }
+        return $this->_prepareValue($value, $assoc);
+    }
+
+    /**
+     * Used by getFrozenHtml() to pass the element's value if _persistantFreeze is on.
+     *
+     * @return string
+     */
+    public function _getPersistantData() {
+        if (!$this->_persistantFreeze) {
+            return '';
+        } else {
+            $id = $this->getAttribute('id');
+            if (isset($id)) {
+                // Id of persistant input is different then the actual input.
+                $id = array('id' => $id . '_persistant');
+            } else {
+                $id = array();
+            }
+
+            return '<input' . $this->_getAttrString(array(
+                        'type'  => 'hidden',
+                        'name'  => $this->getAttribute('name'),
+                        'value' => $this->getAttribute('value')
+                    ) + $id) . ' />';
+        }
+    }
+
+    /**
+     * Given a float, prints it nicely.
+     * This function reserves the number of decimal places.
+     *
+     * @param float|null $value The float number to format
+     * @return string Localised float
+     */
+    private function format_float($value) {
+        if (is_numeric($value)) {
+            if ($value > 0) {
+                $decimals = strlen($value) - strlen(floor($value)) - 1;
+            } else {
+                $decimals = strlen($value) - strlen(ceil($value)) - 1;
+            }
+            $value = format_float($value, $decimals);
+        }
+        return $value;
+    }
+}
index f4f241f..c8a3935 100644 (file)
@@ -257,4 +257,22 @@ class MoodleQuickForm_group extends HTML_QuickForm_group implements templatable
         }
         $renderer->finishGroup($this);
     }
+
+    /**
+     * Calls the validateSubmitValue function for the containing elements and returns an error string as soon as it finds one.
+     *
+     * @param array $values Values of the containing elements.
+     * @return string|null Validation error message or null.
+     */
+    public function validateSubmitValue($values) {
+        foreach ($this->_elements as $element) {
+            if (method_exists($element, 'validateSubmitValue')) {
+                $value = $values[$element->getName()] ?? null;
+                $result = $element->validateSubmitValue($value);
+                if (!empty($result) && is_string($result)) {
+                    return $result;
+                }
+            }
+        }
+    }
 }
index cd9477c..dc520b7 100644 (file)
@@ -110,19 +110,21 @@ class MoodleQuickForm_listing extends HTML_QuickForm_input {
     function toHtml() {
         global $CFG, $PAGE;
 
+        $this->_generateId();
+        $elementid = $this->getAttribute('id');
         $mainhtml = html_writer::tag('div', $this->items[$this->getValue()]->mainhtml,
-                array('id' => $this->getName().'_items_main', 'class' => 'formlistingmain'));
+                array('id' => $elementid . '_items_main', 'class' => 'formlistingmain'));
 
         // Add the main div containing the selected item (+ the caption: "More items").
         $html = html_writer::tag('div', $mainhtml .
                     html_writer::tag('div', $this->showall,
-                        array('id' => $this->getName().'_items_caption', 'class' => 'formlistingmore')),
-                    array('id'=>$this->getName().'_items', 'class' => 'formlisting hide'));
+                        array('id' => $elementid . '_items_caption', 'class' => 'formlistingmore')),
+                    array('id' => $elementid . '_items', 'class' => 'formlisting hide'));
 
         // Add collapsible region: all the items.
         $itemrows = '';
         $html .= html_writer::tag('div', $itemrows,
-                array('id' => $this->getName().'_items_all', 'class' => 'formlistingall'));
+                array('id' => $elementid . '_items_all', 'class' => 'formlistingall'));
 
         // Add radio buttons for non javascript support.
         $radiobuttons = '';
@@ -139,19 +141,19 @@ class MoodleQuickForm_listing extends HTML_QuickForm_input {
 
         // Container for the hidden hidden input which will contain the selected item.
         $html .= html_writer::tag('div', $radiobuttons,
-                array('id' => 'formlistinginputcontainer_' . $this->getName(), 'class' => 'formlistinginputcontainer'));
+                array('id' => 'formlistinginputcontainer_' . $elementid, 'class' => 'formlistinginputcontainer'));
 
         $module = array('name'=>'form_listing', 'fullpath'=>'/lib/form/yui/listing/listing.js',
             'requires'=>array('node', 'event', 'transition', 'escape'));
 
         $PAGE->requires->js_init_call('M.form_listing.init',
                  array(array(
-                'elementid' => $this->getName().'_items',
+                'elementid' => $elementid.'_items',
                 'hideall' => $this->hideall,
                 'showall' => $this->showall,
                 'hiddeninputid' => $this->getAttribute('id'),
                 'items' => $this->items,
-                'inputname' => $this->getName(),
+                'inputname' => $elementid,
                 'currentvalue' => $this->getValue())), true, $module);
 
         return $html;
index d44b1a2..3942eae 100644 (file)
@@ -112,7 +112,7 @@ class MoodleQuickForm_submit extends HTML_QuickForm_submit implements templatabl
                     $onClick = $this->getAttribute('onclick');
                     $skip = 'skipClientValidation = true;';
                     $onClick = ($onClick !== null)?$skip.' '.$onClick:$skip;
-                    $this->updateAttributes(array('onclick'=>$onClick));
+                    $this->updateAttributes(array('data-skip-validation' => 1, 'data-no-submit' => 1, 'onclick' => $onClick));
                 }
                 return true;
                 break;
index e0b235a..447a822 100644 (file)
@@ -82,6 +82,7 @@ trait templatable_form_element {
         $context['type'] = $this->getType();
         $context['attributes'] = implode(' ', $otherattributes);
         $context['emptylabel'] = ($this->getLabel() === '');
+        $context['iderror'] = preg_replace('/^id_/', 'id_error_', $context['id']);
 
         // Elements with multiple values need array syntax.
         if ($this->getAttribute('multiple')) {
index 94d2fd6..0584acc 100644 (file)
@@ -15,7 +15,7 @@
     {{#element.checked}}checked{{/element.checked}}
     size="{{element.size}}"
     {{#error}}
-        autofocus aria-describedby="id_error_{{element.name}}"
+        autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
     {{#element.frozen}}
         disabled
@@ -27,7 +27,7 @@
     <em>{{{.}}}</em>
 {{/text}}
 {{{helpbutton}}}
-<span class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+<span class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
     {{{error}}}
 </span>
 {{^element.frozen}}
index 8e1a472..e775939 100644 (file)
@@ -25,7 +25,7 @@
                 {{/element.selectedvalue}}
                 id="{{element.id}}" {{#element.checked}}checked{{/element.checked}}
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{#element.frozen}}
                     disabled
@@ -43,7 +43,7 @@
                 {{{helpbutton}}}
             </span>
         </div>
-        <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+        <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
         </div>
     </div>
index 175280e..127fa77 100644 (file)
@@ -8,7 +8,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index 8736f0a..ead064d 100644 (file)
@@ -8,7 +8,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index fff81f9..3806649 100644 (file)
@@ -7,7 +7,7 @@
                 id="{{element.id}}"
                 type="button"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{{element.attributes}}}
                 >
index 3ce46e2..c3e29cd 100644 (file)
@@ -7,7 +7,7 @@
                 id="{{element.id}}"
                 type="button"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{{element.attributes}}}>
                 {{{element.value}}}
index 279affb..994ebc5 100644 (file)
@@ -15,7 +15,7 @@
     {{#element.checked}}checked{{/element.checked}}
     size="{{element.size}}"
     {{#error}}
-        autofocus aria-describedby="id_error_{{element.name}}"
+        autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
     {{#element.frozen}}
         disabled
@@