Merge branch 'MDL-63366-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 13 Mar 2019 23:31:39 +0000 (00:31 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 13 Mar 2019 23:31:39 +0000 (00:31 +0100)
276 files changed:
admin/classes/form/testoutgoingmailconf_form.php [new file with mode: 0644]
admin/message.php
admin/settings/messaging.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/server.php
admin/settings/subsystems.php
admin/settings/top.php
admin/testoutgoingmailconf.php [new file with mode: 0644]
admin/tool/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/output/model_logs.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/evaluation_mode_selection.mustache [new file with mode: 0644]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/lp/tests/externallib_test.php
admin/tool/lpimportcsv/classes/framework_importer.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/replace/lang/en/tool_replace.php
admin/tool/task/lang/en/tool_task.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/xmldb/lang/en/tool_xmldb.php
analytics/classes/classifier.php
analytics/classes/model.php
analytics/classes/regressor.php
analytics/classes/stats.php [new file with mode: 0644]
analytics/tests/prediction_test.php
analytics/tests/stats_test.php [new file with mode: 0644]
analytics/upgrade.txt
auth/db/lang/en/auth_db.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/auth.php
auth/mnet/classes/task/cron_task.php [new file with mode: 0644]
auth/mnet/db/tasks.php [new file with mode: 0644]
auth/mnet/lang/en/auth_mnet.php
auth/mnet/version.php
auth/shibboleth/lang/en/auth_shibboleth.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/nav-sort-selector.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_favourite.feature
blocks/timeline/templates/nav-view-selector.mustache
calendar/lib.php
comment/classes/external.php
comment/lib.php
comment/locallib.php
competency/classes/course_competency.php
competency/tests/external_test.php
course/classes/search/course.php [moved from course/classes/search/mycourse.php with 95% similarity]
course/classes/search/customfield.php
course/externallib.php
course/loginas.php
course/tests/behat/customfields_visibility.feature
course/tests/externallib_test.php
course/tests/search_test.php
course/upgrade.txt
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/select/tests/behat/field.feature
customfield/field/text/lang/en/customfield_text.php
customfield/field/textarea/lang/en/customfield_textarea.php
customfield/tests/behat/edit_fields_settings.feature
enrol/externallib.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/manual/lang/en/enrol_manual.php
enrol/self/lang/en/enrol_self.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
files/renderer.php
grade/edit/tree/category_form.php
grade/edit/tree/item_form.php
group/lib.php
install/lang/hr/admin.php
install/lang/hr/error.php
install/lang/hr/install.php
lang/en/admin.php
lang/en/availability.php
lang/en/completion.php
lang/en/course.php
lang/en/customfield.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/grades.php
lang/en/group.php
lang/en/hub.php
lang/en/message.php
lang/en/moodle.php
lang/en/plugin.php
lang/en/question.php
lang/en/role.php
lang/en/search.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/loadingicon.min.js [new file with mode: 0644]
lib/amd/src/form-autocomplete.js
lib/amd/src/loadingicon.js [new file with mode: 0644]
lib/behat/classes/behat_config_util.php
lib/behat/classes/partial_named_selector.php
lib/classes/hub/registration.php
lib/classes/message/manager.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/task/clean_up_deleted_search_area_task.php [new file with mode: 0644]
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/tests/pgsql_native_recordset_test.php
lib/editor/atto/plugins/media/lang/en/atto_media.php
lib/ltiprovider/readme_moodle.txt
lib/ltiprovider/src/OAuth/OAuthRequest.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/outputrenderers.php
lib/templates/navbar.mustache
lib/tests/accesslib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/securelayout.feature [new file with mode: 0644]
lib/tests/fixtures/securetestpage.php [new file with mode: 0644]
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
login/tests/lib_test.php
message/amd/build/message_drawer_events.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_drawer_view_settings.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_events.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_drawer_view_settings.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/helper.php
message/classes/privacy/provider.php
message/defaultoutputs.php
message/externallib.php
message/renderer.php
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_lazy_load_list.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_overview_section_messages.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/group_conversation.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/view_previous_annotations.feature
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/chat/classes/task/cron_task.php [new file with mode: 0644]
mod/chat/db/tasks.php [new file with mode: 0644]
mod/chat/lang/en/chat.php
mod/chat/lib.php
mod/chat/version.php
mod/choice/lang/en/choice.php
mod/choice/tests/behat/choice_availability.feature
mod/data/classes/privacy/provider.php
mod/data/lang/en/data.php
mod/data/tests/externallib_test.php
mod/feedback/lang/en/feedback.php
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/export_import.feature
mod/feedback/tests/behat/multichoice.feature
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/forum/classes/privacy/provider.php
mod/glossary/classes/privacy/provider.php
mod/lesson/lang/en/lesson.php
mod/lti/lang/en/lti.php
mod/scorm/lang/en/scorm.php
mod/workshop/allocation/manual/lang/en/workshopallocation_manual.php
mod/workshop/allocation/random/lang/en/workshopallocation_random.php
mod/workshop/allocation/scheduled/classes/task/cron_task.php [new file with mode: 0644]
mod/workshop/allocation/scheduled/db/tasks.php [new file with mode: 0644]
mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php
mod/workshop/allocation/scheduled/lib.php
mod/workshop/allocation/scheduled/version.php
mod/workshop/classes/task/cron_task.php [new file with mode: 0644]
mod/workshop/classes/task/legacy_workshop_allocation_cron.php [new file with mode: 0644]
mod/workshop/db/tasks.php [new file with mode: 0644]
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/upgrade.txt
mod/workshop/version.php
pix/i/muted.png [new file with mode: 0755]
pix/i/muted.svg [new file with mode: 0755]
pix/t/sort_by.png [new file with mode: 0644]
pix/t/sort_by.svg [new file with mode: 0644]
question/format/gift/lang/en/qformat_gift.php
question/format/multianswer/format.php
question/format/multianswer/tests/fixtures/broken_multianswer_1.txt [new file with mode: 0644]
question/format/multianswer/tests/fixtures/broken_multianswer_2.txt [new file with mode: 0644]
question/format/multianswer/tests/fixtures/broken_multianswer_3.txt [new file with mode: 0644]
question/format/multianswer/tests/fixtures/broken_multianswer_4.txt [new file with mode: 0644]
question/format/multianswer/tests/multianswerformat_test.php
question/format/webct/lang/en/qformat_webct.php
question/format/xml/format.php
question/format/xml/tests/fixtures/broken_cloze_questions.xml [new file with mode: 0644]
question/format/xml/tests/qformat_xml_import_export_test.php
question/type/gapselect/lang/en/qtype_gapselect.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/questiontype.php
question/type/multianswer/version.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/form_test.php [deleted file]
question/type/numerical/tests/questiontype_test.php
question/upgrade.txt
rating/classes/privacy/provider.php
rating/tests/privacy_provider_test.php
repository/dropbox/classes/task/cron_task.php [new file with mode: 0644]
repository/dropbox/db/tasks.php [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/dropbox/lib.php
repository/dropbox/version.php
repository/filesystem/classes/task/cron_task.php [new file with mode: 0644]
repository/filesystem/db/tasks.php [new file with mode: 0644]
repository/filesystem/lang/en/repository_filesystem.php
repository/filesystem/lib.php
repository/filesystem/version.php
search/classes/base.php
search/classes/manager.php
search/classes/output/form/search.php
search/engine/solr/tests/engine_test.php
search/index.php
search/tests/base_test.php
search/tests/fixtures/testable_core_search.php
search/tests/manager_test.php
search/upgrade.txt
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/boost/templates/navbar-secure.mustache
theme/boost/tests/behat/group_conversation.feature [new file with mode: 0644]
theme/bootstrapbase/lang/en/theme_bootstrapbase.php
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
theme/bootstrapbase/templates/core_message/message_drawer_lazy_load_list.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache
theme/clean/lang/en/theme_clean.php
user/externallib.php
user/lib.php
user/profile/field/menu/lang/en/profilefield_menu.php
user/tests/externallib_test.php
version.php
webservice/externallib.php
webservice/rest/locallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

diff --git a/admin/classes/form/testoutgoingmailconf_form.php b/admin/classes/form/testoutgoingmailconf_form.php
new file mode 100644 (file)
index 0000000..fbf8ac4
--- /dev/null
@@ -0,0 +1,59 @@
+<?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 outgoing mail configuration form
+ *
+ * @package    core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Test mail form
+ *
+ * @package    core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testoutgoingmailconf_form extends \moodleform {
+    /**
+     * Add elements to form
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        // Recipient.
+        $options = ['maxlength' => '100', 'size' => '25'];
+        $mform->addElement('text', 'recipient', get_string('testoutgoingmailconf_toemail', 'admin'), $options);
+        $mform->setType('recipient', PARAM_EMAIL);
+        $mform->addRule('recipient', get_string('required'), 'required');
+
+        $buttonarray = array();
+        $buttonarray[] = $mform->createElement('submit', 'send', get_string('testoutgoingmailconf_sendtest', 'admin'));
+        $buttonarray[] = $mform->createElement('cancel');
+
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+
+    }
+}
index c10034a..9a43f61 100644 (file)
@@ -25,47 +25,101 @@ require_once(__DIR__ . '/../config.php');
 require_once($CFG->dirroot . '/message/lib.php');
 require_once($CFG->libdir.'/adminlib.php');
 
-// This is an admin page
+// This is an admin page.
 admin_externalpage_setup('managemessageoutputs');
 
-// Get the submitted params
-$disable    = optional_param('disable', 0, PARAM_INT);
-$enable     = optional_param('enable', 0, PARAM_INT);
+// Fetch processors.
+$allprocessors = get_message_processors();
+$processors = array_filter($allprocessors, function($processor) {
+    return $processor->enabled;
+});
+// Fetch message providers.
+$providers = get_message_providers();
+// Fetch the manage message outputs interface.
+$preferences = get_message_output_default_preferences();
 
-$headingtitle = get_string('managemessageoutputs', 'message');
+if (($form = data_submitted()) && confirm_sesskey()) {
+    $preferences = array();
+    // Prepare default message outputs settings.
+    foreach ($providers as $provider) {
+        $componentproviderbase = $provider->component.'_'.$provider->name;
+        $disableprovidersetting = $componentproviderbase.'_disable';
+        $providerdisabled = false;
+        if (!isset($form->$disableprovidersetting)) {
+            $providerdisabled = true;
+            $preferences[$disableprovidersetting] = 1;
+        } else {
+            $preferences[$disableprovidersetting] = 0;
+        }
 
-if (!empty($disable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$disable))) {
-        print_error('outputdoesnotexist', 'message');
+        foreach (array('permitted', 'loggedin', 'loggedoff') as $setting) {
+            $value = null;
+            $componentprovidersetting = $componentproviderbase.'_'.$setting;
+            if ($setting == 'permitted') {
+                // If we deal with permitted select element, we need to create individual
+                // setting for each possible processor. Note that this block will
+                // always be processed first after entring parental foreach iteration
+                // so we can change form values on this stage.
+                foreach ($allprocessors as $processor) {
+                    $value = '';
+                    if (isset($form->{$componentprovidersetting}[$processor->name])) {
+                        $value = $form->{$componentprovidersetting}[$processor->name];
+                    }
+                    // Ensure that loggedin loggedoff options are set correctly for this permission.
+                    if (($value == 'disallowed') || $providerdisabled) {
+                        // It might be better to unset them, but I can't figure out why that cause error.
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 0;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 0;
+                    } else if ($value == 'forced') {
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 1;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 1;
+                    }
+                    // Record the site preference.
+                    $preferences[$processor->name.'_provider_'.$componentprovidersetting] = $value;
+                }
+            } else if (array_key_exists($componentprovidersetting, $form)) {
+                // We must be processing loggedin or loggedoff checkboxes. Store
+                // defained comma-separated processors as setting value.
+                // Using array_filter eliminates elements set to 0 above.
+                $value = join(',', array_keys(array_filter($form->{$componentprovidersetting})));
+                if (empty($value)) {
+                    $value = null;
+                }
+            }
+            if ($setting != 'permitted') {
+                // We have already recoded site preferences for 'permitted' type.
+                $preferences['message_provider_'.$componentprovidersetting] = $value;
+            }
+        }
+    }
+
+    // Update database.
+    $transaction = $DB->start_delegated_transaction();
+
+    // Save processors enabled/disabled status.
+    foreach ($allprocessors as $processor) {
+        $enabled = isset($form->{$processor->name});
+        \core_message\api::update_processor_status($processor, $enabled);
     }
-    \core_message\api::update_processor_status($processor, 0);     // Disable output.
-    core_plugin_manager::reset_caches();
-}
 
-if (!empty($enable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$enable))) {
-        print_error('outputdoesnotexist', 'message');
+    foreach ($preferences as $name => $value) {
+        set_config($name, $value, 'message');
     }
-    \core_message\api::update_processor_status($processor, 1);      // Enable output.
+    $transaction->allow_commit();
+
     core_plugin_manager::reset_caches();
-}
 
-if ($disable || $enable) {
     $url = new moodle_url('message.php');
     redirect($url);
 }
+
 // Page settings
 $PAGE->set_context(context_system::instance());
+$PAGE->requires->js_init_call('M.core_message.init_defaultoutputs');
 
-// Grab the renderer
 $renderer = $PAGE->get_renderer('core', 'message');
 
-// Display the manage message outputs interface
-$processors = get_message_processors();
-$messageoutputs = $renderer->manage_messageoutputs($processors);
-
-// Display the page
+// Display the page.
 echo $OUTPUT->header();
-echo $OUTPUT->heading($headingtitle);
-echo $messageoutputs;
+echo $renderer->manage_messageoutput_settings($allprocessors, $processors, $providers, $preferences);
 echo $OUTPUT->footer();
diff --git a/admin/settings/messaging.php b/admin/settings/messaging.php
new file mode 100644 (file)
index 0000000..2221e72
--- /dev/null
@@ -0,0 +1,78 @@
+<?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/>.
+
+/**
+ * Adds messaging related settings links for Messaging category to admin tree.
+ *
+ * @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $temp = new admin_settingpage('messages', new lang_string('messagingssettings', 'admin'));
+    $temp->add(new admin_setting_configcheckbox('messaging',
+        new lang_string('messaging', 'admin'),
+        new lang_string('configmessaging', 'admin'),
+        1));
+    $temp->add(new admin_setting_configcheckbox('messagingallusers',
+            new lang_string('messagingallusers', 'admin'),
+            new lang_string('configmessagingallusers', 'admin'),
+             0)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
+            new lang_string('messagingdefaultpressenter', 'admin'),
+            new lang_string('configmessagingdefaultpressenter', 'admin'),
+            1)
+    );
+    $options = array(
+        DAYSECS => new lang_string('secondstotime86400'),
+        WEEKSECS => new lang_string('secondstotime604800'),
+        2620800 => new lang_string('nummonths', 'moodle', 1),
+        7862400 => new lang_string('nummonths', 'moodle', 3),
+        15724800 => new lang_string('nummonths', 'moodle', 6),
+        0 => new lang_string('never')
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeletereadnotificationsdelay',
+            new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
+            604800,
+            $options)
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeleteallnotificationsdelay',
+            new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
+            2620800,
+            $options)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingallowemailoverride',
+        new lang_string('messagingallowemailoverride', 'admin'),
+        new lang_string('configmessagingallowemailoverride', 'admin'),
+        0));
+    $ADMIN->add('messaging', $temp);
+    $ADMIN->add('messaging', new admin_page_managemessageoutputs());
+
+    // Notification outputs plugins.
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\message $plugin */
+        $plugin->load_settings($ADMIN, 'messaging', $hassiteconfig);
+    }
+}
index 616d10e..925829c 100644 (file)
@@ -81,17 +81,6 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'blocksettings', $hassiteconfig);
     }
 
-    // message outputs
-    $ADMIN->add('modules', new admin_category('messageoutputs', new lang_string('messageoutputs', 'message')));
-    $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs());
-    $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs());
-    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
-    core_collator::asort_objects_by_property($plugins, 'displayname');
-    foreach ($plugins as $plugin) {
-        /** @var \core\plugininfo\message $plugin */
-        $plugin->load_settings($ADMIN, 'messageoutputs', $hassiteconfig);
-    }
-
     // authentication plugins
     $ADMIN->add('modules', new admin_category('authsettings', new lang_string('authentication', 'admin')));
 
@@ -576,14 +565,18 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_configduration('searchindextime',
             new lang_string('searchindextime', 'admin'), new lang_string('searchindextime_desc', 'admin'),
             600));
+    $temp->add(new admin_setting_heading('searchcoursesheading', new lang_string('searchablecourses', 'admin'), ''));
     $options = [
         0 => new lang_string('searchallavailablecourses_off', 'admin'),
         1 => new lang_string('searchallavailablecourses_on', 'admin')
     ];
     $temp->add(new admin_setting_configselect('searchallavailablecourses',
             new lang_string('searchallavailablecourses', 'admin'),
-            new lang_string('searchallavailablecourses_desc', 'admin'),
+            new lang_string('searchallavailablecoursesdesc', 'admin'),
             0, $options));
+    $temp->add(new admin_setting_configcheckbox('searchincludeallcourses',
+        new lang_string('searchincludeallcourses', 'admin'), new lang_string('searchincludeallcourses_desc', 'admin'),
+        0));
 
     // Search display options.
     $temp->add(new admin_setting_heading('searchdisplay', new lang_string('searchdisplay', 'admin'), ''));
index 96c7871..8c47a56 100644 (file)
@@ -176,6 +176,8 @@ $ADMIN->add('server', $temp);
 
 $ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment','admin'), "$CFG->wwwroot/$CFG->admin/environment.php"));
 $ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'), "$CFG->wwwroot/$CFG->admin/phpinfo.php"));
+$ADMIN->add('server', new admin_externalpage('testoutgoingmailconf', new lang_string('testoutgoingmailconf', 'admin'),
+            new moodle_url("$CFG->wwwroot/$CFG->admin/testoutgoingmailconf.php"), 'moodle/site:config', true));
 
 
 // "performance" settingpage
@@ -326,6 +328,10 @@ $temp->add(new admin_setting_configtextarea('allowedemaildomains',
         new lang_string('allowedemaildomains', 'admin'),
         new lang_string('configallowedemaildomains', 'admin'),
         ''));
+$url = new moodle_url('/admin/testoutgoingmailconf.php');
+$link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
+$temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
+        new lang_string('testoutgoingmaildetail', 'admin', $link)));
 $temp->add(new admin_setting_heading('emaildoesnotfit', new lang_string('doesnotfit', 'admin'),
         new lang_string('doesnotfitdetail', 'admin')));
 $charsets = get_list_of_charsets();
index b559de3..de5f75b 100644 (file)
@@ -13,45 +13,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablewebservices', new lang_string('enablewebservices', 'admin'), new lang_string('configenablewebservices', 'admin'), 0));
 
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
-        new lang_string('messagingallusers', 'admin'),
-        new lang_string('configmessagingallusers', 'admin'),
-        0)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
-        new lang_string('messagingdefaultpressenter', 'admin'),
-        new lang_string('configmessagingdefaultpressenter', 'admin'),
-        1)
-    );
-
-    $options = array(
-        DAYSECS => new lang_string('secondstotime86400'),
-        WEEKSECS => new lang_string('secondstotime604800'),
-        2620800 => new lang_string('nummonths', 'moodle', 1),
-        7862400 => new lang_string('nummonths', 'moodle', 3),
-        15724800 => new lang_string('nummonths', 'moodle', 6),
-        0 => new lang_string('never')
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeletereadnotificationsdelay',
-        new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
-        604800,
-        $options)
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeleteallnotificationsdelay',
-        new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
-        2620800,
-        $options)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
-
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablestats', new lang_string('enablestats', 'admin'), new lang_string('configenablestats', 'admin'), 0));
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablerssfeeds', new lang_string('enablerssfeeds', 'admin'), new lang_string('configenablerssfeeds', 'admin'), 0));
index 102b758..32d91c6 100644 (file)
@@ -33,6 +33,7 @@ $ADMIN->add('root', new admin_category('competencies', new lang_string('competen
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
+$ADMIN->add('root', new admin_category('messaging', new lang_string('messagingcategory', 'admin')));
 $ADMIN->add('root', new admin_category('modules', new lang_string('plugins', 'admin')));
 $ADMIN->add('root', new admin_category('security', new lang_string('security','admin')));
 $ADMIN->add('root', new admin_category('appearance', new lang_string('appearance','admin')));
diff --git a/admin/testoutgoingmailconf.php b/admin/testoutgoingmailconf.php
new file mode 100644 (file)
index 0000000..ce5857f
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Test output mail configuration page
+ *
+ * @copyright 2019 Victor Deniz <victor@moodle.com>, based on Michael Milette <michael.milette@tngconsulting.ca> code
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+// This is an admin page.
+admin_externalpage_setup('testoutgoingmailconf');
+
+$headingtitle = get_string('testoutgoingmailconf', 'admin');
+$homeurl = new moodle_url('/admin/category.php', array('category' => 'email'));
+$returnurl = new moodle_url('/admin/testoutgoingconf.php');
+
+$form = new core_admin\form\testoutgoingmailconf_form(null, ['returnurl' => $returnurl]);
+if ($form->is_cancelled()) {
+    redirect($homeurl);
+}
+
+// Display the page.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($headingtitle);
+
+$data = $form->get_data();
+if ($data) {
+    $emailuser = new stdClass();
+    $emailuser->email = $data->recipient;
+    $emailuser->id = -99;
+
+    $subject = get_string('testoutgoingmailconf_subject', 'admin', $SITE->fullname);
+    $messagetext = get_string('testoutgoingmailconf_message', 'admin');
+
+    // Manage Moodle debugging options.
+    $debuglevel = $CFG->debug;
+    $debugdisplay = $CFG->debugdisplay;
+    $debugsmtp = $CFG->debugsmtp;
+    $CFG->debugdisplay = true;
+    $CFG->debugsmtp = true;
+    $CFG->debug = 15;
+
+    // Send test email.
+    ob_start();
+    $success = email_to_user($emailuser, $USER, $subject, $messagetext);
+    $smtplog = ob_get_contents();
+    ob_end_clean();
+
+    // Restore Moodle debugging options.
+    $CFG->debug = $debuglevel;
+    $CFG->debugdisplay = $debugdisplay;
+    $CFG->debugsmtp = $debugsmtp;
+
+    if ($success) {
+        $msgparams = new stdClass();
+        $msgparams->fromemail = $USER->email;
+        $msgparams->toemail = $emailuser->email;
+        $msg = get_string('testoutgoingmailconf_sentmail', 'admin', $msgparams);
+        $notificationtype = 'notifysuccess';
+    } else {
+        $notificationtype = 'notifyproblem';
+        // No communication between Moodle and the SMTP server - no error output.
+        if (trim($smtplog) == false) {
+            $msg = get_string('testoutgoingmailconf_errorcommunications', 'admin');
+        } else {
+            $msg = $smtplog;
+        }
+    }
+
+    // Show result.
+    echo $OUTPUT->notification($msg, $notificationtype);
+}
+
+$form->display();
+echo $OUTPUT->footer();
index c66254a..180805d 100644 (file)
Binary files a/admin/tool/analytics/amd/build/model.min.js and b/admin/tool/analytics/amd/build/model.min.js differ
index 2f0b605..f39c577 100644 (file)
@@ -20,8 +20,8 @@
  * @copyright  2017 David Monllao
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events'],
-    function($, Str, log, Notification, ModalFactory, ModalEvents) {
+define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/templates'],
+    function($, Str, log, Notification, ModalFactory, ModalEvents, Templates) {
 
     /**
      * List of actions that require confirmation and confirmation message.
@@ -94,10 +94,65 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
                     modal.getRoot().on(ModalEvents.save, function() {
                         window.location.href = a.attr('href');
                     });
+                    modal.show();
+                    return modal;
+                }).fail(Notification.exception);
+            });
+        },
+
+        /**
+         * Displays a select-evaluation-mode choice.
+         *
+         * @param  {String}  actionId
+         * @param  {Boolean} trainedOnlyExternally
+         */
+        selectEvaluationMode: function(actionId, trainedOnlyExternally) {
+            $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
+                ev.preventDefault();
+
+                var a = $(ev.currentTarget);
+
+                if (!trainedOnlyExternally) {
+                    // We can not evaluate trained models if the model was trained using data from this site.
+                    // Default to evaluate the model configuration if that is the case.
+                    window.location.href = a.attr('href');
+                    return;
+                }
+
+                var stringsPromise = Str.get_strings([
+                    {
+                        key: 'evaluatemodel',
+                        component: 'tool_analytics'
+                    }, {
+                        key: 'evaluationmode',
+                        component: 'tool_analytics'
+                    }
+                ]);
+                var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+                var bodyPromise = Templates.render('tool_analytics/evaluation_mode_selection', {});
+
+                $.when(stringsPromise, modalPromise).then(function(strings, modal) {
+
+
+                    modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));
+
+                    modal.setTitle(strings[1]);
+                    modal.setSaveButtonText(strings[0]);
+                    modal.setBody(bodyPromise);
+
+                    modal.getRoot().on(ModalEvents.save, function() {
+                        var evaluationMode = $("input[name='evaluationmode']:checked").val();
+                        if (evaluationMode == 'trainedmodel') {
+                            a.attr('href', a.attr('href') + '&mode=trainedmodel');
+                        }
+                        window.location.href = a.attr('href');
+                        return;
+                    });
+
                     modal.show();
                     return modal;
                 }).fail(Notification.exception);
             });
         }
     };
-});
+});
\ No newline at end of file
index df59038..2e3a552 100644 (file)
@@ -41,6 +41,11 @@ class model_logs extends \table_sql {
      */
     protected $model = null;
 
+    /**
+     * @var string|false
+     */
+    protected $evaluationmode = false;
+
     /**
      * Sets up the table_log parameters.
      *
@@ -57,21 +62,32 @@ class model_logs extends \table_sql {
         $this->set_attribute('class', 'modellog generaltable generalbox');
         $this->set_attribute('aria-live', 'polite');
 
-        $this->define_columns(array('time', 'version', 'indicators', 'timesplitting', 'accuracy', 'info', 'usermodified'));
+        $this->define_columns(array('time', 'version', 'evaluationmode', 'indicators', 'timesplitting',
+            'accuracy', 'info', 'usermodified'));
         $this->define_headers(array(
             get_string('time'),
             get_string('version'),
+            get_string('evaluationmode', 'tool_analytics'),
             get_string('indicators', 'tool_analytics'),
             get_string('timesplittingmethod', 'analytics'),
             get_string('accuracy', 'tool_analytics'),
             get_string('info', 'tool_analytics'),
             get_string('fullnameuser'),
         ));
+
+        $evaluationmodehelp = new \help_icon('evaluationmode', 'tool_analytics');
+        $this->define_help_for_headers([null, null, $evaluationmodehelp, null, null, null, null, null]);
+
         $this->pageable(true);
         $this->collapsible(false);
         $this->sortable(false);
         $this->is_downloadable(false);
 
+        $this->evaluationmode = optional_param('evaluationmode', false, PARAM_ALPHANUM);
+        if ($this->evaluationmode && $this->evaluationmode != 'configuration' && $this->evaluationmode != 'trainedmodel') {
+            $this->evaluationmode = '';
+        }
+
         $this->define_baseurl($PAGE->url);
     }
 
@@ -86,6 +102,15 @@ class model_logs extends \table_sql {
         return userdate($log->version, $recenttimestr);
     }
 
+    /**
+     * Generate the evaluation mode column.
+     *
+     * @param \stdClass $log log data.
+     * @return string HTML for the evaluationmode column
+     */
+    public function col_evaluationmode($log) {
+        return get_string('evaluationmodecol' . $log->evaluationmode, 'tool_analytics');
+    }
     /**
      * Generate the time column.
      *
index b3a1c43..351dae4 100644 (file)
@@ -187,15 +187,21 @@ class models_list implements \renderable, \templatable {
 
             // Evaluate machine-learning-based models.
             if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
+
+                // Extra is_trained call as trained_locally returns false if the model has not been trained yet.
+                $trainedonlyexternally = !$model->trained_locally() && $model->is_trained();
+
+                $actionid = 'evaluate-' . $model->get_id();
+                $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationMode', [$actionid, $trainedonlyexternally]);
                 $urlparams['action'] = 'evaluate';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
-                    get_string('evaluate', 'tool_analytics'));
+                    get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid]);
                 $actionsmenu->add($icon);
             }
 
             // Machine-learning-based models evaluation log.
-            if (!$model->is_static()) {
+            if (!$model->is_static() && $model->get_logs()) {
                 $urlparams['action'] = 'log';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
index 25d182f..2836ca7 100644 (file)
@@ -35,6 +35,8 @@ Options:
 --non-interactive      Not interactive questions
 --timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
+--mode                 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
+    " model was imported" . "
 --reuse-prev-analysed  Reuse recently analysed courses instead of analysing the whole site. Set it to false while" .
     " coding indicators. Defaults to true (Optional)" . "
 -h, --help             Print out this help
@@ -50,6 +52,7 @@ list($options, $unrecognized) = cli_get_params(
         'modelid'               => false,
         'list'                  => false,
         'timesplitting'         => false,
+        'mode'                  => 'configuration',
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
         'filter'                => false
@@ -64,16 +67,30 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['list'] || $options['modelid'] === false) {
+if ($options['list']) {
     \tool_analytics\clihelper::list_models();
     exit(0);
 }
 
+if ($options['modelid'] === false) {
+    // All actions but --list require a modelid.
+    echo $help;
+    exit(0);
+}
+
 // Reformat them as an array.
 if ($options['filter'] !== false) {
     $options['filter'] = explode(',', $options['filter']);
 }
 
+if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') {
+    cli_error('Error: The provided mode is not supported');
+}
+
+if ($options['mode'] == 'trainedmodel' && $options['timesplitting']) {
+    cli_error('Sorry, no time splitting method can be specified when using \'trainedmodel\' mode.');
+}
+
 // We need admin permissions.
 \core\session\manager::set_user(get_admin());
 
@@ -89,6 +106,7 @@ $analyseroptions = array(
     'filter' => $options['filter'],
     'timesplitting' => $options['timesplitting'],
     'reuseprevanalysed' => $options['reuse-prev-analysed'],
+    'mode' => $options['mode'],
 );
 // Evaluate its suitability to predict accurately.
 $results = $model->evaluate($analyseroptions);
index 580d7b8..36eed2f 100644 (file)
@@ -53,6 +53,18 @@ $string['erroronlycli'] = 'Execution only allowed via command line';
 $string['errortrainingdataexport'] = 'The model training data could not be exported';
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
+$string['evaluationmode'] = 'Evaluation mode';
+$string['evaluationmode_help'] = 'There are two evaluation modes:
+
+* Trained model -  Site data is used as testing data to evaluate the accuracy of the trained model.
+* Configuration - Site data is split into training and testing data, to both train and test the accuracy of the model configuration.
+
+Trained model is only available if a trained model has been imported into the site, and has not yet been re-trained using site data.';
+$string['evaluationmodeinfo'] = 'This model has been imported into the site. You can either evaluate the performance of the model, or you can evaluate the performance of the model configuration using site data.';
+$string['evaluationmodetrainedmodel'] = 'Evaluate the trained model';
+$string['evaluationmodecoltrainedmodel'] = 'Trained model';
+$string['evaluationmodecolconfiguration'] = 'Configuration';
+$string['evaluationmodeconfiguration'] = 'Evaluate the model configuration';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
 $string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
@@ -70,7 +82,7 @@ $string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between thi
 $string['importedsuccessfully'] = 'The model has been successfully imported.';
 $string['insights'] = 'Insights';
 $string['invalidanalysables'] = 'Invalid site elements';
-$string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
+$string['invalidanalysablesinfo'] = 'This page lists analysable elements that can\'t be used by this prediction model. The listed elements can\'t be used either to train the prediction model nor can the prediction model obtain predictions for them.';
 $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidindicatorsremoved'] = 'A new model has been added. Indicators that do not work with the selected target have been automatically removed.';
 $string['invalidprediction'] = 'Invalid to get predictions';
@@ -104,7 +116,7 @@ $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['viewlog'] = 'Log';
+$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.';
 $string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.';
index 23d8454..71f303a 100644 (file)
@@ -169,7 +169,13 @@ switch ($action) {
         // Web interface is used by people who can not use CLI nor code stuff, always use
         // cached stuff as they will change the model through the web interface as well
         // which invalidates the previously analysed stuff.
-        $results = $model->evaluate(array('reuseprevanalysed' => true));
+        $options = ['reuseprevanalysed' => true];
+
+        $mode = optional_param('mode', false, PARAM_ALPHANUM);
+        if ($mode == 'trainedmodel') {
+            $options['mode'] = 'trainedmodel';
+        }
+        $results = $model->evaluate($options);
         $renderer = $PAGE->get_renderer('tool_analytics');
         echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
         break;
diff --git a/admin/tool/analytics/templates/evaluation_mode_selection.mustache b/admin/tool/analytics/templates/evaluation_mode_selection.mustache
new file mode 100644 (file)
index 0000000..e9b32ce
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    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_analytics/evaluation_mode_selector
+
+    Evaluation mode selector.
+
+    The purpose of this template is to render the evaluation mode radio button.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="box mb-4">{{#str}} evaluationmodeinfo, tool_analytics {{/str}}</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-trainedmodel" value="trainedmodel" checked>
+    <label class="form-check-label" for="id-mode-trainedmodel">{{#str}} evaluationmodetrainedmodel, tool_analytics {{/str}}</label>
+</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-configuration" value="configuration">
+    <label class="form-check-label" for="id-mode-configuration">{{#str}} evaluationmodeconfiguration, tool_analytics {{/str}}</label>
+</div>
\ No newline at end of file
index 86b1799..bb3fa70 100644 (file)
@@ -277,7 +277,7 @@ $string['resubmitrequestasnew'] = 'Resubmit as new request';
 $string['resubmitrequest'] = 'Resubmit {$a->type} request for {$a->username}';
 $string['resubmittedrequest'] = 'The existing {$a->type} request for {$a->username} was cancelled and resubmitted';
 $string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
-$string['resultdownloadready'] = 'Your copy of your personal data in {$a} that you recently requested is now available for download. Please click on the link below to go to the download page.';
+$string['resultdownloadready'] = 'Your copy of your personal data from {$a} that you recently requested is now available for download from the following link.';
 $string['reviewdata'] = 'Review data';
 $string['retentionperiod'] = 'Retention period';
 $string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
index 138a420..44c9521 100644 (file)
@@ -116,7 +116,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $this->userrole = create_role('User role', 'lpuserrole', 'learning plan user role description');
 
         assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
-        assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
+        assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
         assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
         assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
         assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id);
index c8f9816..5371c48 100644 (file)
@@ -274,7 +274,6 @@ class framework_importer {
             // We are calling from browser, display progress bar.
             if ($this->useprogressbar === true) {
                 $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'tool_lpimportcsv'));
-                $this->progress->start_html();
             } else {
                 // Avoid html output on CLI scripts.
                 $this->progress = new \core\progress\none();
@@ -464,7 +463,6 @@ class framework_importer {
         $framework = api::create_framework($record);
         if ($this->useprogressbar === true) {
             $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
-            $this->progress->start_html();
         } else {
             $this->progress = new \core\progress\none();
         }
index a9c378d..1711f5e 100644 (file)
@@ -109,7 +109,7 @@ $string['sslv3'] = 'SSLv2 (Force SSL Version 3)';
 $string['taskcleanup'] = 'Cleanup of unverified incoming email';
 $string['taskpickup'] = 'Incoming email pickup';
 $string['tls'] = 'TLS (TLS; started via protocol-level negotiation over unencrypted channel; RECOMMENDED way of initiating secure connection)';
-$string['tlsv1'] = 'TLSv1 (TLS direct version 1.x connection to server)';
+$string['tlsv1'] = 'TLSv1 (direct connection to TLS server version 1.x)';
 $string['validateaddress'] = 'Validate sender email address';
 $string['validateaddress_help'] = 'When a message is received from a user, Moodle attempts to validate the message by comparing the email address of the sender with the email address in their user profile.
 
index 01b18fd..ef9052e 100644 (file)
@@ -64,11 +64,11 @@ $string['managerules'] = 'Event monitoring rules';
 $string['messageprovider:notification'] = 'Notifications of rule subscriptions';
 $string['messagetemplate'] = 'Notification message';
 $string['messagetemplate_help'] = 'A notification message is sent to subscribers once the notification threshold has been reached. It can include any or all of the following placeholders:
-<br /><br />
-* Link to the location of the event {link}<br />
-* Link to the area monitored {modulelink}<br />
-* Rule name {rulename}<br />
-* Description {description}<br />
+
+* Link to the location of the event {link}
+* Link to the area monitored {modulelink}
+* Rule name {rulename}
+* Description {description}
 * Event {eventname}';
 $string['messagetemplate_link'] = 'admin/tool/monitor/managerules';
 $string['moduleinstance'] = 'Instance';
index 3f5b0b9..6117521 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['cannotfit'] = 'The replacement is longer than original and shortening is not allow, cannot continue.';
+$string['cannotfit'] = 'The replacement is longer than the original and shortening is not allowed; cannot continue.';
 $string['disclaimer'] = 'I understand the risks of this operation';
 $string['doit'] = 'Yes, do it!';
 $string['excludedtables'] = 'Several tables are not updated as part of the text replacement. These include configuration, log, events, and session tables.';
index a28b478..1255622 100644 (file)
@@ -50,14 +50,39 @@ $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of schedule
 $string['taskdisabled'] = 'Task disabled';
 $string['tasklogs'] = 'Task logs';
 $string['taskscheduleday'] = 'Day';
-$string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every day</li><li><strong>*/2</strong> Every 2nd day</li><li><strong>1</strong> The first of every month</li><li><strong>1,15</strong> The first and fifteenth of every month</li></ul>';
+$string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:
+
+* <strong>*</strong> Every day
+* <strong>*/2</strong> Every 2nd day
+* <strong>1</strong> The first of every month
+* <strong>1,15</strong> The first and fifteenth of every month';
 $string['taskscheduledayofweek'] = 'Day of week';
-$string['taskscheduledayofweek_help'] = 'Day of week field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every day</li><li><strong>0</strong> Every Sunday</li><li><strong>6</strong> Every Saturday</li><li><strong>1,5</strong> Every Monday and Friday</li></ul>';
+$string['taskscheduledayofweek_help'] = 'Day of week field for task schedule. The field uses the same format as unix cron. Some examples are:
+
+* <strong>*</strong> Every day
+* <strong>0</strong> Every Sunday
+* <strong>6</strong> Every Saturday
+* <strong>1,5</strong> Every Monday and Friday';
 $string['taskschedulehour'] = 'Hour';
-$string['taskschedulehour_help'] = 'Hour field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every hour</li><li><strong>*/2</strong> Every 2 hours</li><li><strong>2-10</strong> Every hour from 2am until 10am (inclusive)</li><li><strong>2,6,9</strong> 2am, 6am and 9am</li></ul>';
+$string['taskschedulehour_help'] = 'Hour field for task schedule. The field uses the same format as unix cron. Some examples are:
+
+* <strong>*</strong> Every hour
+* <strong>*/2</strong> Every 2 hours
+* <strong>2-10</strong> Every hour from 2am until 10am (inclusive)
+* <strong>2,6,9</strong> 2am, 6am and 9am';
 $string['taskscheduleminute'] = 'Minute';
-$string['taskscheduleminute_help'] = 'Minute field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every minute</li><li><strong>*/5</strong> Every 5 minutes</li><li><strong>2-10</strong> Every minute between 2 and 10 past the hour (inclusive)</li><li><strong>2,6,9</strong> 2 6 and 9 minutes past the hour</li></ul>';
+$string['taskscheduleminute_help'] = 'Minute field for task schedule. The field uses the same format as unix cron. Some examples are:
+
+* <strong>*</strong> Every minute
+* <strong>*/5</strong> Every 5 minutes
+* <strong>2-10</strong> Every minute between 2 and 10 past the hour (inclusive)
+* <strong>2,6,9</strong> 2, 6 and 9 minutes past the hour';
 $string['taskschedulemonth'] = 'Month';
-$string['taskschedulemonth_help'] = 'Month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every month</li><li><strong>*/2</strong> Every second month</li><li><strong>1</strong> Every January</li><li><strong>1,5</strong> Every January and May</li></ul>';
+$string['taskschedulemonth_help'] = 'Month field for task schedule. The field uses the same format as unix cron. Some examples are:
+
+* <strong>*</strong> Every month
+* <strong>*/2</strong> Every second month
+* <strong>1</strong> Every January
+* <strong>1,5</strong> Every January and May';
 $string['privacy:metadata'] = 'The Scheduled task configuration plugin does not store any personal data.';
 $string['viewlogs'] = 'View logs for {$a}';
index c498b34..6c20d8f 100644 (file)
@@ -104,8 +104,7 @@ $string['reset_help'] = 'Whether to reset the course after creating/updating it.
 $string['result'] = 'Result';
 $string['restoreafterimport'] = 'Restore after import';
 $string['rowpreviewnum'] = 'Preview rows';
-$string['rowpreviewnum_help'] = 'Number of rows from the CSV file that will be previewed in the next page. This option exists in
-order to limit the next page size.';
+$string['rowpreviewnum_help'] = 'Number of rows from the CSV file that will be previewed on the following page. This option is for limiting the size of the following page.';
 $string['shortnametemplate'] = 'Template to generate a shortname';
 $string['shortnametemplate_help'] = 'The short name of the course is displayed in the navigation. You may use template syntax here (%f = fullname, %i = idnumber), or enter an initial value that is incremented.';
 $string['templatefile'] = 'Restore from this file after upload';
index d2efeab..2abd947 100644 (file)
@@ -156,7 +156,7 @@ $string['newtable'] = 'New table';
 $string['newtablefrommysql'] = 'New table from MySQL';
 $string['new_table_from_mysql'] = 'New table from MySQL';
 $string['nofieldsspecified'] = 'No fields specified';
-$string['nomasterprimaryuniquefound'] = 'The column(s) that you foreign key references must be included in a primary or unique KEY in the referenced table. Note, the column being in a UNIQUE INDEX is not good enough.';
+$string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
 $string['nomissingindexesfound'] = 'No missing indexes have been found, your DB doesn\'t need further actions.';
 $string['noreffieldsspecified'] = 'No reference fields specified';
 $string['noreftablespecified'] = 'Specified reference table not found';
@@ -221,6 +221,6 @@ $string['yeswrongdefaultsfound'] = '<p>Some inconsistent defaults have been foun
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more inconsistent defaults are found.</p>';
 $string['yeswrongintsfound'] = '<p>Some wrong integers have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them. Remember to backup your data first!</p>
 <p>After fixing them, it is highly recommended to execute this utility again to check that no more wrong integers are found.</p>';
-$string['yeswrongoraclesemanticsfound'] = '<p>Some Oracle columns using BYTE semantics have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all them. Remember to backup your data first!</p>
+$string['yeswrongoraclesemanticsfound'] = '<p>Some Oracle columns using BYTE semantics have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to convert them all. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more wrong semantics are found.</p>';
 $string['privacy:metadata'] = 'The XMLDB editor plugin does not store any personal data.';
index be0d3a0..919e9f6 100644 (file)
@@ -63,7 +63,9 @@ interface classifier extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
index 3b6c6e4..d4a073b 100644 (file)
@@ -537,6 +537,29 @@ class model {
         }
 
         $options['evaluation'] = true;
+
+        if (empty($options['mode'])) {
+            $options['mode'] = 'configuration';
+        }
+
+        switch ($options['mode']) {
+            case 'trainedmodel':
+
+                // We are only interested on the time splitting method used by the trained model.
+                $options['timesplitting'] = $this->model->timesplitting;
+
+                // Provide the trained model directory to the ML backend if that is what we want to evaluate.
+                $trainedmodeldir = $this->get_output_dir(['execution']);
+                break;
+            case 'configuration':
+
+                $trainedmodeldir = false;
+                break;
+
+            default:
+                throw new \moodle_exception('errorunknownaction', 'analytics');
+        }
+
         $this->init_analyser($options);
 
         if (empty($this->get_indicators())) {
@@ -575,10 +598,10 @@ class model {
             // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
             if ($this->get_target()->is_linear()) {
                 $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             } else {
                 $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             }
 
             $result->status = $predictorresult->status;
@@ -596,7 +619,7 @@ class model {
                 $dir = $predictorresult->dir;
             }
 
-            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
+            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info, $options['mode']);
 
             $results[$timesplitting->get_id()] = $result;
         }
@@ -1462,6 +1485,29 @@ class model {
         return \core_analytics\dataset_manager::export_training_data($this->get_id(), $timesplittingid);
     }
 
+    /**
+     * Has the model been trained using data from this site?
+     *
+     * This method is useful to determine if a trained model can be evaluated as
+     * we can not use the same data for training and for evaluation.
+     *
+     * @return bool
+     */
+    public function trained_locally() : bool {
+        global $DB;
+
+        if (!$this->is_trained() || $this->is_static()) {
+            // Early exit.
+            return false;
+        }
+
+        if ($DB->record_exists('analytics_train_samples', ['modelid' => $this->model->id])) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Flag the provided file as used for training or prediction.
      *
@@ -1487,14 +1533,16 @@ class model {
      * @param float $score
      * @param string $dir
      * @param array $info
+     * @param string $evaluationmode
      * @return int The inserted log id
      */
-    protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
+    protected function log_result($timesplittingid, $score, $dir = false, $info = false, $evaluationmode = 'configuration') {
         global $DB, $USER;
 
         $log = new \stdClass();
         $log->modelid = $this->get_id();
         $log->version = $this->model->version;
+        $log->evaluationmode = $evaluationmode;
         $log->target = $this->model->target;
         $log->indicators = $this->model->indicators;
         $log->timesplitting = $timesplittingid;
index c2d2a89..c8e0bcf 100644 (file)
@@ -63,7 +63,9 @@ interface regressor extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
diff --git a/analytics/classes/stats.php b/analytics/classes/stats.php
new file mode 100644 (file)
index 0000000..0caf975
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Provides the {@link \core_analytics\stats} class.
+ *
+ * @package     core_analytics
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provides stats and meta information about the analytics usage on this site.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class stats {
+
+    /**
+     * Return the number of models enabled on this site.
+     *
+     * @return int
+     */
+    public static function enabled_models() : int {
+        return count(manager::get_all_models(true));
+    }
+
+    /**
+     * Return the number of predictions generated by the system.
+     *
+     * @return int
+     */
+    public static function predictions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_predictions');
+    }
+
+    /**
+     * Return the number of suggested actions executed by users.
+     *
+     * @return int
+     */
+    public static function actions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions');
+    }
+
+    /**
+     * Return the number of suggested actions flagged as not useful.
+     *
+     * @return int
+     */
+    public static function actions_not_useful() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]);
+    }
+}
index 18582c3..9b69093 100644 (file)
@@ -274,7 +274,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      * test_ml_export_import
      *
      * @param string $predictionsprocessorclass The class name
-     * @dataProvider provider_ml_export_import
+     * @dataProvider provider_ml_processors
      */
     public function test_ml_export_import($predictionsprocessorclass) {
 
@@ -296,6 +296,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
 
         $model->train();
+        $this->assertTrue($model->trained_locally());
 
         $this->generate_courses(10, ['visible' => 0]);
 
@@ -314,16 +315,18 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             $this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
         }
 
+        $this->assertFalse($importmodel->trained_locally());
+
         set_config('enabled_stores', '', 'tool_log');
         get_log_manager(true);
     }
 
     /**
-     * provider_ml_export_import
+     * provider_ml_processors
      *
      * @return array
      */
-    public function provider_ml_export_import() {
+    public function provider_ml_processors() {
         $cases = [
             'case' => [],
         ];
@@ -425,14 +428,14 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     /**
      * Basic test to check that prediction processors work as expected.
      *
-     * @dataProvider provider_ml_test_evaluation
+     * @dataProvider provider_ml_test_evaluation_configuration
      * @param string $modelquality
      * @param int $ncourses
      * @param array $expected
      * @param string $predictionsprocessorclass
      * @return void
      */
-    public function test_ml_evaluation($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
+    public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
         $this->resetAfterTest(true);
         $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
@@ -473,6 +476,46 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         get_log_manager(true);
     }
 
+    /**
+     * Tests the evaluation of already trained models.
+     *
+     * @dataProvider provider_ml_processors
+     * @param  string $predictionsprocessorclass
+     * @return null
+     */
+    public function test_ml_evaluation_trained_model($predictionsprocessorclass) {
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+        set_config('timesplittings',
+            '\core\analytics\time_splitting\quarters,\core\analytics\time_splitting\quarters_accum', 'analytics');
+
+        $model = $this->add_perfect_model();
+
+        // Generate training data.
+        $this->generate_courses(50);
+
+        // We repeat the test for all prediction processors.
+        $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
+        if ($predictionsprocessor->is_ready() !== true) {
+            $this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
+        }
+
+        $model->update(true, false, '\\core\\analytics\\time_splitting\\quarters', get_class($predictionsprocessor));
+        $model->train();
+
+        $zipfilename = 'model-zip-' . microtime() . '.zip';
+        $zipfilepath = $model->export_model($zipfilename);
+        $importmodel = \core_analytics\model::import_model($zipfilepath);
+
+        $results = $importmodel->evaluate(['mode' => 'trainedmodel']);
+        $this->assertEquals(0, $results['\\core\\analytics\\time_splitting\\quarters']->status);
+        $this->assertEquals(1, $results['\\core\\analytics\\time_splitting\\quarters']->score);
+
+        set_config('enabled_stores', '', 'tool_log');
+        get_log_manager(true);
+    }
+
     /**
      * test_read_indicator_calculations
      *
@@ -547,11 +590,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     }
 
     /**
-     * provider_ml_test_evaluation
+     * provider_ml_test_evaluation_configuration
      *
      * @return array
      */
-    public function provider_ml_test_evaluation() {
+    public function provider_ml_test_evaluation_configuration() {
 
         $cases = array(
             'bad' => array(
diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php
new file mode 100644 (file)
index 0000000..d92f403
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Provides the {@link analytics_stats_testcase} class.
+ *
+ * @package     core_analytics
+ * @category    test
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
+require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+
+/**
+ * Unit tests for the analytics stats.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_stats_testcase extends advanced_testcase {
+
+    /**
+     * Set up the test environment.
+     */
+    public function setUp() {
+
+        $this->setAdminUser();
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::enabled_models()} implementation.
+     */
+    public function test_enabled_models() {
+
+        $this->resetAfterTest(true);
+
+        // By default, sites have {@link \core\analytics\target\no_teaching} enabled.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('\core\analytics\target\course_dropout'),
+            [
+                \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
+            ]
+        );
+
+        // Purely adding a new model does not make it included in the stats.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        // New models must be enabled to have them counted.
+        $model->enable('\core\analytics\time_splitting\quarters');
+        $this->assertEquals(2, \core_analytics\stats::enabled_models());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::predictions()} implementation.
+     */
+    public function test_predictions() {
+
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // No predictions yet.
+        $this->assertEquals(0, \core_analytics\stats::predictions());
+
+        // Get one new prediction.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $result = $model->predict();
+
+        $this->assertEquals(1, count($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Nothing changes if there is no new prediction.
+        $result = $model->predict();
+        $this->assertFalse(isset($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Get two more predictions, we have three in total now.
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]);
+
+        $result = $model->predict();
+        $this->assertEquals(2, count($result->predictions));
+        $this->assertEquals(3, \core_analytics\stats::predictions());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation.
+     */
+    public function test_actions() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // Generate two predictions.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $model->predict();
+
+        list($p1, $p2) = array_values($DB->get_records('analytics_predictions'));
+
+        $p1 = new \core_analytics\prediction($p1, []);
+        $p2 = new \core_analytics\prediction($p2, []);
+
+        // No actions executed at the start.
+        $this->assertEquals(0, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has acknowledged the first prediction.
+        $p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
+        $this->assertEquals(1, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has marked the other prediction as not useful.
+        $p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target());
+        $this->assertEquals(2, \core_analytics\stats::actions());
+        $this->assertEquals(1, \core_analytics\stats::actions_not_useful());
+    }
+}
index 550a3da..3187365 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in analytics sub system,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+
+* \core_analytics\regressor::evaluate_regression and \core_analytics\classifier::evaluate_classification
+  have been updated to include a new $trainedmodeldir param. This new param will be used to evaluate the
+  existing trained model.
+
 === 3.5 ===
 
 * There are two new methods for analysers, processes_user_data() and join_sample_user(). You
index c6c0145..d3f8a83 100644 (file)
@@ -33,7 +33,7 @@ $string['auth_dbextencodinghelp'] = 'Encoding used in external database';
 $string['auth_dbextrafields'] = 'These fields are optional.  You can choose to pre-fill some Moodle user fields with information from the <b>external database fields</b> that you specify here. <p>If you leave these blank, then defaults will be used.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>';
 $string['auth_dbfieldpass'] = 'Name of the field containing passwords';
 $string['auth_dbfieldpass_key'] = 'Password field';
-$string['auth_dbfielduser'] = 'Name of the field containing usernames';
+$string['auth_dbfielduser'] = 'Name of the field containing usernames. This field must be a varchar data type.';
 $string['auth_dbfielduser_key'] = 'Username field';
 $string['auth_dbhost'] = 'The computer hosting the database server. Use a system DSN entry if using ODBC. Use a PDO DSN entry if using PDO.';
 $string['auth_dbhost_key'] = 'Host';
index 76a4317..bbed21a 100644 (file)
@@ -164,10 +164,10 @@ $string['usernotfound'] = 'User not found in LDAP';
 $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}';
 
 $string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.';
-$string['diag_toooldversion'] = 'Its is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.';
+$string['diag_toooldversion'] = 'It is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.';
 $string['diag_emptycontext'] = 'Empty context found.';
-$string['diag_contextnotfound'] = 'Context {$a} does not  exists or cannot be read by bind DN.';
-$string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} does not exists or cannot be read by bind DN.';
+$string['diag_contextnotfound'] = 'Context {$a} doesn\'t exist or can\'t be read by bind DN.';
+$string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} doesn\'t exist or can\'t be read by bind DN.';
 
 // Deprecated since Moodle 3.4.
 $string['auth_ldap_creators'] = 'List of groups or contexts whose members are allowed to create new courses. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
index b3b22f5..2744dcd 100644 (file)
@@ -737,25 +737,6 @@ class auth_plugin_mnet extends auth_plugin_base {
         return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
     }
 
-    /**
-     * Cron function will be called automatically by cron.php every 5 minutes
-     *
-     * @return void
-     */
-    function cron() {
-        global $DB;
-
-        // run the keepalive client
-        $this->keepalive_client();
-
-        $random100 = rand(0,100);
-        if ($random100 < 10) {     // Approximately 10% of the time.
-            // nuke olden sessions
-            $longtime = time() - (1 * 3600 * 24);
-            $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
-        }
-    }
-
     /**
      * Cleanup any remote mnet_sessions, kill the local mnet_session data
      *
diff --git a/auth/mnet/classes/task/cron_task.php b/auth/mnet/classes/task/cron_task.php
new file mode 100644 (file)
index 0000000..dbd8e17
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+namespace auth_mnet\task;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A schedule task for mnet cron.
+ *
+ * @package   auth_mnet
+ * @copyright 2019 Simey Lameze <simey@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cron_task extends \core\task\scheduled_task {
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('crontask', 'auth_mnet');
+    }
+    /**
+     * Run auth mnet cron.
+     */
+    public function execute() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/auth/mnet/auth.php');
+        $mnetplugin = new \auth_plugin_mnet();
+        $mnetplugin->keepalive_client();
+
+        $random100 = rand(0,100);
+        if ($random100 < 10) {
+            $longtime = time() - DAYSECS;
+            $DB->delete_records_select('mnet_session', "expires < ?", [$longtime]);
+        }
+    }
+}
diff --git a/auth/mnet/db/tasks.php b/auth/mnet/db/tasks.php
new file mode 100644 (file)
index 0000000..e85dc3d
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+/**
+ * Definition of chat scheduled tasks.
+ *
+ * @package   auth_mnet
+ * @copyright 2019 Simey Lameze <simey@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+$tasks = array(
+    array(
+        'classname' => '\auth_mnet\task\cron_task',
+        'blocking' => 0,
+        'minute' => '*',
+        'hour' => '*',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*'
+    )
+);
index a8c7ed3..4836214 100644 (file)
@@ -28,6 +28,7 @@ $string['auth_mnet_roamin'] = 'These host\'s users can roam in to your site';
 $string['auth_mnet_roamout'] = 'Your users can roam out to these hosts';
 $string['auth_mnet_rpc_negotiation_timeout'] = 'The timeout in seconds for authentication over the XMLRPC transport.';
 $string['auto_add_remote_users'] = 'Auto add remote users';
+$string['crontask'] = 'Background processing for MNET authentication';
 $string['rpc_negotiation_timeout'] = 'RPC negotiation timeout';
 $string['sso_idp_description'] = 'Publish this service to allow your users to roam to the {$a} site without having to re-login there. <ul><li><em>Dependency</em>: You must also <strong>subscribe</strong> to the SSO (Service Provider) service on {$a}.</li></ul><br />Subscribe to this service to allow authenticated users from {$a} to access your site without having to re-login. <ul><li><em>Dependency</em>: You must also <strong>publish</strong> the SSO (Service Provider) service to {$a}.</li></ul><br />';
 $string['sso_idp_name'] = 'SSO  (Identity Provider)';
index ae88a9f..93ddd7c 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2019030700;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018112800;        // Requires this Moodle version
 $plugin->component = 'auth_mnet';       // Full name of the plugin (used for diagnostics)
index fd8b747..77d6ec4 100644 (file)
@@ -34,7 +34,7 @@ $string['auth_shibboleth_login'] = 'Shibboleth login';
 $string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth';
 $string['auth_shibboleth_manual_login'] = 'Manual login';
 $string['auth_shibboleth_select_member'] = 'I\'m a member of ...';
-$string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organization from the drop down list:';
+$string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:';
 $string['auth_shib_convert_data'] = 'Data modification API';
 $string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="../auth/shibboleth/README.txt">README</a> for further instructions.';
 $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!';
index ed51a17..910f9fb 100644 (file)
@@ -33,10 +33,10 @@ $string['aria:controls'] = 'Course overview controls';
 $string['aria:courseactions'] = 'Actions for current course';
 $string['aria:coursesummary'] = 'Course summary text:';
 $string['aria:courseprogress'] = 'Course progress:';
-$string['aria:displaydropdown'] = 'Display dropdown';
+$string['aria:displaydropdown'] = 'Display drop-down menu';
 $string['aria:favourites'] = 'Show starred courses';
 $string['aria:future'] = 'Show future courses';
-$string['aria:groupingdropdown'] = 'Grouping dropdown';
+$string['aria:groupingdropdown'] = 'Grouping drop-down menu';
 $string['aria:inprogress'] = 'Show in courses in progress';
 $string['aria:lastaccessed'] = 'Sort courses by last accessed date';
 $string['aria:list'] = 'Switch to list view';
@@ -44,11 +44,11 @@ $string['aria:title'] = 'Sort courses by course name';
 $string['aria:past'] = 'Show past courses';
 $string['aria:removefromfavourites'] = 'Remove star for';
 $string['aria:summary'] = 'Switch to summary view';
-$string['aria:sortingdropdown'] = 'Sorting dropdown';
+$string['aria:sortingdropdown'] = 'Sorting drop-down menu';
 $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
-$string['complete'] = 'Complete';
+$string['complete'] = 'complete';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
index 2567555..574b5e5 100644 (file)
 }}
 
 <div class="m-b-1 mr-1 d-flex align-items-center">
-    <div class="d-none d-md-inline-block mr-1">{{#str}} sortby, core {{/str}}</div>
     <div class="dropdown">
         <button id="sortingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown"  aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:sortingdropdown, block_myoverview {{/str}}">
-            <span data-active-item-text>
+            {{#pix}} t/sort_by {{/pix}}
+            <span class="d-sm-inline-block" data-active-item-text>
                 {{#title}}{{#str}} title, block_myoverview {{/str}}{{/title}}
                 {{#lastaccessed}}{{#str}} lastaccessed, block_myoverview {{/str}}{{/lastaccessed}}
             </span>
index 175d417..98ac925 100644 (file)
@@ -124,7 +124,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: List display  persistence
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     And I click on "List" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "List" in the "Course overview" "block"
@@ -132,7 +132,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: Cards display  persistence
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     And I click on "Card" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "Card" in the "Course overview" "block"
@@ -140,7 +140,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: Summary display  persistence
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     And I click on "Summary" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "Summary" in the "Course overview" "block"
@@ -206,18 +206,18 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: Show course category in cards display
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     When I click on "Card" "link" in the "Course overview" "block"
     Then I should see "Category 1" in the "Course overview" "block"
 
   Scenario: Show course category in list display
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     When I click on "List" "link" in the "Course overview" "block"
     Then I should see "Category 1" in the "Course overview" "block"
 
   Scenario: Show course category in summary display
     Given I log in as "student1"
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     When I click on "Summary" "link" in the "Course overview" "block"
     Then I should see "Category 1" in the "Course overview" "block"
index 40a2442..9752f6f 100644 (file)
@@ -39,7 +39,7 @@ Feature: The my overview block allows users to favourite their courses
     When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
     And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
     And I reload the page
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     And I click on "List" "link" in the "Course overview" "block"
     And I reload the page
     Then "//li[contains(concat(' ', normalize-space(@class), ' '), 'list-group-item') and contains(.,'Course 5')]//span[@data-region='is-favourite' and @aria-hidden='false']" "xpath_element" should exist
@@ -53,7 +53,7 @@ Feature: The my overview block allows users to favourite their courses
     When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
     And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
     And I reload the page
-    And I click on "Display dropdown" "button" in the "Course overview" "block"
+    And I click on "Display drop-down menu" "button" in the "Course overview" "block"
     And I click on "Summary" "link" in the "Course overview" "block"
     And I reload the page
     Then "//div[contains(concat(' ', normalize-space(@class), ' '), 'course-summaryitem') and contains(.,'Course 5')]//span[@data-region='is-favourite' and @aria-hidden='false']" "xpath_element" should exist
index c7cbc1c..aa9d14d 100644 (file)
@@ -24,7 +24,7 @@
 }}
 <div data-region="view-selector" class="btn-group">
     <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        {{#pix}} i/filter {{/pix}}
+        {{#pix}} t/sort_by {{/pix}}
         <span class="sr-only">
             {{#sorttimelinecourses}}<span data-active-item-text>{{/sorttimelinecourses}}{{#str}} ariaviewselector, block_timeline{{/str}}{{#sorttimelinecourses}}</span>{{/sorttimelinecourses}}
             {{#sorttimelinedates}}<span data-active-item-text>{{/sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{#sorttimelinedates}}</span>{{/sorttimelinedates}}
index 5775b13..fd6078a 100644 (file)
@@ -3512,6 +3512,11 @@ function calendar_output_fragment_event_form($args) {
         $mform->set_data($data);
     } else {
         $event = calendar_event::load($eventid);
+
+        if (!calendar_edit_event_allowed($event)) {
+            print_error('nopermissiontoupdatecalendar');
+        }
+
         $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
         $eventdata = $mapper->from_legacy_event_to_data($event);
         $data = array_merge((array) $eventdata, $data);
index 6ca460c..06f312d 100644 (file)
@@ -102,6 +102,7 @@ class core_comment_external extends external_api {
         if ($comments === false) {
             throw new moodle_exception('nopermissions', 'error', '', 'view comments');
         }
+        $options = array('blanktarget' => true);
 
         foreach ($comments as $key => $comment) {
 
@@ -110,7 +111,8 @@ class core_comment_external extends external_api {
                                                                                                  $context->id,
                                                                                                  $params['component'],
                                                                                                  '',
-                                                                                                 0);
+                                                                                                 0,
+                                                                                                 $options);
         }
 
         $results = array(
index 07f7a5b..59536cf 100644 (file)
@@ -570,7 +570,7 @@ class comment {
         $params['itemid'] = $this->itemid;
 
         $comments = array();
-        $formatoptions = array('overflowdiv' => true);
+        $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
         $rs = $DB->get_recordset_sql($sql, $params, $start, $perpage);
         foreach ($rs as $u) {
             $c = new stdClass();
@@ -717,7 +717,8 @@ class comment {
             $newcmt->fullname = fullname($USER);
             $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
             $newcmt->profileurl = $url->out();
-            $newcmt->content = format_text($newcmt->content, $newcmt->format, array('overflowdiv'=>true));
+            $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
+            $newcmt->content = format_text($newcmt->content, $newcmt->format, $formatoptions);
             $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
 
             $commentlist = array($newcmt);
index dc30c34..d04182a 100644 (file)
@@ -68,7 +68,7 @@ class comment_manager {
                        ON u.id=c.userid
               ORDER BY c.timecreated ASC";
         $rs = $DB->get_recordset_sql($sql, null, $start, $this->perpage);
-        $formatoptions = array('overflowdiv' => true);
+        $formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
         foreach ($rs as $item) {
             // Set calculated fields
             $item->fullname = fullname($item);
index 3033d0d..4277282 100644 (file)
@@ -245,7 +245,7 @@ class course_competency extends persistent {
 
         $results = $DB->get_records_sql('SELECT course.id, course.visible, course.shortname, course.idnumber,
                                                 course.fullname, course.summary, course.summaryformat, course.startdate,
-                                                course.enddate
+                                                course.enddate, course.category
                                            FROM {course} course
                                            JOIN {' . self::TABLE . '} coursecomp
                                              ON coursecomp.courseid = course.id
index 1a26d38..4c83541 100644 (file)
@@ -139,7 +139,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->userrole = create_role('User role', 'userrole', 'learning plan user role description');
 
         assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
-        assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
+        assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
         assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id);
         assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
         assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
similarity index 95%
rename from course/classes/search/mycourse.php
rename to course/classes/search/course.php
index bac0127..27ba201 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Search area for Moodle courses I can access.
+ * Search area for Moodle courses.
  *
  * @package    core_course
  * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
@@ -26,13 +26,13 @@ namespace core_course\search;
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Search area for Moodle courses I can access.
+ * Search area for Moodle courses.
  *
  * @package    core_course
  * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mycourse extends \core_search\base {
+class course extends \core_search\base {
 
     /**
      * The context levels the search implementation is working on.
@@ -112,9 +112,13 @@ class mycourse extends \core_search\base {
         if (!$course) {
             return \core_search\manager::ACCESS_DELETED;
         }
-        if (can_access_course($course)) {
+
+        $coursecontext = \context_course::instance($course->id);
+
+        if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
             return \core_search\manager::ACCESS_GRANTED;
         }
+
         return \core_search\manager::ACCESS_DENIED;
     }
 
index 8f5db60..315281e 100644 (file)
@@ -135,7 +135,8 @@ class customfield extends \core_search\base {
         if (!$course) {
             return \core_search\manager::ACCESS_DELETED;
         }
-        if (can_access_course($course)) {
+        $coursecontext = \context_course::instance($course->id);
+        if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
             return \core_search\manager::ACCESS_GRANTED;
         }
         return \core_search\manager::ACCESS_DENIED;
index 27d2629..3e8744c 100644 (file)
@@ -318,7 +318,28 @@ class core_course_external extends external_api {
                             require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php');
                             $getcontentfunction = $cm->modname.'_export_contents';
                             if (function_exists($getcontentfunction)) {
-                                if (empty($filters['excludecontents']) and $contents = $getcontentfunction($cm, $baseurl)) {
+                                $contents = $getcontentfunction($cm, $baseurl);
+                                $module['contentsinfo'] = array(
+                                    'filescount' => count($contents),
+                                    'filessize' => 0,
+                                    'lastmodified' => 0,
+                                    'mimetypes' => array(),
+                                );
+                                foreach ($contents as $content) {
+                                    if (isset($content['filesize'])) {
+                                        $module['contentsinfo']['filessize'] += $content['filesize'];
+                                    }
+                                    if (isset($content['timemodified']) &&
+                                            ($content['timemodified'] > $module['contentsinfo']['lastmodified'])) {
+
+                                        $module['contentsinfo']['lastmodified'] = $content['timemodified'];
+                                    }
+                                    if (isset($content['mimetype'])) {
+                                        $module['contentsinfo']['mimetypes'][$content['mimetype']] = $content['mimetype'];
+                                    }
+                                }
+
+                                if (empty($filters['excludecontents']) and !empty($contents)) {
                                     $module['contents'] = $contents;
                                 } else {
                                     $module['contents'] = array();
@@ -470,7 +491,18 @@ class core_course_external extends external_api {
                                                   'license' => new external_value(PARAM_TEXT, 'Content license'),
                                               )
                                           ), VALUE_DEFAULT, array()
-                                      )
+                                      ),
+                                    'contentsinfo' => new external_single_structure(
+                                        array(
+                                            'filescount' => new external_value(PARAM_INT, 'Total number of files.'),
+                                            'filessize' => new external_value(PARAM_INT, 'Total files size.'),
+                                            'lastmodified' => new external_value(PARAM_INT, 'Last time files were modified.'),
+                                            'mimetypes' => new external_multiple_structure(
+                                                new external_value(PARAM_RAW, 'File mime type.'),
+                                                'Files mime types.'
+                                            ),
+                                        ), 'Contents summary information.', VALUE_OPTIONAL
+                                    ),
                                 )
                             ), 'list of module'
                     )
index 9feeec5..d5db319 100644 (file)
@@ -79,6 +79,9 @@ if (has_capability('moodle/user:loginas', $systemcontext)) {
 
 // Login as this user and return to course home page.
 \core\session\manager::loginas($userid, $context);
+// Add a notification to let the logged in as user know that all content will be force cleaned
+// while in this session.
+\core\notification::info(get_string('sessionforceclean', 'core'));
 $newfullname = fullname($USER, true);
 
 $strloginas    = get_string('loginas');
index bc32f62..e8650a3 100644 (file)
@@ -47,7 +47,7 @@ Feature: The visibility of fields control where they are displayed
     And I set the following fields to these values:
       | Name       | Test field  |
       | Short name | testfield   |
-      | Visible to | Not visible |
+      | Visible to | Nobody      |
     And I press "Save changes"
     And I log out
     When I log in as "teacher1"
@@ -67,7 +67,7 @@ Feature: The visibility of fields control where they are displayed
     And I set the following fields to these values:
       | Name       | Test field     |
       | Short name | testfield      |
-      | Visible to | Course editors |
+      | Visible to | Teachers       |
     And I press "Save changes"
     And I log out
     When I log in as "teacher1"
index 7520c64..ad77f90 100644 (file)
@@ -1264,6 +1264,58 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals('text/plain', $customdata['filedetails']['mimetype']);
     }
 
+    /**
+     * Test contents info is returned.
+     */
+    public function test_get_course_contents_contentsinfo() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $course = self::getDataGenerator()->create_course();
+
+        $record = new stdClass();
+        $record->course = $course->id;
+        // One resource with one file.
+        $resource1 = self::getDataGenerator()->create_module('resource', $record);
+
+        $timenow = time();
+        // More type of files.
+        $record->files = file_get_unused_draft_itemid();
+        $usercontext = context_user::instance($USER->id);
+        $extensions = array('txt', 'png', 'pdf');
+        foreach ($extensions as $key => $extension) {
+            // Add actual file there.
+            $filerecord = array('component' => 'user', 'filearea' => 'draft',
+                    'contextid' => $usercontext->id, 'itemid' => $record->files,
+                    'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
+            $fs = get_file_storage();
+            $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
+        }
+
+        $resource2 = self::getDataGenerator()->create_module('resource', $record);
+
+        $result = core_course_external::get_course_contents($course->id);
+        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
+        $this->assertCount(2, $result[0]['modules']);
+        foreach ($result[0]['modules'] as $module) {
+            if ($module['instance'] == $resource1->id) {
+                $this->assertEquals(1, $module['contentsinfo']['filescount']);
+                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
+                $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
+                $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
+            } else {
+                $this->assertEquals(count($extensions), $module['contentsinfo']['filescount']);
+                $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
+                    $module['contents'][2]['filesize'];
+                $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
+                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
+                $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
+            }
+        }
+    }
+
     /**
      * Test duplicate_course
      */
index c64ce2c..c5ada25 100644 (file)
@@ -41,7 +41,7 @@ class course_search_testcase extends advanced_testcase {
     /**
      * @var string Area id
      */
-    protected $mycoursesareaid = null;
+    protected $coursesareaid = null;
 
     /**
      * @var string Area id for sections
@@ -57,7 +57,7 @@ class course_search_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
         set_config('enableglobalsearch', true);
 
-        $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
+        $this->coursesareaid = \core_search\manager::generate_areaid('core_course', 'course');
         $this->sectionareaid = \core_search\manager::generate_areaid('core_course', 'section');
         $this->customfieldareaid = \core_search\manager::generate_areaid('core_course', 'customfield');
 
@@ -66,15 +66,15 @@ class course_search_testcase extends advanced_testcase {
     }
 
     /**
-     * Indexing my courses contents.
+     * Indexing courses contents.
      *
      * @return void
      */
-    public function test_mycourses_indexing() {
+    public function test_courses_indexing() {
 
         // Returns the instance as long as the area is supported.
-        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
-        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+        $searcharea = \core_search\manager::get_search_area($this->coursesareaid);
+        $this->assertInstanceOf('\core_course\search\course', $searcharea);
 
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
@@ -113,10 +113,10 @@ class course_search_testcase extends advanced_testcase {
     /**
      * Tests course indexing support for contexts.
      */
-    public function test_mycourses_indexing_contexts() {
+    public function test_courses_indexing_contexts() {
         global $DB, $USER, $SITE;
 
-        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $searcharea = \core_search\manager::get_search_area($this->coursesareaid);
 
         // Create some courses in categories, and a forum.
         $generator = $this->getDataGenerator();
@@ -194,11 +194,11 @@ class course_search_testcase extends advanced_testcase {
      *
      * @return void
      */
-    public function test_mycourses_document() {
+    public function test_courses_document() {
 
         // Returns the instance as long as the area is supported.
-        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
-        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+        $searcharea = \core_search\manager::get_search_area($this->coursesareaid);
+        $this->assertInstanceOf('\core_course\search\course', $searcharea);
 
         $user = self::getDataGenerator()->create_user();
         $course = self::getDataGenerator()->create_course();
@@ -207,7 +207,7 @@ class course_search_testcase extends advanced_testcase {
         $doc = $searcharea->get_document($course);
         $this->assertInstanceOf('\core_search\document', $doc);
         $this->assertEquals($course->id, $doc->get('itemid'));
-        $this->assertEquals($this->mycoursesareaid . '-' . $course->id, $doc->get('id'));
+        $this->assertEquals($this->coursesareaid . '-' . $course->id, $doc->get('id'));
         $this->assertEquals($course->id, $doc->get('courseid'));
         $this->assertFalse($doc->is_set('userid'));
         $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
@@ -224,10 +224,11 @@ class course_search_testcase extends advanced_testcase {
      *
      * @return void
      */
-    public function test_mycourses_access() {
+    public function test_courses_access() {
+        $this->resetAfterTest();
 
         // Returns the instance as long as the area is supported.
-        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $searcharea = \core_search\manager::get_search_area($this->coursesareaid);
 
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
@@ -244,13 +245,13 @@ class course_search_testcase extends advanced_testcase {
         $this->setUser($user1);
         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course2->id));
-        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course3->id));
         $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
 
         $this->setUser($user2);
         $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
-        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course3->id));
     }
 
     /**
@@ -538,10 +539,43 @@ class course_search_testcase extends advanced_testcase {
     }
 
     /**
-     * Test document icon for mycourse area.
+     * Document accesses for customfield area.
      */
-    public function test_get_doc_icon_for_mycourse_area() {
-        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+    public function test_customfield_access() {
+        $this->resetAfterTest();
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->customfieldareaid);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course(array('visible' => 0));
+        $course3 = self::getDataGenerator()->create_course();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
+
+        $this->setUser($user1);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course3->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
+
+        $this->setUser($user2);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course3->id));
+    }
+
+    /**
+     * Test document icon for course area.
+     */
+    public function test_get_doc_icon_for_course_area() {
+        $searcharea = \core_search\manager::get_search_area($this->coursesareaid);
 
         $document = $this->getMockBuilder('\core_search\document')
             ->disableOriginalConstructor()
@@ -573,7 +607,7 @@ class course_search_testcase extends advanced_testcase {
      * Test assigned search categories.
      */
     public function test_get_category_names() {
-        $coursessearcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $coursessearcharea = \core_search\manager::get_search_area($this->coursesareaid);
         $sectionsearcharea = \core_search\manager::get_search_area($this->sectionareaid);
 
         $this->assertEquals(['core-courses'], $coursessearcharea->get_category_names());
index f23582e..efa1b2f 100644 (file)
@@ -2,8 +2,10 @@ This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
 === 3.7 ===
+
  * External function core_course_external::get_course_contents new returns the following additional completiondata field:
    - valueused (indicates whether the completion state affects the availability of other content)
+ * External function core_course_external::get_course_contents now returns a new contentsinfo field with summary files information.
 
 === 3.6 ===
 
index 91bd63e..74a41d9 100644 (file)
@@ -24,7 +24,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['checkedbydefault'] = 'Checked by default';
-$string['errorconfigunique'] = 'Checkbox field can not be defined as unique';
+$string['errorconfigunique'] = 'The checkbox field cannot be defined as unique.';
 $string['pluginname'] = 'Checkbox';
-$string['privacy:metadata'] = 'Checkbox field type plugin does not store any personal data, it uses tables defined in core';
+$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';
index 27d0582..fd5b3cc 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['errormaxdate'] = 'Please enter date no later than {$a}';
-$string['errormindate'] = 'Please enter date on or after {$a}';
+$string['errormaxdate'] = 'Please enter a date no later than {$a}.';
+$string['errormindate'] = 'Please enter a date on or after {$a}.';
 $string['includetime'] = 'Include time';
 $string['maxdate'] = 'Maximum value';
 $string['mindate'] = 'Minimum value';
-$string['mindateaftermax'] = 'The minimum value can not be bigger than the maximum value';
+$string['mindateaftermax'] = 'The minimum value cannot be bigger than the maximum value.';
 $string['pluginname'] = 'Date and time';
-$string['privacy:metadata'] = 'Date and time field type plugin does not store any personal data, it uses tables defined in core';
+$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';
index 7e66d98..81dc118 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['errordefaultvaluenotinlist'] = 'Default value must be one of the options from the list above';
-$string['errornotenoughoptions'] = 'Please provide at least two options separated with a newline';
+$string['errordefaultvaluenotinlist'] = 'The default value must be one of the options from the list above.';
+$string['errornotenoughoptions'] = 'Please provide at least two options, with each on a new line.';
 $string['invalidoption'] = 'Invalid option selected';
 $string['menuoptions'] = 'Menu options (one per line)';
 $string['pluginname'] = 'Dropdown menu';
-$string['privacy:metadata'] = 'Dropdown menu field type plugin does not store any personal data, it uses tables defined in core';
+$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';
index 6c43de1..4641365 100644 (file)
@@ -71,7 +71,7 @@ Feature: Managers can manage course custom fields select
       | Name       | Test field |
       | Short name | testfield  |
     And I press "Save changes"
-    And I should see "Please provide at least two options separated with a newline" in the "Menu options (one per line)" "form_row"
+    And I should see "Please provide at least two options, with each on a new line." in the "Menu options (one per line)" "form_row"
     And I set the field "Menu options (one per line)" to multiline:
     """
     a
@@ -79,7 +79,7 @@ Feature: Managers can manage course custom fields select
     """
     And I set the field "Default value" to "c"
     And I press "Save changes"
-    And I should see "Default value must be one of the options from the list above" in the "Default value" "form_row"
+    And I should see "The default value must be one of the options from the list above" in the "Default value" "form_row"
     And I set the field "Default value" to "b"
     And I press "Save changes"
     And "testfield" "text" should exist in the "Test field" "table_row"
index 78962d7..c9e47a7 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 $string['displaysize'] = 'Form input size';
-$string['errorconfigdisplaysize'] = 'Form input size must be between 1 and 200 characters';
-$string['errorconfiglinkplaceholder'] = 'Link must contain placeholder $$';
-$string['errorconfiglinksyntax'] = 'Link must be a valid URL starting with either http:// or https://';
-$string['errorconfigmaxlen'] = 'Maximum length must be between 1 and 1333';
-$string['errormaxlength'] = 'This field maximum length is {$a}';
+$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['islink'] = 'Link field';
-$string['islink_help'] = 'To transform the text into a link, enter a URL containing $$, where $$ will be replaced with the text. For example, to transform a Twitter ID to a link, enter http://twitter.com/$$.';
+$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['newwindow'] = 'New window';
 $string['none'] = 'None';
 $string['pluginname'] = 'Text field';
-$string['privacy:metadata'] = 'Text field field type plugin does not store any personal data, it uses tables defined in core';
+$string['privacy:metadata'] = 'The Text field 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';
index d4bb571..7149604 100644 (file)
@@ -25,5 +25,5 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'Text area';
-$string['privacy:metadata'] = 'Text area field type plugin does not store any personal data, it uses tables defined in core';
+$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';
index 6d9f96c..247f5ce 100644 (file)
@@ -108,7 +108,7 @@ Feature: Teachers can edit course custom fields
     Then I should see "You must supply a value here" in the "Short name" "form_row"
     And I set the field "Short name" to "short name"
     And I press "Save changes"
-    And I should see "Short name can only contain lowercase latin letters, digits and an underscore sign" in the "Short name" "form_row"
+    And I should see "The short name can only contain alphanumeric lowercase characters and underscores (_)." in the "Short name" "form_row"
     And I set the field "Short name" to "f1"
     And I press "Save changes"
     And I should see "Short name already exists" in the "Short name" "form_row"
index 0f10bba..9ba48d2 100644 (file)
@@ -165,8 +165,13 @@ class core_enrol_external extends external_api {
                 $courseusers['capability'] = $capability;
 
                 list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, $capability, $groupid, $onlyactive);
+                $enrolledparams['courseid'] = $courseid;
 
-                $sql = "SELECT u.* FROM {user} u WHERE u.id IN ($enrolledsql) ORDER BY u.id ASC";
+                $sql = "SELECT u.*, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
+                          FROM {user} u
+                     LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid)
+                         WHERE u.id IN ($enrolledsql)
+                      ORDER BY u.id ASC";
 
                 $enrolledusers = $DB->get_recordset_sql($sql, $enrolledparams, $limitfrom, $limitnumber);
                 $users = array();
@@ -216,6 +221,7 @@ class core_enrol_external extends external_api {
                     'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
                     'firstaccess' => new external_value(PARAM_INT, 'first access to the site (0 if never)', VALUE_OPTIONAL),
                     'lastaccess'  => new external_value(PARAM_INT, 'last access to the site (0 if never)', VALUE_OPTIONAL),
+                    'lastcourseaccess'  => new external_value(PARAM_INT, 'last access to the course (0 if never)', VALUE_OPTIONAL),
                     'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
                     'descriptionformat' => new external_value(PARAM_INT, 'User profile description format', VALUE_OPTIONAL),
                     'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
@@ -280,6 +286,11 @@ class core_enrol_external extends external_api {
         return new external_function_parameters(
             array(
                 'userid' => new external_value(PARAM_INT, 'user id'),
+                'returnusercount' => new external_value(PARAM_BOOL,
+                        'Include count of enrolled users for each course? This can add several seconds to the response time'
+                            . ' if a user is on several large courses, so set this to false if the value will not be used to'
+                            . ' improve performance.',
+                        VALUE_DEFAULT, true),
             )
         );
     }
@@ -289,9 +300,10 @@ class core_enrol_external extends external_api {
      * Please note the current user must be able to access the course, otherwise the course is not included.
      *
      * @param int $userid
+     * @param bool $returnusercount
      * @return array of courses
      */
-    public static function get_users_courses($userid) {
+    public static function get_users_courses($userid, $returnusercount = true) {
         global $CFG, $USER, $DB;
 
         require_once($CFG->dirroot . '/course/lib.php');
@@ -299,8 +311,10 @@ class core_enrol_external extends external_api {
 
         // Do basic automatic PARAM checks on incoming data, using params description
         // If any problems are found then exceptions are thrown with helpful error messages
-        $params = self::validate_parameters(self::get_users_courses_parameters(), array('userid'=>$userid));
+        $params = self::validate_parameters(self::get_users_courses_parameters(),
+                ['userid' => $userid, 'returnusercount' => $returnusercount]);
         $userid = $params['userid'];
+        $returnusercount = $params['returnusercount'];
 
         $courses = enrol_get_users_courses($userid, true, '*');
         $result = array();
@@ -337,9 +351,11 @@ class core_enrol_external extends external_api {
                 continue;
             }
 
-            list($enrolledsqlselect, $enrolledparams) = get_enrolled_sql($context);
-            $enrolledsql = "SELECT COUNT('x') FROM ($enrolledsqlselect) enrolleduserids";
-            $enrolledusercount = $DB->count_records_sql($enrolledsql, $enrolledparams);
+            if ($returnusercount) {
+                list($enrolledsqlselect, $enrolledparams) = get_enrolled_sql($context);
+                $enrolledsql = "SELECT COUNT('x') FROM ($enrolledsqlselect) enrolleduserids";
+                $enrolledusercount = $DB->count_records_sql($enrolledsql, $enrolledparams);
+            }
 
             $displayname = external_format_string(get_course_display_name_for_list($course), $context->id);
             list($course->summary, $course->summaryformat) =
@@ -395,14 +411,13 @@ class core_enrol_external extends external_api {
                 );
             }
 
-            $result[] = array(
+            $courseresult = [
                 'id' => $course->id,
                 'shortname' => $course->shortname,
                 'fullname' => $course->fullname,
                 'displayname' => $displayname,
                 'idnumber' => $course->idnumber,
                 'visible' => $course->visible,
-                'enrolledusercount' => $enrolledusercount,
                 'summary' => $course->summary,
                 'summaryformat' => $course->summaryformat,
                 'format' => $course->format,
@@ -420,7 +435,11 @@ class core_enrol_external extends external_api {
                 'isfavourite' => isset($favouritecourseids[$course->id]),
                 'hidden' => $hidden,
                 'overviewfiles' => $overviewfiles,
-            );
+            ];
+            if ($returnusercount) {
+                $courseresult['enrolledusercount'] = $enrolledusercount;
+            }
+            $result[] = $courseresult;
         }
 
         return $result;
@@ -439,7 +458,8 @@ class core_enrol_external extends external_api {
                     'shortname' => new external_value(PARAM_RAW, 'short name of course'),
                     'fullname'  => new external_value(PARAM_RAW, 'long name of course'),
                     'displayname' => new external_value(PARAM_TEXT, 'course display name for lists.', VALUE_OPTIONAL),
-                    'enrolledusercount' => new external_value(PARAM_INT, 'Number of enrolled users in this course'),
+                    'enrolledusercount' => new external_value(PARAM_INT, 'Number of enrolled users in this course',
+                            VALUE_OPTIONAL),
                     'idnumber'  => new external_value(PARAM_RAW, 'id number of course'),
                     'visible'   => new external_value(PARAM_INT, '1 means visible, 0 means not yet visible course'),
                     'summary'   => new external_value(PARAM_RAW, 'summary', VALUE_OPTIONAL),
@@ -720,15 +740,18 @@ class core_enrol_external extends external_api {
                 return array();
             }
         }
-        $sql = "SELECT us.*
+        $sql = "SELECT us.*, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
                   FROM {user} us
                   JOIN (
                       SELECT DISTINCT u.id $ctxselect
                         FROM {user} u $ctxjoin $groupjoin
                        WHERE u.id IN ($enrolledsql)
                   ) q ON q.id = us.id
+             LEFT JOIN {user_lastaccess} ul ON (ul.userid = us.id AND ul.courseid = :courseid)
                 ORDER BY $sortby $sortdirection";
         $enrolledparams = array_merge($enrolledparams, $sortparams);
+        $enrolledparams['courseid'] = $courseid;
+
         $enrolledusers = $DB->get_recordset_sql($sql, $enrolledparams, $limitfrom, $limitnumber);
         $users = array();
         foreach ($enrolledusers as $user) {
@@ -771,6 +794,7 @@ class core_enrol_external extends external_api {
                     'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
                     'firstaccess' => new external_value(PARAM_INT, 'first access to the site (0 if never)', VALUE_OPTIONAL),
                     'lastaccess'  => new external_value(PARAM_INT, 'last access to the site (0 if never)', VALUE_OPTIONAL),
+                    'lastcourseaccess'  => new external_value(PARAM_INT, 'last access to the course (0 if never)', VALUE_OPTIONAL),
                     'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
                     'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
                     'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
index a346a99..2a95489 100644 (file)
@@ -109,7 +109,7 @@ $string['role_mapping_context'] = 'LDAP contexts for {$a}';
 $string['role_mapping_key'] = 'Map roles from LDAP ';
 $string['roles'] = 'Role mapping';
 $string['server_settings'] = 'LDAP server settings';
-$string['syncenrolmentstask'] = 'Synchronise enrolments task';
+$string['syncenrolmentstask'] = 'Synchronise LDAP enrolments task';
 $string['synccourserole'] = "== Synching course '{\$a->idnumber}' for role '{\$a->role_shortname}'\n";
 $string['template'] = 'Optional: auto-created courses can copy their settings from a template course';
 $string['template_key'] = 'Template';
index a28d07d..ca36639 100644 (file)
@@ -72,7 +72,7 @@ $string['status_desc'] = 'Allow course access of internally enrolled users. This
 $string['status_help'] = 'This setting determines whether users can be enrolled manually, via a link in the course administration settings, by a user with appropriate permissions such as a teacher.';
 $string['statusenabled'] = 'Enabled';
 $string['statusdisabled'] = 'Disabled';
-$string['syncenrolmentstask'] = 'Manual enrolment synchronise enrolments task';
+$string['syncenrolmentstask'] = 'Synchronise manual enrolments task';
 $string['unenrol'] = 'Unenrol user';
 $string['unenrolselectedusers'] = 'Unenrol selected users';
 $string['unenrolselfconfirm'] = 'Do you really want to unenrol yourself from course "{$a}"?';
index 509f04f..504b7ba 100644 (file)
@@ -110,7 +110,7 @@ $string['showhint_desc'] = 'Show first letter of the guest access key.';
 $string['status'] = 'Allow existing enrolments';
 $string['status_desc'] = 'Enable self enrolment method in new courses.';
 $string['status_help'] = 'If enabled together with \'Allow new enrolments\' disabled, only users who self enrolled previously can access the course. If disabled, this self enrolment method is effectively disabled, since all existing self enrolments are suspended and new users cannot self enrol.';
-$string['syncenrolmentstask'] = 'Self enrolment synchronise enrolments task';
+$string['syncenrolmentstask'] = 'Synchronise self enrolments task';
 $string['unenrol'] = 'Unenrol user';
 $string['unenrolselfconfirm'] = 'Do you really want to unenrol yourself from course "{$a}"?';
 $string['unenroluser'] = 'Do you really want to unenrol "{$a->user}" from course "{$a->course}"?';
index f945dc3..0fa3155 100644 (file)
@@ -425,7 +425,7 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
 
         $this->setUser($student);
         // Call the external function.
-        $enrolledincourses = core_enrol_external::get_users_courses($student->id);
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
@@ -455,6 +455,7 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertTrue($courseenrol['completionhascriteria']);
                 $this->assertTrue($courseenrol['hidden']);
                 $this->assertTrue($courseenrol['isfavourite']);
+                $this->assertEquals(2, $courseenrol['enrolledusercount']);
             } else {
                 // Check language pack. Should be empty since an incorrect one was used when creating the course.
                 $this->assertEmpty($courseenrol['lang']);
@@ -466,13 +467,21 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertFalse($courseenrol['completionhascriteria']);
                 $this->assertFalse($courseenrol['hidden']);
                 $this->assertFalse($courseenrol['isfavourite']);
+                $this->assertEquals(1, $courseenrol['enrolledusercount']);
             }
         }
 
+        // Check that returnusercount works correctly.
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, false);
+        $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
+        foreach ($enrolledincourses as $courseenrol) {
+            $this->assertFalse(isset($courseenrol['enrolledusercount']));
+        }
+
         // Now check that admin users can see all the info.
         $this->setAdminUser();
 
-        $enrolledincourses = core_enrol_external::get_users_courses($student->id);
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
         $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
         $this->assertEquals(2, count($enrolledincourses));
         foreach ($enrolledincourses as $courseenrol) {
@@ -493,7 +502,7 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         // Check other users can't see private info.
         $this->setUser($otherstudent);
 
-        $enrolledincourses = core_enrol_external::get_users_courses($student->id);
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
         $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
         $this->assertEquals(1, count($enrolledincourses));
 
@@ -502,7 +511,7 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
 
         // Change some global profile visibility fields.
         $CFG->hiddenuserfields = 'lastaccess';
-        $enrolledincourses = core_enrol_external::get_users_courses($student->id);
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
         $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
 
         $this->assertEquals(0, $enrolledincourses[0]['lastaccess']); // I can't see this, hidden by global setting.
@@ -656,6 +665,46 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         $this->assertArrayNotHasKey('email', $enrolledusers[0]);
     }
 
+
+    /**
+     * Test get_enrolled_users last course access.
+     */
+    public function test_get_enrolled_users_including_lastcourseaccess() {
+        global $DB;
+        $capability = 'moodle/course:viewparticipants';
+        $data = $this->get_enrolled_users_setup($capability);
+
+        // Call the external function.
+        $enrolledusers = core_enrol_external::get_enrolled_users($data->course->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $enrolledusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $enrolledusers);
+
+        // Check the result set.
+        $this->assertEquals(3, count($enrolledusers));
+        $this->assertArrayHasKey('email', $enrolledusers[0]);
+        $this->assertEquals(0, $enrolledusers[0]['lastcourseaccess']);
+        $this->assertEquals(0, $enrolledusers[1]['lastcourseaccess']);
+        $this->assertNotEquals(0, $enrolledusers[2]['lastcourseaccess']);   // We forced an access to the course via setUser.
+
+        // Force last access.
+        $timenow = time();
+        $lastaccess = array(
+            'userid' => $enrolledusers[0]['id'],
+            'courseid' => $data->course->id,
+            'timeaccess' => $timenow
+        );
+        $DB->insert_record('user_lastaccess', $lastaccess);
+
+        $enrolledusers = core_enrol_external::get_enrolled_users($data->course->id);
+        $enrolledusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $enrolledusers);
+
+        // Check the result set.
+        $this->assertEquals(3, count($enrolledusers));
+        $this->assertEquals($timenow, $enrolledusers[0]['lastcourseaccess']);
+        $this->assertEquals(0, $enrolledusers[1]['lastcourseaccess']);
+        $this->assertNotEquals(0, $enrolledusers[2]['lastcourseaccess']);
+    }
+
     /**
      * Test get_enrolled_users from core_enrol_external with capability to
      * viewparticipants removed.
@@ -772,6 +821,56 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($data->student1->id, $expecteduser['id']);
     }
 
+    /**
+     * Test get_enrolled_users last course access.
+     */
+    public function test_get_enrolled_users_with_capability_including_lastcourseaccess() {
+        global $DB;
+        $capability = 'moodle/course:viewparticipants';
+        $data = $this->get_enrolled_users_with_capability_setup($capability);
+
+        $parameters = array(
+            'coursecapabilities' => array(
+                'courseid' => $data->course->id,
+                'capabilities' => array(
+                    $capability,
+                ),
+            ),
+        );
+
+        $result = core_enrol_external::get_enrolled_users_with_capability($parameters, array());
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_with_capability_returns(), $result);
+
+        // Check an array containing the expected user for the course capability is returned.
+        $expecteduserlist = $result[0];
+        $this->assertEquals($data->course->id, $expecteduserlist['courseid']);
+        $this->assertEquals($capability, $expecteduserlist['capability']);
+        $this->assertEquals(2, count($expecteduserlist['users']));
+        // We forced an access to the course via setUser.
+        $this->assertNotEquals(0, $expecteduserlist['users'][0]['lastcourseaccess']);
+        $this->assertEquals(0, $expecteduserlist['users'][1]['lastcourseaccess']);
+
+        // Force last access.
+        $timenow = time();
+        $lastaccess = array(
+            'userid' => $expecteduserlist['users'][1]['id'],
+            'courseid' => $data->course->id,
+            'timeaccess' => $timenow
+        );
+        $DB->insert_record('user_lastaccess', $lastaccess);
+
+        $result = core_enrol_external::get_enrolled_users_with_capability($parameters, array());
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_with_capability_returns(), $result);
+
+        // Check the result set.
+        $expecteduserlist = $result[0];
+        $this->assertEquals(2, count($expecteduserlist['users']));
+        $this->assertNotEquals(0, $expecteduserlist['users'][0]['lastcourseaccess']);
+        $this->assertEquals($timenow, $expecteduserlist['users'][1]['lastcourseaccess']);
+    }
+
     /**
      * Test for core_enrol_external::edit_user_enrolment().
      */
index df24f1f..34c840d 100644 (file)
@@ -7,6 +7,8 @@ information provided here is intended especially for developers.
   - users: List of user objects returned by the query.
   - moreusers: True if there are still more users, otherwise is False.
   - totalusers: Number users matching the search. (This element only exists if the function is called with $returnexactcount param set to true).
+* enrolledusercount is now optional in the return value of get_users_courses() for performance reasons. This is controlled with the new
+  optional returnusercount parameter (default true).
 
 === 3.6 ===
 
@@ -20,6 +22,8 @@ information provided here is intended especially for developers.
   - completionhascriteria: Whether completion criteria is set for the course.
   - isfavourite: Whether the user marked the course as favourite.
   - hidden: Whether the user hide the course from the dashboard.
+* External functions core_enrol_external::get_enrolled_users and core_enrol_external::get_enrolled_users_with_capability now return
+  the last access time for the users in the given course.
 
 === 3.5 ===
 
index 5484ff5..f48feb7 100644 (file)
@@ -168,7 +168,7 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fp-reficons2"></div>
     </div>
     <div class="fp-filename-field">
-        <div class="fp-filename"></div>
+        <div class="fp-filename text-truncate"></div>
     </div>
     </a>
     <a class="fp-contextmenu" href="#">'.$this->pix_icon('i/menu', '▶').'</a>
@@ -335,7 +335,7 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fp-reficons2"></div>
     </div>
     <div class="fp-filename-field">
-        <p class="fp-filename"></p>
+        <p class="fp-filename text-truncate"></p>
     </div>
 </a>';
         return $rv;
index 98a18a4..ef6ada4 100644 (file)
@@ -211,6 +211,8 @@ class edit_category_form extends moodleform {
         }
         $mform->addElement('select', 'grade_item_display', get_string('gradedisplaytype', 'grades'), $options);
         $mform->addHelpButton('grade_item_display', 'gradedisplaytype', 'grades');
+        $mform->disabledIf('grade_item_display', 'grade_item_gradetype', 'in',
+            array(GRADE_TYPE_TEXT, GRADE_TYPE_NONE));
 
         $default_gradedecimals = grade_get_setting($COURSE->id, 'decimalpoints', $CFG->grade_decimalpoints);
         $options = array(-1=>get_string('defaultprev', 'grades', $default_gradedecimals), 0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5);
@@ -218,6 +220,8 @@ class edit_category_form extends moodleform {
         $mform->addHelpButton('grade_item_decimals', 'decimalpoints', 'grades');
         $mform->setDefault('grade_item_decimals', -1);
         $mform->disabledIf('grade_item_decimals', 'grade_item_display', 'eq', GRADE_DISPLAY_TYPE_LETTER);
+        $mform->disabledIf('grade_item_decimals', 'grade_item_gradetype', 'in',
+            array(GRADE_TYPE_TEXT, GRADE_TYPE_NONE));
 
         if ($default_gradedisplaytype == GRADE_DISPLAY_TYPE_LETTER) {
             $mform->disabledIf('grade_item_decimals', 'grade_item_display', "eq", GRADE_DISPLAY_TYPE_DEFAULT);
index 5dfb8e9..f762cc7 100644 (file)
@@ -166,6 +166,7 @@ class edit_item_form extends moodleform {
         }
         $mform->addElement('select', 'display', get_string('gradedisplaytype', 'grades'), $options);
         $mform->addHelpButton('display', 'gradedisplaytype', 'grades');
+        $mform->disabledIf('display', 'gradetype', 'eq', GRADE_TYPE_TEXT);
 
         $default_gradedecimals = grade_get_setting($COURSE->id, 'decimalpoints', $CFG->grade_decimalpoints);
         $options = array(-1=>get_string('defaultprev', 'grades', $default_gradedecimals), 0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5);
@@ -176,6 +177,7 @@ class edit_item_form extends moodleform {
         if ($default_gradedisplaytype == GRADE_DISPLAY_TYPE_LETTER) {
             $mform->disabledIf('decimals', 'display', "eq", GRADE_DISPLAY_TYPE_DEFAULT);
         }
+        $mform->disabledIf('decimals', 'gradetype', 'eq', GRADE_TYPE_TEXT);
 
         /// hiding
         if ($item->cancontrolvisibility) {
index c2edce9..42e7fbe 100644 (file)
@@ -548,12 +548,20 @@ function groups_delete_group($grouporid) {
         }
     }
 
+    $context = context_course::instance($group->courseid);
+
     // delete group calendar events
     $DB->delete_records('event', array('groupid'=>$groupid));
     //first delete usage in groupings_groups
     $DB->delete_records('groupings_groups', array('groupid'=>$groupid));
     //delete members
     $DB->delete_records('groups_members', array('groupid'=>$groupid));
+
+    // Delete any members in a conversation related to this group.
+    if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) {
+        \core_message\api::delete_all_conversation_data($conversation->id);
+    }
+
     //group itself last
     $DB->delete_records('groups', array('id'=>$groupid));
 
index 3286cd5..bffba6d 100644 (file)
@@ -31,7 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['clianswerno'] = 'n';
-$string['cliansweryes'] = 'y';
+$string['cliansweryes'] = 'd';
 $string['cliincorrectvalueerror'] = 'Pogreška, netočna vrijednost "{$a->value}" za "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Netočna vrijednost, pokušajte ponovno';
 $string['clitypevalue'] = 'unesite vrijednost';
index 5b3bc0e..2aa9085 100644 (file)
@@ -42,6 +42,7 @@ $string['cannotsavemd5file'] = 'Nije moguće pohraniti md5 datoteku';
 $string['cannotsavezipfile'] = 'Nije moguće pohraniti ZIP datoteku';
 $string['cannotunzipfile'] = 'Nije moguće otpakirati datoteku';
 $string['componentisuptodate'] = 'Komponenta je dostupna u svojoj najnovijoj inačici.';
+$string['dmlexceptiononinstall'] = '<p>Dogodila se pogreška baze podataka [{$a->errorcode}].<br />{$a->debuginfo}';
 $string['downloadedfilecheckfailed'] = 'Došlo je do pogreške pri provjeri preuzete datoteke';
 $string['invalidmd5'] = 'Neispravna md5 datoteka';
 $string['missingrequiredfield'] = 'Nedostaje neko obvezatno polje';
index 433a8e2..8fe4df3 100644 (file)
@@ -31,7 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['admindirname'] = 'Admin mapa';
-$string['availablelangs'] = 'Popis dostupnih jezika';
+$string['availablelangs'] = 'Popis dostupnih jezičnih paketa';
 $string['chooselanguagehead'] = 'Odaberite jezik';
 $string['chooselanguagesub'] = 'Molimo odaberite jezik instalacije. Ovaj jezik će biti korišten kao zadani jezik sustava, a kasnije to možete lagano promijeniti.';
 $string['clialreadyconfigured'] = 'Datoteka config.php već postoji. Upotrijebite naredbu admin/cli/install_database.php ako želite nastaviti instalaciju.';
@@ -68,7 +68,7 @@ $string['pathsroparentdataroot'] = 'Nije moguće zapisivati podatke u nadređenu
 $string['pathssubadmindir'] = 'Manji broj webhosting tvrtki koristi /admin kao posebni URL za Vaš pristup upravljanju vašim hosting paketom. Nažalost, to rezultira konfliktom sa standardnom lokacijom za Moodle administratorsku stranicu. Navedenu lokaciju unutar Moodle sustava možete preimenovati. Na primjer: <br /> <br /><em>moodleadmin</em><br /> <br />
 Ovo će promijeniti administratorsku poveznicu na Moodle sustavu u novu vrijednost.';
 $string['pathssubdataroot'] = 'Mora postojati mapa u koju Moodle može pohraniti prenešene datoteke. Korisnik pod kojim je pokrenut web server (obično \'nobody\' ili \'apache\') bi morao imati mogućnost čitanja/pisanja podataka u toj mapi, ali oni ne bi trebali biti dostupni direktno preko weba. Instalacijska skripta će pokušati stvoriti navedenu mapu ako ista ne postoji.';
-$string['pathssubdirroot'] = 'Puna putanja (PATH) do Moodle instalacije.';
+$string['pathssubdirroot'] = '<p>Puna putanja (PATH) do Moodle instalacije.</p>';
 $string['pathssubwwwroot'] = 'Unesite punu web adresu putem koje će se pristupati vašem Moodle sustavu.
 Moodle sustavu NIJE MOGUĆE pristupiti preko više URL-ova, odaberite onaj koji vam najviše odgovara.
 Ako vaš poslužitelj ima višestruke javne adrese, onda morate postaviti tzv. permanent redirect na sve osim ove adrese.
index 257381f..779a889 100644 (file)
@@ -143,7 +143,7 @@ $string['computedfromlogs'] = 'Computed from logs since {$a}.';
 $string['condifmodeditdefaults'] = 'Default values are used in the settings form when creating a new activity or resource.';
 $string['confeditorhidebuttons'] = 'Select the buttons that should be hidden in the HTML editor.';
 $string['configallowattachments'] = 'If enabled, emails sent from the site can have attachments, such as badges.';
-$string['configenableactivitychooser'] = 'The activity chooser is a dialog box with a short description of each activity and resource. If disabled, separate resource and activity dropdown menus are provided instead.';
+$string['configenableactivitychooser'] = 'The activity chooser is a dialog box with a short description of each activity and resource. If disabled, separate resource and activity drop-down menus are provided instead.';
 $string['configallcountrycodes'] = 'This is the list of countries that may be selected in various places, for example in a user\'s profile. If blank (the default) the list in countries.php in the standard English language pack is used. That is the list from ISO 3166-1. Otherwise, you can specify a comma-separated list of codes, for example \'GB,FR,ES\'. If you add new, non-standard codes here, you will need to add them to countries.php in \'en\' and your language pack.';
 $string['configallowassign'] = 'You can allow people who have the roles on the left side to assign some of the column roles to other people';
 $string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
@@ -301,7 +301,7 @@ $string['confignotifyloginthreshold'] = 'If notifications about failed logins ar
 $string['confignotloggedinroleid'] = 'Users who are not logged in to the site will be treated as if they have this role granted to them at the site context.  Guest is almost always what you want here, but you might want to create roles that are less or more restrictive.  Things like creating posts still require the user to log in properly.';
 $string['configopentogoogle'] = 'If you enable this setting, then Google will be allowed to enter your site as a Guest.  In addition, people coming in to your site via a Google search will automatically be logged in as a Guest.  Note that this only provides transparent access to courses that already allow guest access.';
 $string['configoverride'] = 'Defined in config.php';
-$string['configpasswordpolicy'] = 'Turning this on will make Moodle check user passwords against a valid password policy. Use the settings below to specify your policy (they will be ignored if you set this to \'No\').';
+$string['configpasswordpolicy'] = 'If enabled, user passwords will be checked against the password policy as specified in the settings below. Enabling the password policy will not affect existing users until they decide to, or are required to, change their password.';
 $string['configpasswordresettime'] = 'This specifies the amount of time people have to validate a password reset request before it expires. Usually 30 minutes is a good value.';
 $string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If you enter this, pages that display directory contents will run much faster for directories with a lot of files.';
 $string['configpathtophp'] = 'Path to PHP CLI. Probably something like /usr/bin/php. If you enter this, cron scripts can be executed from admin web interface.';
@@ -600,7 +600,7 @@ $string['getremoteaddrconf'] = 'Logged IP address source';
 $string['globalsearch'] = 'Global search';
 $string['globalsearchmanage'] = 'Manage global search';
 $string['groupenrolmentkeypolicy'] = 'Group enrolment key policy';
-$string['groupenrolmentkeypolicy_desc'] = 'Turning this on will make Moodle check group enrolment keys against a valid password policy.';
+$string['groupenrolmentkeypolicy_desc'] = 'If enabled, group enrolment keys will be checked against the password policy as specified in the settings above.';
 $string['googlemapkey3'] = 'Google Maps API V3 key';
 $string['googlemapkey3_help'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="https://developers.google.com/maps/documentation/javascript/tutorial#api_key" target="_blank">https://developers.google.com/maps/documentation/javascript/tutorial#api_key</a>';
 $string['gotofirst'] = 'Go to first missing string';
@@ -773,9 +773,11 @@ $string['mediapluginyoutube'] = 'Enable YouTube links filter';
 $string['messaging'] = 'Enable messaging system';
 $string['messagingallowemailoverride'] = 'Notification email override';
 $string['messagingallusers'] = 'Allow site-wide messaging';
+$string['messagingcategory'] = 'Messaging';
 $string['messagingdefaultpressenter'] = 'Use enter to send enabled by default';
 $string['messagingdeletereadnotificationsdelay'] = 'Delete read notifications';
 $string['messagingdeleteallnotificationsdelay'] = 'Delete all notifications';
+$string['messagingssettings'] = 'Messaging settings';
 $string['minpassworddigits'] = 'Digits';
 $string['minpasswordlength'] = 'Password length';
 $string['minpasswordlower'] = 'Lowercase letters';
@@ -1055,6 +1057,7 @@ $string['save'] = 'Save';
 $string['savechanges'] = 'Save changes';
 $string['scssinvalid'] = 'SCSS code is not valid, fails with: {$a}';
 $string['search'] = 'Search';
+$string['searchablecourses'] = 'Searchable courses';
 $string['searchallavailablecourses'] = 'Searchable courses';
 $string['searchallavailablecourses_off'] = 'Search within enrolled courses only';
 $string['searchallavailablecourses_on'] = 'Search within all courses the user can access';
@@ -1066,6 +1069,9 @@ $string['searchhideallcategory'] = 'Hide All results category';
 $string['searchhideallcategory_desc'] = 'If checked, the category with all results will be hidden on the search result screen.';
 $string['searchdefaultcategory'] = 'Default search category';
 $string['searchdefaultcategory_desc'] = 'Results from the selected search area category will be displayed by default.';
+$string['searchallavailablecoursesdesc'] = 'If set to search within enrolled courses only, course information (name and summary) and course content will only be searched in courses which the user is enrolled in. Otherwise, course information and course content will be searched in all courses which the user can access, such as courses with guest access enabled.';
+$string['searchincludeallcourses'] = 'Include all visible courses';
+$string['searchincludeallcourses_desc'] = 'If enabled, search results will include course information (name and summary) of courses which are visible to the user, even if they don\'t have access to the course content.';
 $string['searchalldeleted'] = 'All indexed contents have been deleted';
 $string['searchareaenabled'] = 'Search area enabled';
 $string['searchareadisabled'] = 'Search area disabled';
@@ -1195,7 +1201,7 @@ $string['task_logretention'] = 'Retention period';
 $string['task_logretention_desc'] = 'The maximum period that logs should be kept for. This setting interacts with the \'Retain runs\' setting: whichever is reached first will apply';
 $string['task_logretainruns'] = 'Retain runs';
 $string['task_logretainruns_desc'] = 'The number of runs of each task to retain. This setting interacts with the \'Retention period\' setting: whichever is reached first will apply.';
-$string['task_type:adhoc'] = 'Adhoc';
+$string['task_type:adhoc'] = 'Ad hoc';
 $string['task_type:scheduled'] = 'Scheduled';
 $string['task_result:failed'] = 'Fail';
 $string['task_stats:dbreads'] = '{$a} reads';
@@ -1246,6 +1252,14 @@ $string['taskstatscron'] = 'Background processing for statistics';
 $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['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_subject'] = '{$a}: test message';
+$string['testoutgoingmailconf_toemail'] = 'To email address';
 $string['themedesignermode'] = 'Theme designer mode';
 $string['themedesignermodewarning'] = 'Theme designer mode is enabled. This should not be enabled on production sites as it can significantly reduce performance.';
 $string['themelist'] = 'Theme list';
index 3c95526..c92b544 100644 (file)
@@ -37,7 +37,7 @@ $string['itemheading'] = '{$a->number} {$a->type} restriction';
 $string['item_unknowntype'] = 'These restrictions use a plugin which is no longer available (if it is okay to remove that restriction, delete it below)';
 $string['shown_individual'] = 'Displayed greyed-out if user does not meet this condition';
 $string['hide_verb'] = 'Click to hide';
-$string['show_verb'] = 'Click to show';
+$string['show_verb'] = 'Click to display greyed-out';
 $string['hidden_all'] = 'Hidden entirely if user does not meet conditions';
 $string['shown_all'] = 'Displayed greyed-out if user does not meet conditions';
 $string['label_multi'] = 'Required restrictions';
index dc1a66f..94894df 100644 (file)
@@ -102,7 +102,7 @@ $string['completionnotenabled'] = 'Completion is not enabled';
 $string['completionnotenabledforcourse'] = 'Completion is not enabled for this course';
 $string['completionnotenabledforsite'] = 'Completion is not enabled for this site';
 $string['completionondate'] = 'Date';
-$string['completionondatevalue'] = 'User must remain enrolled until';
+$string['completionondatevalue'] = 'Date when course will be marked as complete';
 $string['completionduration'] = 'Enrolment';
 $string['completionsettingslocked'] = 'Completion settings locked';
 $string['completionusegrade'] = 'Require grade';
index 20b2fcb..aba28b3 100644 (file)
@@ -28,12 +28,12 @@ $string['aria:courseshortname'] = 'Course short name';
 $string['aria:coursename'] = 'Course name';
 $string['aria:favourite'] = 'Course is starred';
 $string['customfield_islocked'] = 'Locked';
-$string['customfield_islocked_help'] = 'When the field is locked only managers with capability "Modify locked fields" will be able to change it in the course editing form';
-$string['customfield_notvisible'] = 'Not visible';
+$string['customfield_islocked_help'] = 'If the field is locked, only users with the capability to change locked custom fields (by default users with the default role of manager only) will be able to change it in the course settings.';
+$string['customfield_notvisible'] = 'Nobody';
 $string['customfield_visibility'] = 'Visible to';
-$string['customfield_visibility_help'] = 'Who should be able able to see the data in the course listing';
+$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'] = 'Course editors';
+$string['customfield_visibletoteachers'] = 'Teachers';
 $string['customfieldsettings'] = 'Settings for course custom fields';
 $string['favourite'] = 'Starred course';
 $string['privacy:perpage'] = 'The number of courses to show per page.';
index 73d3608..a916c31 100644 (file)
@@ -30,15 +30,15 @@ $string['categorynotfound'] = 'Category not found';
 $string['checked'] = 'Checked';
 $string['commonsettings'] = 'Common settings';
 $string['componentsettings'] = 'Component settings';
-$string['confirmdeletecategory'] = 'Are you sure you want to delete this category? All fields inside this category will also be deleted and all data associated with them. This action can not be undone.';
-$string['confirmdeletefield'] = 'Are you sure you want to delete this field? All associated data will also be deleted. This operation can not be undone.';
+$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.';
 $string['createnewcustomfield'] = 'Add a new custom field';
 $string['customfield'] = 'Custom field';
 $string['customfielddata'] = 'Custom fields data';
 $string['customfields'] = 'Custom fields';
 $string['defaultvalue'] = 'Default value';
 $string['description'] = 'Description';
-$string['description_help'] = 'Description will be displayed in the form under the field';
+$string['description_help'] = 'The description is displayed in the form below the field.';
 $string['edit'] = 'Edit';
 $string['editcategoryname'] = 'Edit category name';
 $string['editingfield'] = 'Updating {$a}';
@@ -54,7 +54,7 @@ $string['fieldname'] = 'Name';
 $string['fieldnotfound'] = 'Field not found';
 $string['fieldshortname'] = 'Short name';
 $string['formfieldcheckshortname'] = 'Short name already exists';
-$string['invalidshortnameerror'] = 'Short name can only contain lowercase latin letters, digits and an underscore sign';
+$string['invalidshortnameerror'] = 'The short name can only contain alphanumeric lowercase characters and underscores (_).';
 $string['isdataunique'] = 'Unique data';
 $string['isdataunique_help'] = 'Should the data be unique?';
 $string['isfieldrequired'] = 'Required';
@@ -71,13 +71,13 @@ $string['notchecked'] = 'Not checked';
 $string['otherfields'] = 'Other fields';
 $string['otherfieldsn'] = 'Other fields {$a}';
 $string['privacy:metadata:customfield_data'] = 'Represents custom field data saved to a context';
-$string['privacy:metadata:customfield_data:charvalue'] = 'Data value, when it is a char';
+$string['privacy:metadata:customfield_data:charvalue'] = 'Data value when it is a character';
 $string['privacy:metadata:customfield_data:contextid'] = 'The ID of the context where the data was saved';
 $string['privacy:metadata:customfield_data:decvalue'] = 'Data value, when it is a decimal ';
 $string['privacy:metadata:customfield_data:fieldid'] = 'Field definition ID';
 $string['privacy:metadata:customfield_data:instanceid'] = 'Instance ID related to the data';
 $string['privacy:metadata:customfield_data:intvalue'] = 'Data value, when it is an integer';
-$string['privacy:metadata:customfield_data:shortcharvalue'] = 'Data value, when it is a short char';
+$string['privacy:metadata:customfield_data:shortcharvalue'] = 'Data value when it is a short character';
 $string['privacy:metadata:customfield_data:timecreated'] = 'Time when data was created';
 $string['privacy:metadata:customfield_data:timemodified'] = 'Time when data was last modified';
 $string['privacy:metadata:customfield_data:value'] = 'Data value, when it is a text';
@@ -85,11 +85,11 @@ $string['privacy:metadata:customfield_data:valueformat'] = 'The format of the va
 $string['privacy:metadata:customfieldpluginsummary'] = 'Fields for various components';
 $string['privacy:metadata:filepurpose'] = 'File attached to the custom field data';
 $string['shortname'] = 'Short name';
-$string['shortname_help'] = 'Custom field short name is required, must be unique and can only contain latin letters, digits and undescore sign. It is not displayed to the users but may be used for synchronisation with external systems, in web services and APIs';
+$string['shortname_help'] = 'The short name must be unique and can only contain alphanumeric lowercase characters and underscores (_). It is not displayed anywhere on the site, but may be used for synchronisation with external systems or in web services.';
 $string['showdate'] = 'Show date';
 $string['specificsettings'] = 'Specific settings';
 $string['therearenofields'] = 'There are no fields in this category';
 $string['totopofcategory'] = 'To the top of category {$a}';
 $string['type'] = 'Type';
-$string['unknownhandler'] = 'Unable to find handler for custom fields for component {$a->component} and area {$a->area}';
+$string['unknownhandler'] = 'Unable to find handler for custom fields for component {$a->component} and area {$a->area}.';
 $string['yes'] = 'Yes';
index c5bba12..951f257 100644 (file)
@@ -140,4 +140,10 @@ eventmessagecontactunblocked,core_message
 userisblockingyou,core_message
 userisblockingyounoncontact,core_message
 error:invalidbadgeurl,core_badges
-nomessages,core_message
\ No newline at end of file
+nomessages,core_message
+searchallavailablecourses_desc,core_admin
+search:mycourse,core_search
+outputdisabled,core_message
+outputdoesnotexist,core_message
+outputenabled,core_message
+outputnotconfigured,core_message
index 2317fb2..d6c2df6 100644 (file)
@@ -441,7 +441,7 @@ $string['nopermissiontomanagegroup'] = 'You do not have the required permissions
 $string['nopermissiontorate'] = 'Rating of items not allowed!';
 $string['nopermissiontoshow'] = 'No permission to see this!';
 $string['nopermissiontounlock'] = 'No permission to unlock!';
-$string['nopermissiontoupdatecalendar'] = 'Sorry, but you do not currently have permissions to update a calendar event.';
+$string['nopermissiontoupdatecalendar'] = 'Sorry, but you do not have permission to update the calendar event.';
 $string['nopermissiontoviewgrades'] = 'Can not view grades.';
 $string['nopermissiontoviewletergrade'] = 'Missing permission to view letter grades';
 $string['nopermissiontoviewpage'] = 'You are not allowed to look at this page';
index 8bea3d3..23747eb 100644 (file)
@@ -118,7 +118,7 @@ $string['categoryname'] = 'Category name';
 $string['categorytotal'] = 'Category total';
 $string['categorytotalname'] = 'Category total name';
 $string['categorytotalfull'] = '{$a->category} total';
-$string['combo'] = 'Tabs and Dropdown menu';
+$string['combo'] = 'Tabs and drop-down menu';
 $string['compact'] = 'Compact';
 $string['componentcontrolsvisibility'] = 'Whether this grade item is hidden is controlled by the activity settings.';
 $string['contract'] = 'Contract category';
@@ -153,7 +153,7 @@ $string['displaylettergrade'] = 'Display letter grades';
 $string['displaypercent'] = 'Display percents';
 $string['displaypoints'] = 'Display points';
 $string['displayweighted'] = 'Display weighted grades';
-$string['dropdown'] = 'Dropdown menu';
+$string['dropdown'] = 'Drop-down menu';
 $string['droplow'] = 'Drop the lowest';
 $string['droplow_help'] = 'This setting enables a specified number of the lowest grades to be excluded from the aggregation.';
 $string['droplowestvalue'] = 'Set drop lowest grade value';
@@ -266,9 +266,9 @@ $string['gradedisplay'] = 'Grade display';
 $string['gradedisplaytype'] = 'Grade display type';
 $string['gradedisplaytype_help'] = 'This setting determines how grades are displayed in the grader and user reports.
 
-* Real - Actual grades
-* Percentage
-* Letter - Letters or words are used to represent a range of grades';
+* Letter - Letters or words are used to represent a range of grades, as defined in \'Letters\' in the gradebook setup
+* Percentage - Relative to maximum and minimum grades
+* Real - Actual grades or scale values';
 $string['gradedon'] = 'Graded: {$a}';
 $string['gradeexport'] = 'Grade export';
 $string['gradeexportcolumntype'] = '{$a->name} ({$a->extra})';
@@ -484,7 +484,7 @@ $string['missingitemtypeoreid'] = 'Array key (itemtype or eid) missing from 2nd
 $string['missingscale'] = 'Scale must be selected';
 $string['mode'] = 'Mode';
 $string['modgrade'] = 'Grade';
-$string['modgrade_help'] = 'Select the type of grading used for this activity. If "scale" is chosen, you can then choose the scale from the "scale" dropdown. If using "point" grading, you can then enter the maximum grade available for this activity.';
+$string['modgrade_help'] = 'Select the type of grading used for this activity. If \'scale\' is chosen, you can then choose the scale from the drop-down menu. If using point grading, you can then enter the maximum grade available for this activity.';
 $string['modgradecantchangegradetype'] = 'You cannot change the type, as grades already exist for this item.';
 $string['modgradecantchangegradetypemsg'] = 'Some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades.';
 $string['modgradecantchangegradetyporscalemsg'] = 'Some grades have already been awarded, so the grade type and scale cannot be changed.';
index aed3fcb..9d448ec 100644 (file)
@@ -140,7 +140,7 @@ $string['importgroups_help'] = 'Groups may be imported via text file. The format
 * Each record is a series of data separated by commas
 * The first record contains a list of fieldnames defining the format of the rest of the file
 * Required fieldname is groupname
-* Optional fieldnames are description, enrolmentkey, picture, hidepicture';
+* Optional fieldnames are groupidnumber, description, enrolmentkey, groupingname, enablemessaging';
 $string['importgroups_link'] = 'group/import';
 $string['includeonlyactiveenrol'] = 'Include only active enrolments';
 $string['includeonlyactiveenrol_help'] = 'If enabled, suspended users will not be included in groups.';
index 2e2bcf7..92af304 100644 (file)
@@ -29,6 +29,10 @@ $string['advertised'] = 'For people to join';
 $string['advertiseon'] = 'Share this course on {$a}';
 $string['readvertiseon'] = 'Update advertising information on {$a}';
 $string['advertisepublication_help'] = 'This course will be listed on Moodle.net as a course that people can enrol in and participate. Email-based self-registration should be enabled on the site and you need to enable self enrolment in this course.';
+$string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})';
+$string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})';
+$string['analyticsenabledmodels'] = 'Number of enabled prediction models ({$a})';
+$string['analyticspredictions'] = 'Number of generated predictions ({$a})';
 $string['audience'] = 'Audience';
 $string['audience_help'] = 'Select the intended audience for the course.';
 $string['audienceeducators'] = 'Educators';
index 38c9faa..b00647f 100644 (file)
@@ -47,7 +47,7 @@ $string['contactrequests'] = 'Contact requests';
 $string['contactrequestsent'] = 'Contact request sent';
 $string['contacts'] = 'Contacts';
 $string['decline'] = 'Decline';
-$string['defaultmessageoutputs'] = 'Default message outputs';
+$string['defaultmessageoutputs'] = 'Notification settings';
 $string['defaults'] = 'Defaults';
 $string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.";
 $string['deleteallmessages'] = "Delete all messages";
@@ -90,8 +90,8 @@ $string['loggedindescription'] = 'When you are logged into Moodle';
 $string['loggedoff'] = 'Offline';
 $string['loggedoff_help'] = 'Configure how you would like to receive notifications when you are not logged into Moodle';
 $string['loggedoffdescription'] = 'When you are not logged into Moodle';
-$string['managemessageoutputs'] = 'Manage message outputs';
-$string['messageoutputs'] = 'Message outputs';
+$string['managemessageoutputs'] = 'Default notification preferences';
+$string['messageoutputs'] = 'Notification plugins';
 $string['messagepreferences'] = 'Message preferences';
 $string['message'] = 'Message';
 $string['messagecontactrequestsnotification'] = '{$a} wants to be added as a contact';
@@ -107,6 +107,7 @@ $string['messagepreferences'] = 'Message preferences';
 $string['messages'] = 'Messages';
 $string['messagesselected:'] = 'Messages selected:';
 $string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.';
+$string['muteconversation'] = 'Mute';
 $string['newonlymsg'] = 'Show only new';
 $string['newmessage'] = 'New message';
 $string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
@@ -135,11 +136,7 @@ $string['offline'] = 'Offline';
 $string['on'] = 'On';
 $string['online'] = 'Online';
 $string['otherparticipants'] = 'Other participants';
-$string['outputdisabled'] = 'Output disabled';
-$string['outputdoesnotexist'] = 'Message output does not exist';
-$string['outputenabled'] = 'Output enabled';
 $string['outputnotavailable'] = 'Not available';
-$string['outputnotconfigured'] = 'Not configured';
 $string['participants'] = 'Participants';
 $string['permitted'] = 'Permitted';
 $string['privacy'] = 'Privacy';
@@ -162,6 +159,11 @@ $string['privacy:metadata:message_contact_requests'] = 'The list of contact requ
 $string['privacy:metadata:message_contact_requests:requesteduserid'] = 'The ID of the user who received the contact request';
 $string['privacy:metadata:message_contact_requests:timecreated'] = 'The time when the contact request was created';
 $string['privacy:metadata:message_contact_requests:userid'] = 'The ID of the user who sent the contact request';
+$string['privacy:metadata:message_conversation_actions'] = 'The list of conversation user actions';
+$string['privacy:metadata:message_conversation_actions:action'] = 'The action that was performed';
+$string['privacy:metadata:message_conversation_actions:conversationid'] = 'The ID of the conversation this action belongs to';
+$string['privacy:metadata:message_conversation_actions:timecreated'] = 'The time when the action was created';
+$string['privacy:metadata:message_conversation_actions:userid'] = 'The ID of the user who performed this action';
 $string['privacy:metadata:message_conversation_members'] = 'The list of users in a conversation';
 $string['privacy:metadata:message_conversation_members:conversationid'] = 'The ID of the conversation';
 $string['privacy:metadata:message_conversation_members:timecreated'] = 'The time when the member was created';
@@ -233,6 +235,7 @@ $string['unblockcontact'] = 'Unblock contact';
 $string['unblockuser'] = 'Unblock user';
 $string['unblockuserconfirm'] = 'Are you sure you want to unblock {$a}?';
 $string['unknownuser'] = 'Unknown user';
+$string['unmuteconversation'] = 'Unmute';
 $string['unreadnotification'] = 'Unread notification: {$a}';
 $string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
 $string['unreadnewmessage'] = 'New message from {$a}';
@@ -248,7 +251,7 @@ $string['viewunreadmessageswith'] = 'View unread messages with {$a}';
 $string['writeamessage'] = 'Write a message...';
 $string['wouldliketocontactyou'] = 'Would like to contact you';
 $string['you'] = 'You:';
-$string['youhaveblockeduser'] = 'You have blocked this user in the past';
+$string['youhaveblockeduser'] = 'You have blocked this user.';
 $string['yourcontactrequestpending'] = 'Your contact request is pending with {$a}';
 
 // Deprecated since Moodle 3.6.
@@ -260,3 +263,7 @@ $string['userisblockingyounoncontact'] = '{$a} only accepts messages from their
 
 // Deprecated since Moodle 3.7.
 $string['nomessages'] = 'No messages';
+$string['outputdisabled'] = 'Output disabled';
+$string['outputdoesnotexist'] = 'Message output does not exist';
+$string['outputenabled'] = 'Output enabled';
+$string['outputnotconfigured'] = 'Not configured';
index 7a93fd4..e42d025 100644 (file)
@@ -1178,7 +1178,7 @@ $string['maximumshort'] = 'Max';
 $string['maximumupload'] = 'Maximum upload size';
 $string['maximumupload_help'] = 'This setting determines the largest size of file that can be uploaded to the course, limited by the site-wide setting set by an administrator. Activity modules also include a maximum upload size setting for further restricting the file size.';
 $string['maxnumberweeks'] = 'Maximum number of sections';
-$string['maxnumberweeks_desc'] = 'The maximum value in the number of sections dropdown menu (applies to certain course formats only).';
+$string['maxnumberweeks_desc'] = 'The maximum value in the number of sections drop-down menu (applies to certain course formats only).';
 $string['maxnumcoursesincombo'] = 'Browse <a href="{$a->link}">{$a->numberofcourses} courses</a>.';
 $string['maxsize'] = 'Max size: {$a}';
 $string['maxsizeandareasize'] = 'Maximum size for new files: {$a->size}, overall limit: {$a->areasize}';
@@ -1808,6 +1808,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['setcategorytheme'] = 'Set category theme';
 $string['setpassword'] = 'Set password';
 $string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
index 2f3322b..c0d4da1 100644 (file)
@@ -152,8 +152,8 @@ $string['type_local'] = 'Local plugin';
 $string['type_local_plural'] = 'Local plugins';
 $string['type_media'] = 'Media player';
 $string['type_media_plural'] = 'Media players';
-$string['type_message'] = 'Messaging output';
-$string['type_message_plural'] = 'Messaging outputs';
+$string['type_message'] = 'Notification plugin';
+$string['type_message_plural'] = 'Notification plugins';
 $string['type_mnetservice'] = 'MNet service';
 $string['type_mnetservice_plural'] = 'MNet services';
 $string['type_mod'] = 'Activity module';
index 0459dc4..d9df15e 100644 (file)
@@ -387,7 +387,9 @@ $string['partiallycorrectfeedbackdefault'] = 'Your answer is partially correct.'
 $string['penaltyforeachincorrecttry'] = 'Penalty for each incorrect try';
 $string['penaltyforeachincorrecttry_help'] = 'When questions are run using the \'Interactive with multiple tries\' or \'Adaptive mode\' behaviour, so that the student will have several tries to get the question right, then this option controls how much they are penalised for each incorrect try.
 
-The penalty is a proportion of the total question grade, so if the question is worth three marks, and the penalty is 0.3333333, then the student will score 3 if they get the question right first time, 2 if they get it right second try, and 1 of they get it right on the third try.';
+The penalty is a proportion of the total question grade, so if the question is worth three marks, and the penalty is 0.3333333, then the student will score 3 if they get the question right first time, 2 if they get it right second try, and 1 of they get it right on the third try.
+
+For some multi-part questions this scoring logic is applied separately to each part of the question. The details depend on the question type and can be complicated, but the principle is to give students credit for the knowledge they have demonstrated as fairly as possible.';
 $string['previewquestion'] = 'Preview question: {$a}';
 $string['privacy:metadata:database:question'] = 'The details about a specific question.';
 $string['privacy:metadata:database:question:createdby'] = 'The person who created the question.';
@@ -414,9 +416,9 @@ $string['privacy:metadata:link:qformat'] = 'The Question subsystem makes use of
 $string['privacy:metadata:link:qtype'] = 'The Question subsystem interacts with the Question Type plugintype which contains the different types of questions.';
 $string['questionbehaviouradminsetting'] = 'Question behaviour settings';
 $string['questionbehavioursdisabled'] = 'Question behaviours to disable';
-$string['questionbehavioursdisabledexplained'] = 'Enter a comma separated list of behaviours you do not want to appear in dropdown menu';
+$string['questionbehavioursdisabledexplained'] = 'Enter a comma-separated list of behaviours you do not want to appear in the drop-down menu.';
 $string['questionbehavioursorder'] = 'Question behaviours order';
-$string['questionbehavioursorderexplained'] = 'Enter a comma separated list of behaviours in the order you want them to appear in dropdown menu';
+$string['questionbehavioursorderexplained'] = 'Enter a comma-separated list of behaviours in the order you want them to appear in the drop-down menu.';
 $string['questionidmismatch'] = 'Question ids mismatch';
 $string['questionformtagheader'] = '{$a} tags';
 $string['questionname'] = 'Question name';
index 3d80c1f..79fea6d 100644 (file)
@@ -161,7 +161,7 @@ $string['course:viewsuspendedusers'] = 'View suspended users';
 $string['course:changecategory'] = 'Change course category';
 $string['course:changefullname'] = 'Change course full name';
 $string['course:changeidnumber'] = 'Change course ID number';
-$string['course:changelockedcustomfields'] = 'Modify locked custom fields';
+$string['course:changelockedcustomfields'] = 'Change locked custom fields';
 $string['course:changeshortname'] = 'Change course short name';
 $string['course:changesummary'] = 'Change course summary';
 $string['course:configurecustomfields'] = 'Configure custom fields';
@@ -492,4 +492,4 @@ $string['privacy:metadata:role_capabilities:roleid'] = 'The ID of the role';
 $string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabilities and override capabilities for a particular role in a particular context';
 $string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
 $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
-$string['course:togglecompletion'] = 'Allow users to manually complete activities';
+$string['course:togglecompletion'] = 'Manually mark activities as complete';
index 9bf7ce8..7d791e3 100644 (file)
@@ -86,6 +86,7 @@ $string['invalidindexerror'] = 'Index directory either contains an invalid index
 $string['ittook'] = 'It took';
 $string['matchingfile'] = 'Matched from file <span class="filename">{$a}</span>';
 $string['matchingfiles'] = 'Matched from files:';
+$string['mycoursesonly'] = 'My courses only';
 $string['next'] = 'Next';
 $string['noindexmessage'] = 'Admin: There appears to be no search index. Please';
 $string['noresults'] = 'No results';
@@ -113,6 +114,7 @@ $string['search'] = 'Search';
 $string['search:message_received'] = 'Messages - received';
 $string['search:message_sent'] = 'Messages - sent';
 $string['search:mycourse'] = 'My courses';
+$string['search:course'] = 'Courses';
 $string['search:section'] = 'Course sections';
 $string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
index 279943f..ad1960b 100644 (file)
@@ -349,6 +349,7 @@ function get_role_definitions_uncached(array $roleids) {
     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
               FROM {role_capabilities} rc
               JOIN {context} ctx ON rc.contextid = ctx.id
+              JOIN {capabilities} cap ON rc.capability = cap.name
              WHERE rc.roleid $sql";
     $rs = $DB->get_recordset_sql($sql, $params);
 
@@ -1191,7 +1192,17 @@ function is_safe_capability($capability) {
  */
 function get_local_override($roleid, $contextid, $capability) {
     global $DB;
-    return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
+
+    return $DB->get_record_sql("
+        SELECT rc.*
+          FROM {role_capabilities} rc
+          JOIN {capability} cap ON rc.capability = cap.name
+         WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
+            'roleid' => $roleid,
+            'contextid' => $contextid,
+            'capability' => $capability,
+
+        ]);
 }
 
 /**
@@ -1335,6 +1346,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
         $context = context::instance_by_id($contextid);
     }
 
+    // Capability must exist.
+    if (!$capinfo = get_capability_info($capability)) {
+        throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
+    }
+
     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
         unassign_capability($capability, $roleid, $context->id);
         return true;
@@ -1380,6 +1396,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
 function unassign_capability($capability, $roleid, $contextid = null) {
     global $DB;
 
+    // Capability must exist.
+    if (!$capinfo = get_capability_info($capability)) {
+        throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
+    }
+
     if (!empty($contextid)) {
         if ($contextid instanceof context) {
             $context = $contextid;
@@ -1434,6 +1455,7 @@ function get_roles_with_capability($capability, $permission = null, $context = n
               FROM {role} r
              WHERE r.id IN (SELECT rc.roleid
                               FROM {role_capabilities} rc
+                              JOIN {capabilities} cap ON rc.capability = cap.name
                              WHERE rc.capability = :capname
                                    $contextsql
                                    $permissionsql)";
@@ -2238,6 +2260,9 @@ function update_capabilities($component = 'moodle') {
 
         $DB->insert_record('capabilities', $capability, false);
 
+        // Flush the cached, as we have changed DB.
+        cache::make('core', 'capabilities')->delete('core_capabilities');
+
         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
                 foreach ($rolecapabilities as $rolecapability){
@@ -2291,9 +2316,6 @@ function capabilities_cleanup($component, $newcapdef = null) {
             if (empty($newcapdef) ||
                         array_key_exists($cachedcap->name, $newcapdef) === false) {
 
-                // Remove from capabilities cache.
-                $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
-                $removedcount++;
                 // Delete from roles.
                 if ($roles = get_roles_with_capability($cachedcap->name)) {
                     foreach($roles as $role) {
@@ -2302,6 +2324,13 @@ function capabilities_cleanup($component, $newcapdef = null) {
                         }
                     }
                 }
+
+                // Remove from role_capabilities for any old ones.
+                $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
+
+                // Remove from capabilities cache.
+                $DB->delete_records('capabilities', array('name' => $cachedcap->name));
+                $removedcount++;
             } // End if.
         }
     }
@@ -2366,10 +2395,12 @@ function role_context_capabilities($roleid, context $context, $cap = '') {
     }
 
     $sql = "SELECT rc.*
-              FROM {role_capabilities} rc, {context} c
+              FROM {role_capabilities} rc
+              JOIN {context} c ON rc.contextid = c.id
+              JOIN {capabilities} cap ON rc.capability = cap.name
              WHERE rc.contextid in $contexts
                    AND rc.roleid = ?
-                   AND rc.contextid = c.id $search
+                   $search
           ORDER BY c.contextlevel DESC, rc.capability DESC";
 
     $capabilities = array();
@@ -3108,6 +3139,7 @@ function get_switchable_roles(context $context) {
         SELECT r.id, r.name, r.shortname, rn.name AS coursealias
           FROM (SELECT DISTINCT rc.roleid
                   FROM {role_capabilities} rc
+
                   $extrajoins
                   $extrawhere) idlist
           JOIN {role} r ON r.id = idlist.roleid
@@ -3419,11 +3451,23 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
     $defs = array();
     list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
     list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap');
-    $params = array_merge($params, $params2);
+
+    // Check whether context locking is enabled.
+    // Filter out any write capability if this is the case.
+    $excludelockedcaps = '';
+    $excludelockedcapsparams = [];
+    if (!empty($CFG->contextlocking) && $context->locked) {
+        $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
+        $excludelockedcapsparams['capread'] = 'read';
+        $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
+    }
+
+    $params = array_merge($params, $params2, $excludelockedcapsparams);
     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
               FROM {role_capabilities} rc
+              JOIN {capabilities} cap ON rc.capability = cap.name
               JOIN {context} ctx on rc.contextid = ctx.id
-             WHERE rc.contextid $incontexts AND rc.capability $incaps";
+             WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
 
     $rcs = $DB->get_records_sql($sql, $params);
     foreach ($rcs as $rc) {
@@ -4487,6 +4531,7 @@ function get_roles_with_cap_in_context($context, $capability) {
     $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
               FROM {role_capabilities} rc
               JOIN {context} ctx ON ctx.id = rc.contextid
+              JOIN {capabilities} cap ON rc.capability = cap.name
              WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
           ORDER BY rc.roleid ASC, ctx.depth DESC";
     $params = array('cap'=>$capability);
@@ -4606,6 +4651,7 @@ function prohibit_is_removable($roleid, context $context, $capability) {
     $sql = "SELECT ctx.id
               FROM {role_capabilities} rc
               JOIN {context} ctx ON ctx.id = rc.contextid
+              JOIN {capabilities} cap ON rc.capability = cap.name
              WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
           ORDER BY ctx.depth DESC";
 
@@ -4648,6 +4694,7 @@ function role_change_permission($roleid, $context, $capname, $permission) {
     $sql = "SELECT ctx.id, rc.permission, ctx.depth
               FROM {role_capabilities} rc
               JOIN {context} ctx ON ctx.id = rc.contextid
+              JOIN {capabilities} cap ON rc.capability = cap.name
              WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
           ORDER BY ctx.depth DESC";
 
@@ -7521,12 +7568,23 @@ function get_with_capability_join(context $context, $capability, $useridcolumn)
 
     list($incaps, $capsparams) = $DB->get_in_or_equal($capability, SQL_PARAMS_NAMED, 'cap');
 
+    // Check whether context locking is enabled.
+    // Filter out any write capability if this is the case.
+    $excludelockedcaps = '';
+    $excludelockedcapsparams = [];
+    if (!empty($CFG->contextlocking) && $context->locked) {
+        $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
+        $excludelockedcapsparams['capread'] = 'read';
+        $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
+    }
+
     $defs = array();
     $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
               FROM {role_capabilities} rc
+              JOIN {capabilities} cap ON rc.capability = cap.name
               JOIN {context} ctx on rc.contextid = ctx.id
-             WHERE rc.contextid $incontexts AND rc.capability $incaps";
-    $rcs = $DB->get_records_sql($sql, array_merge($cparams, $capsparams));
+             WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
+    $rcs = $DB->get_records_sql($sql, array_merge($cparams, $capsparams, $excludelockedcapsparams));
     foreach ($rcs as $rc) {
         $defs[$rc->path][$rc->roleid] = $rc->permission;
     }
index edf5998..07f66e4 100644 (file)
@@ -6339,7 +6339,10 @@ class admin_page_managemessageoutputs extends admin_externalpage {
      */
     public function __construct() {
         global $CFG;
-        parent::__construct('managemessageoutputs', get_string('managemessageoutputs', 'message'), new moodle_url('/admin/message.php'));
+        parent::__construct('managemessageoutputs',
+            get_string('defaultmessageoutputs', 'message'),
+            new moodle_url('/admin/message.php')
+        );
     }
 
     /**
@@ -6385,14 +6388,24 @@ class admin_page_managemessageoutputs extends admin_externalpage {
 /**
  * Default message outputs configuration
  *
+ * @deprecated since Moodle 3.7 MDL-64495. Please use admin_page_managemessageoutputs instead.
+ * @todo       MDL-64866 This will be deleted in Moodle 4.1.
+ *
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class admin_page_defaultmessageoutputs extends admin_page_managemessageoutputs {
     /**
      * Calls parent::__construct with specific arguments
+     *
+     * @deprecated since Moodle 3.7 MDL-64495. Please use admin_page_managemessageoutputs instead.
+     * @todo       MDL-64866 This will be deleted in Moodle 4.1.
      */
     public function __construct() {
         global $CFG;
+
+        debugging('admin_page_defaultmessageoutputs class is deprecated. Please use admin_page_managemessageoutputs instead.',
+            DEBUG_DEVELOPER);
+
         admin_externalpage::__construct('defaultmessageoutputs', get_string('defaultmessageoutputs', 'message'), new moodle_url('/message/defaultoutputs.php'));
     }
 }
index 830d6fb..ccd55f0 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
diff --git a/lib/amd/build/loadingicon.min.js b/lib/amd/build/loadingicon.min.js
new file mode 100644 (file)
index 0000000..645d044
Binary files /dev/null and b/lib/amd/build/loadingicon.min.js differ
index 90275cb..754f9a6 100644 (file)
@@ -24,7 +24,9 @@
  * @since      3.0
  */
 /* globals require: false */
-define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) {
+define(
+    ['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification', 'core/loadingicon'],
+function($, log, str, templates, notification, LoadingIcon) {
 
     // Private functions and variables.
     /** @var {Object} KEYS - List of keycode constants. */
@@ -555,6 +557,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      */
     var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
         var pendingPromise = addPendingJSPromise('updateAjax');
+        // We need to show the indicator outside of the hidden select list.
+        // So we get the parent id of the hidden select list.
+        var parentElement = $(document.getElementById(state.selectId)).parent();
+        LoadingIcon.addIconToContainerRemoveOnCompletion(parentElement, pendingPromise);
 
         // Get the query to pass to the ajax function.
         var query = $(e.currentTarget).val();
diff --git a/lib/amd/src/loadingicon.js b/lib/amd/src/loadingicon.js
new file mode 100644 (file)
index 0000000..a8886c9
--- /dev/null
@@ -0,0 +1,110 @@
+// 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/>.
+
+/**
+ * Contain the logic for the loading icon.
+ *
+ * @module     core/loading_icon
+ * @class      loading_icon
+ * @package    core
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/templates'], function($, Templates) {
+    var TEMPLATES = {
+        LOADING: 'core/loading',
+    };
+
+    var getIcon = function() {
+        return Templates.render(TEMPLATES.LOADING, {});
+    };
+
+    /**
+     * Add a loading icon to the end of the specified container and return an unresolved promise.
+     *
+     * Resolution of the returned promise causes the icon to be faded out and removed.
+     *
+     * @method  addIconToContainer
+     * @param   {jQuery}  container  The element to add the spinner to
+     * @return  {Promise} The Promise used to create the icon.
+     */
+    var addIconToContainer = function(container) {
+        return getIcon()
+        .then(function(html) {
+            var loadingIcon = $(html).hide();
+            container.append(loadingIcon);
+            loadingIcon.fadeIn(150);
+
+            return loadingIcon;
+        });
+    };
+
+    /**
+     * Add a loading icon to the end of the specified container and return an unresolved promise.
+     *
+     * Resolution of the returned promise causes the icon to be faded out and removed.
+     *
+     * @method  addIconToContainerWithPromise
+     * @param   {jQuery}  container  The element to add the spinner to
+     * @param   {Promise} loadingIconPromise The jQuery Promise which determines the removal of the icon
+     * @return  {jQuery}  The Promise used to create and then remove the icon.
+     */
+    var addIconToContainerRemoveOnCompletion = function(container, loadingIconPromise) {
+        return getIcon()
+        .then(function(html) {
+            var loadingIcon = $(html).hide();
+            container.append(loadingIcon);
+            loadingIcon.fadeIn(150);
+
+            return $.when(loadingIcon.promise(), loadingIconPromise);
+        })
+        .then(function(loadingIcon) {
+            // Once the content has finished loading and
+            // the loading icon has been shown then we can
+            // fade the icon away to reveal the content.
+            return loadingIcon.fadeOut(100).promise();
+        })
+        .then(function(loadingIcon) {
+            loadingIcon.remove();
+
+            return;
+        });
+    };
+
+    /**
+     * Add a loading icon to the end of the specified container and return an unresolved promise.
+     *
+     * Resolution of the returned promise causes the icon to be faded out and removed.
+     *
+     * @method  addIconToContainerWithPromise
+     * @param   {jQuery}  container  The element to add the spinner to
+     * @return  {Promise} A jQuery Promise to resolve when ready
+     */
+    var addIconToContainerWithPromise = function(container) {
+        var loadingIconPromise = $.Deferred();
+
+        addIconToContainerRemoveOnCompletion(container, loadingIconPromise);
+
+        return loadingIconPromise;
+    };
+
+    return {
+        getIcon: getIcon,
+        addIconToContainer: addIconToContainer,
+        addIconToContainerWithPromise: addIconToContainerWithPromise,
+        addIconToContainerRemoveOnCompletion: addIconToContainerRemoveOnCompletion,
+    };
+
+});
index 0337ba1..2121f7a 100644 (file)
@@ -631,6 +631,8 @@ class behat_config_util {
 
         // Automatically add Chrome command line option to skip the prompt about allowing file
         // storage - needed for mobile app testing (won't hurt for everything else either).
+        // We also need to disable web security, otherwise it can't make CSS requests to the server
+        // on localhost due to CORS restrictions.
         if (!empty($values['browser']) && $values['browser'] === 'chrome') {
             if (!isset($values['capabilities'])) {
                 $values['capabilities'] = [];
@@ -642,6 +644,7 @@ class behat_config_util {
                 $values['capabilities']['chrome']['switches'] = [];
             }
             $values['capabilities']['chrome']['switches'][] = '--unlimited-storage';
+            $values['capabilities']['chrome']['switches'][] = '--disable-web-security';
 
             // If the mobile app is enabled, check its version and add appropriate tags.
             if ($mobiletags = $this->get_mobile_version_tags()) {
index def514b..9175784 100644 (file)
@@ -65,8 +65,6 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'fieldset' => 'fieldset',
         'icon' => 'icon',
         'list_item' => 'list_item',
-        'message_area_region' => 'message_area_region',
-        'message_area_region_content' => 'message_area_region_content',
         'question' => 'question',
         'region' => 'region',
         'section' => 'section',
@@ -90,13 +88,14 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'fieldset' => 'fieldset',
         'file' => 'file',
         'filemanager' => 'filemanager',
+        'group_message' => 'group_message',
+        'group_message_header' => 'group_message_header',
+        'group_message_member' => 'group_message_member',
+        'group_message_tab' => 'group_message_tab',
         'icon' => 'icon',
         'link' => 'link',
         'link_or_button' => 'link_or_button',
         'list_item' => 'list_item',
-        'message_area_action' => 'message_area_action',
-        'message_area_region' => 'message_area_region',
-        'message_area_region_content' => 'message_area_region_content',
         'optgroup' => 'optgroup',
         'option' => 'option',
         'question' => 'question',
@@ -151,6 +150,21 @@ XPATH
             and
         normalize-space(descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' modal-header ')] = %locator%)
     ]
+XPATH
+        , 'group_message' => <<<XPATH
+        .//*[@data-conversation-id]//img[contains(@alt, %locator%)]/..
+XPATH
+        , 'group_message_header' => <<<XPATH
+        .//*[@data-region='message-drawer']//div[@data-region='header-container']//*[text()[contains(., %locator%)]]
+XPATH
+    , 'group_message_member' => <<<XPATH
+        .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+        //div[@class='list-group' and not(contains(@class, 'hidden'))]//*[text()[contains(., %locator%)]] |
+        .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+        //div[@data-region='empty-message-container' and not(contains(@class, 'hidden')) and contains(., %locator%)]
+XPATH
+    , 'group_message_tab' => <<<XPATH
+        .//*[@data-region='message-drawer']//button[@data-toggle='collapse']//*[text()[contains(., %locator%)]]/..
 XPATH
         , 'icon' => <<<XPATH
 .//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
@@ -183,15 +197,6 @@ XPATH
 XPATH
         , 'form_row' => <<<XPATH
 .//*[self::label or self::div[contains(concat(' ', @class, ' '), ' fstaticlabel ')]][contains(., %locator%)]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
-XPATH
-        , 'message_area_region' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-region = %locator%]
-XPATH
-        , 'message_area_region_content' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-region-content = %locator%]
-XPATH
-        , 'message_area_action' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-action = %locator%]
 XPATH
         , 'autocomplete_selection' => <<<XPATH
 .//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='listitem'][contains(normalize-space(.), %locator%)]
index 823e47f..805434f 100644 (file)
@@ -56,6 +56,8 @@ class registration {
             'commnews', // Receive communication news. This was added in 3.4 and is "On" by default. Admin must confirm or opt-out.
             'mobileservicesenabled', 'mobilenotificationsenabled', 'registereduserdevices', 'registeredactiveuserdevices' // Mobile stats added in 3.4.
         ],
+        // Analytics stats added in Moodle 3.7.
+        2019022200 => ['analyticsenabledmodels', 'analyticspredictions', 'analyticsactions', 'analyticsactionsnotuseful'],
     ];
 
     /** @var Site privacy: not displayed */
@@ -196,6 +198,12 @@ class registration {
             }
         }
 
+        // Analytics related data follow.
+        $siteinfo['analyticsenabledmodels'] = \core_analytics\stats::enabled_models();
+        $siteinfo['analyticspredictions'] = \core_analytics\stats::predictions();
+        $siteinfo['analyticsactions'] = \core_analytics\stats::actions();
+        $siteinfo['analyticsactionsnotuseful'] = \core_analytics\stats::actions_not_useful();
+
         // IMPORTANT: any new fields in siteinfo have to be added to the constant CONFIRM_NEW_FIELDS.
 
         return $siteinfo;
@@ -236,6 +244,10 @@ class registration {
             'mobilenotificationsenabled' => get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsenabled),
             'registereduserdevices' => get_string('registereduserdevices', 'hub', $siteinfo['registereduserdevices']),
             'registeredactiveuserdevices' => get_string('registeredactiveuserdevices', 'hub', $siteinfo['registeredactiveuserdevices']),
+            'analyticsenabledmodels' => get_string('analyticsenabledmodels', 'hub', $siteinfo['analyticsenabledmodels']),
+            'analyticspredictions' => get_string('analyticspredictions', 'hub', $siteinfo['analyticspredictions']),
+            'analyticsactions' => get_string('analyticsactions', 'hub', $siteinfo['analyticsactions']),
+            'analyticsactionsnotuseful' => get_string('analyticsactionsnotuseful', 'hub', $siteinfo['analyticsactionsnotuseful']),
         ];
 
         foreach ($senddata as $key => $str) {
index 5440a58..de30732 100644 (file)
@@ -84,13 +84,16 @@ class manager {
         // Get user records for all members of the conversation.
         // We must fetch distinct users, because it's possible for a user to message themselves via bulk user actions.
         // In such cases, there will be 2 records referring to the same user.
-        $sql = "SELECT u.*
+        $sql = "SELECT u.*, mca.id as ismuted
                   FROM {user} u
+             LEFT JOIN {message_conversation_actions} mca
+                    ON mca.userid = u.id AND mca.conversationid = ? AND mca.action = ?
                  WHERE u.id IN (
                           SELECT mcm.userid FROM {message_conversation_members} mcm
-                           WHERE mcm.conversationid = :convid
+                           WHERE mcm.conversationid = ?
                  )";
-        $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
+        $members = $DB->get_records_sql($sql, [$eventdata->convid, \core_message\api::CONVERSATION_ACTION_MUTED,
+            $eventdata->convid]);
         if (empty($members)) {
             throw new \moodle_exception("Conversation has no members or does not exist.");
         }
@@ -138,7 +141,10 @@ class manager {
         foreach ($otherusers as $recipient) {
             // If this message was a legacy (1:1) message, then we use the userto.
             if ($legacymessage) {
+                $ismuted = $recipient->ismuted;
+
                 $recipient = $eventdata->userto;
+                $recipient->ismuted = $ismuted;
             }
 
             $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
@@ -195,8 +201,9 @@ class manager {
 
             // Fill in the array of processors to be used based on default and user preferences.
             // This applies only to individual conversations. Messages to group conversations ignore processors.
+            // Do not process muted conversations.
             $processorlist = [];
-            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && !$recipient->ismuted) {
                 foreach ($processors as $processor) {
                     // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
                     if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
index cb46366..f187601 100644 (file)
@@ -260,6 +260,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/moodle_host' => 'fa-graduation-cap',
             'core:i/moremenu' => 'fa-ellipsis-h',
             'core:i/move_2d' => 'fa-arrows',
+            'core:i/muted' => 'fa-microphone-slash',
             'core:i/navigationitem' => 'fa-fw',
             'core:i/ne_red_mark' => 'fa-remove',
             'core:i/new' => 'fa-bolt',
@@ -386,6 +387,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/right' => 'fa-arrow-right',
             'core:t/sendmessage' => 'fa-paper-plane',
             'core:t/show' => 'fa-eye-slash',
+            'core:t/sort_by' => 'fa-sort-amount-asc',
             'core:t/sort_asc' => 'fa-sort-asc',
             'core:t/sort_desc' => 'fa-sort-desc',
             'core:t/sort' => 'fa-sort',
diff --git a/lib/classes/task/clean_up_deleted_search_area_task.php b/lib/classes/task/clean_up_deleted_search_area_task.php
new file mode 100644 (file)
index 0000000..ae8d73c
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * Adhoc task that clean up data related ro deleted search area.
+ *
+ * @package    core
+ * @copyright  2019 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class that cleans up data related to deleted search area.
+ *
+ * Custom data accepted:
+ *  - areaid -> String search area id .
+ *
+ * @package     core
+ * @copyright   2019 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class clean_up_deleted_search_area_task extends adhoc_task {
+
+    /**
+     * Run the task to clean up deleted search are data.
+     */
+    public function execute() {
+        $areaid = $this->get_custom_data();
+
+        try {
+            \core_search\manager::clean_up_non_existing_area($areaid);
+        } catch (\core_search\engine_exception $e) {
+            mtrace('Search is not configured. Skip deleting index for search area ' . $areaid);
+        }
+    }
+}
index e106f9c..fd1548f 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20190122" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20190308" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="message_conversation_actions" COMMENT="Stores all per-user actions on individual conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="action" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
     <TABLE NAME="message_user_actions" COMMENT="Stores all per-user actions on individual messages">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="modelid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="evaluationmode" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="target" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="indicators" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timesplitting" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
index e69b247..ea5f502 100644 (file)
@@ -874,6 +874,24 @@ $functions = array(
         'type' => 'write',
         'capabilities' => 'moodle/course:managegroups'
     ),
+    'core_message_mute_conversations' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'mute_conversations',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Mutes a list of conversations',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_unmute_conversations' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'unmute_conversations',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Unmutes a list of conversations',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_block_user' => array(
         'classname' => 'core_message_external',
         'methodname' => 'block_user',
index 3aa8faa..e096525 100644 (file)
@@ -2736,5 +2736,112 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019021500.02);
     }
 
+    if ($oldversion < 2019030100.01) {
+        // Create adhoc task to delete renamed My Course search area (ID core_course-mycourse).
+        $record = new \stdClass();
+        $record->classname = '\core\task\clean_up_deleted_search_area_task';
+        $record->component = 'core';
+
+        // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task().
+        $nextruntime = time() - 1;
+        $record->nextruntime = $nextruntime;
+        $record->customdata = json_encode('core_course-mycourse');
+
+        $DB->insert_record('task_adhoc', $record);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019030100.01);
+    }
+
+    if ($oldversion < 2019030700.01) {
+
+        // Define field evaluationmode to be added to analytics_models_log.
+        $table = new xmldb_table('analytics_models_log');
+        $field = new xmldb_field('evaluationmode', XMLDB_TYPE_CHAR, '50', null, null, null,
+            null, 'version');
+
+        // Conditionally launch add field evaluationmode.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+
+            $updatesql = "UPDATE {analytics_models_log}
+                             SET evaluationmode = 'configuration'";
+            $DB->execute($updatesql, []);
+
+            // Changing nullability of field evaluationmode on table block_instances to not null.
+            $field = new xmldb_field('evaluationmode', XMLDB_TYPE_CHAR, '50', null, XMLDB_NOTNULL,
+                null, null, 'version');
+
+            // Launch change of nullability for field evaluationmode.
+            $dbman->change_field_notnull($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019030700.01);
+    }
+
+    if ($oldversion < 2019030800.00) {
+        // Define table 'message_conversation_actions' to be created.
+        // Note - I would have preferred 'message_conversation_user_actions' but due to Oracle we can't. Boo.
+        $table = new xmldb_table('message_conversation_actions');
+
+        // Adding fields to table 'message_conversation_actions'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('action', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversation_actions'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, ['conversationid'], 'message_conversations', ['id']);
+
+        // Conditionally launch create table for 'message_conversation_actions'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019030800.00);
+    }
+
+    if ($oldversion < 2019030800.02) {
+        // Remove any conversations and their members associated with non-existent groups.
+        $sql = "SELECT mc.id
+                  FROM {message_conversations} mc
+             LEFT JOIN {groups} g
+                    ON mc.itemid = g.id
+                 WHERE mc.component = :component
+                   AND mc.itemtype = :itemtype
+                   AND g.id is NULL";
+        $conversations = $DB->get_records_sql($sql, ['component' => 'core_group', 'itemtype' => 'groups']);
+
+        if ($conversations) {
+            $conversationids = array_keys($conversations);
+
+            $DB->delete_records_list('message_conversations', 'id', $conversationids);
+            $DB->delete_records_list('message_conversation_members', 'conversationid', $conversationids);
+            $DB->delete_records_list('message_conversation_actions', 'conversationid', $conversationids);
+
+            // Now, go through each conversation and delete any messages and related message actions.
+            foreach ($conversationids as $conversationid) {
+                if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
+                    $messageids = array_keys($messages);
+
+                    // Delete the actions.
+                    list($insql, $inparams) = $DB->get_in_or_equal($messageids);
+                    $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
+
+                    // Delete the messages.
+                    $DB->delete_records('messages', ['conversationid' => $conversationid]);
+                }
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019030800.02);
+    }
+
     return true;
 }
index 07ebe6f..690d128 100644 (file)
@@ -65,7 +65,8 @@ class pgsql_native_recordset_testcase extends basic_testcase {
         // To make testing easier, create a database with the same dboptions as the real one,
         // but a low number for the cursor size.
         $this->specialdb = \moodle_database::get_driver_instance('pgsql', 'native', true);
-        $dboptions = ['fetchbuffersize' => $fetchbuffersize];
+        $dboptions = $CFG->dboptions;
+        $dboptions['fetchbuffersize'] = $fetchbuffersize;
         $this->specialdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname,
                 $DB->get_prefix(), $dboptions);
 
index 2e14642..08b6c6b 100644 (file)
@@ -74,7 +74,7 @@ $string['subtitles_help'] = 'Subtitles may be used to provide a transcription or
 $string['subtitlessourcelabel'] = 'Subtitle track URL';
 $string['track'] = 'Track URL';
 $string['tracks'] = 'Subtitles and captions';
-$string['tracks_help'] = 'Subtitles, captions, chapters and descriptions can be added via a WebVTT (Web Video Text Tracks) format file. Track labels will be shown in the selection dropdown menu. For each type of track, any track set as default will be pre-selected at the start of the video.';
+$string['tracks_help'] = 'Subtitles, captions, chapters and descriptions can be added via a WebVTT (Web Video Text Tracks) format file. Track labels will be shown in the selection drop-down menu. For each type of track, any track set as default will be pre-selected at the start of the video.';
 $string['video'] = 'Video';
 $string['videoheight'] = 'Video height';
 $string['videosourcelabel'] = 'Video source URL';
index 9b3b1b8..89c9e20 100644 (file)
@@ -6,10 +6,12 @@ Some changes from the upstream version have been made:
 * Added context type property for Context class
 * Set context type if 'context_type' parameter was submitted through POST
 * Do not require tool_consumer_instance_guid
-These changes can be reverted once the following pull requests/issues have been integrated upstream:
-* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10/commits/a9a1641f1a593eba4638133245c21d9ad47d8680
-* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11/commits/0bae60389bd020a02be5554516b86336e651e237
-* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/issues/19
+* Prevent modification of the request to the provider
+These changes can be reverted once the following pull requests have been integrated upstream:
+* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10
+* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11
+* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/47
+* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/48
 
 It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
 with an autoloader so it's better to do it manually.