Merge branch 'MDL-56637-master' of git://github.com/dpalou/moodle
authorDan Poltawski <dan@moodle.com>
Wed, 2 Nov 2016 13:56:22 +0000 (13:56 +0000)
committerDan Poltawski <dan@moodle.com>
Wed, 2 Nov 2016 13:56:22 +0000 (13:56 +0000)
216 files changed:
.eslintignore
.stylelintignore
admin/roles/ajax.php
admin/roles/define.php
admin/templates/setting_configpasswordunmask.mustache
admin/tool/lp/classes/form/user_evidence.php
admin/tool/lpimportcsv/lang/en/tool_lpimportcsv.php
admin/tool/messageinbound/classes/manager.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/monitor/classes/notification_task.php
admin/tool/templatelibrary/classes/api.php
admin/tool/templatelibrary/classes/external.php
admin/tool/usertours/classes/local/filter/role.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/usertours/tests/role_filter_test.php
admin/user/user_bulk_forms.php
admin/user/user_bulk_message.php
auth/email/classes/external.php
auth/ldap/lang/en/auth_ldap.php
backup/util/helper/backup_cron_helper.class.php
badges/cron.php
blog/external_blog_edit_form.php
cache/classes/dummystore.php
cache/classes/factory.php
cache/classes/helper.php
cache/classes/store.php
cache/locallib.php
cache/stores/apcu/lang/en/cachestore_apcu.php
cache/stores/apcu/lib.php
cache/stores/apcu/tests/apcu_test.php
cache/stores/file/lib.php
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
cache/stores/mongodb/lib.php
cache/stores/mongodb/tests/mongodb_test.php
cache/stores/redis/README.md [new file with mode: 0755]
cache/stores/redis/addinstanceform.php [new file with mode: 0755]
cache/stores/redis/lang/en/cachestore_redis.php [new file with mode: 0755]
cache/stores/redis/lib.php [new file with mode: 0755]
cache/stores/redis/settings.php [new file with mode: 0755]
cache/stores/redis/tests/redis_test.php [new file with mode: 0644]
cache/stores/redis/version.php [new file with mode: 0755]
cache/stores/session/lib.php
cache/stores/static/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/tests/fixtures/stores.php
cache/upgrade.txt
cohort/index.php
competency/lib.php
course/lib.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/flatfile/lib.php
enrol/imsenterprise/lib.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/lti/classes/tool_provider.php
enrol/lti/lang/en/enrol_lti.php
enrol/lti/tool.php
enrol/manual/lang/en/enrol_manual.php
enrol/meta/lib.php
enrol/paypal/classes/util.php
enrol/paypal/ipn.php
enrol/paypal/lang/en/enrol_paypal.php
enrol/self/lang/en/enrol_self.php
error/index.php
lang/en/admin.php
lang/en/auth.php
lang/en/role.php
lib/amd/build/inplace_editable.min.js
lib/amd/src/inplace_editable.js
lib/badgeslib.php
lib/classes/antivirus/scanner.php
lib/classes/event/message_sent.php
lib/classes/message/manager.php
lib/classes/message/message.php
lib/classes/plugin_manager.php
lib/classes/update/checker.php
lib/db/access.php
lib/enrollib.php
lib/form/autocomplete.php
lib/form/editor.php
lib/form/group.php
lib/form/select.php
lib/form/selectgroups.php
lib/form/selectwithlink.php
lib/form/static.php
lib/form/templates/editor_textarea.mustache
lib/form/url.php
lib/messagelib.php
lib/outputrenderers.php
lib/pagelib.php
lib/phpunit/tests/advanced_test.php
lib/portfolio/forms.php
lib/portfoliolib.php
lib/templates/popover_region.mustache
lib/testing/classes/util.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-debug.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer.js
lib/yui/src/formchangechecker/js/formchangechecker.js
lib/yui/src/maintenancemodetimer/js/maintenancemodetimer.js
login/confirm.php
message/amd/build/message_area_contacts.min.js
message/amd/src/message_area_contacts.js
message/classes/api.php
message/externallib.php
message/lib.php
message/templates/message_preferences_component.mustache
message/templates/message_preferences_notification_processor.mustache
message/templates/notification_preferences_component.mustache
message/templates/notification_preferences_component_notification.mustache
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/search_test_received.php
message/tests/search_test_sent.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/externallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/styles.css
mod/assign/tests/locallib_test.php
mod/data/view.php
mod/feedback/classes/complete_form.php
mod/feedback/edit_form.php
mod/feedback/lib.php
mod/feedback/show_nonrespondents.php
mod/forum/lib.php
mod/forum/tests/mail_test.php
mod/lesson/essay.php
mod/lesson/view.php
mod/lti/lang/en/lti.php
mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/renderer.php
mod/quiz/tests/behat/attempt_redo_questions.feature
mod/scorm/locallib.php
mod/scorm/module.js
mod/scorm/report/basic/classes/report.php
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
mod/scorm/styles.css
mod/wiki/view.php
portfolio/googledocs/tests/plugin_test.php [new file with mode: 0644]
question/type/calculated/lang/en/qtype_calculated.php
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/src/form-display-errors.js
theme/boost/classes/output/core_renderer.php
theme/boost/config.php
theme/boost/lang/en/theme_boost.php
theme/boost/layout/columns1.php
theme/boost/layout/columns2.php
theme/boost/lib.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bootswatch.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/expendable.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/search.scss
theme/boost/scss/moodle/undo.scss
theme/boost/scss/preset-flatly.scss [deleted file]
theme/boost/scss/preset-paper.scss [deleted file]
theme/boost/scss/preset-readable.scss [deleted file]
theme/boost/scss/preset.scss [new file with mode: 0644]
theme/boost/scss/preset/default.scss [moved from theme/boost/scss/preset-default.scss with 100% similarity]
theme/boost/scss/preset/plain.scss [moved from theme/boost/scss/preset-plain.scss with 100% similarity]
theme/boost/settings.php
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/help_icon.mustache
theme/boost/templates/core_admin/setting_configpasswordunmask.mustache
theme/boost/templates/core_form/editor_textarea.mustache
theme/boost/templates/core_form/element-advcheckbox-inline.mustache
theme/boost/templates/core_form/element-advcheckbox.mustache
theme/boost/templates/core_form/element-checkbox-inline.mustache
theme/boost/templates/core_form/element-checkbox.mustache
theme/boost/templates/core_form/element-date_selector.mustache
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/boost/templates/core_form/element-group-inline.mustache
theme/boost/templates/core_form/element-group.mustache
theme/boost/templates/core_form/element-radio-inline.mustache
theme/boost/templates/core_form/element-radio.mustache
theme/boost/templates/core_form/element-select-inline.mustache
theme/boost/templates/core_form/element-select.mustache
theme/boost/templates/core_form/element-selectgroups-inline.mustache
theme/boost/templates/core_form/element-selectgroups.mustache
theme/boost/templates/core_form/element-selectwithlink.mustache
theme/boost/templates/core_form/element-static.mustache
theme/boost/templates/core_form/element-template.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/header.mustache
theme/boost/templates/maintenance.mustache
theme/boost/thirdpartylibs.xml
theme/boost/version.php
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/tool_usertours.less
theme/bootstrapbase/style/moodle.css
user/profile/lib.php
version.php
webservice/renderer.php

index 9ad89b7..68293b9 100644 (file)
@@ -72,9 +72,6 @@ theme/boost/amd/src/tab.js
 theme/boost/amd/src/tooltip.js
 theme/boost/amd/src/util.js
 theme/boost/amd/src/tether.js
-theme/boost/scss/preset-flatly.scss
-theme/boost/scss/preset-paper.scss
-theme/boost/scss/preset-readable.scss
 theme/bootstrapbase/less/bootstrap/
 theme/bootstrapbase/javascript/html5shiv.js
 theme/bootstrapbase/amd/src/bootstrap.js
\ No newline at end of file
index f6ac277..383c520 100644 (file)
@@ -73,9 +73,6 @@ theme/boost/amd/src/tab.js
 theme/boost/amd/src/tooltip.js
 theme/boost/amd/src/util.js
 theme/boost/amd/src/tether.js
-theme/boost/scss/preset-flatly.scss
-theme/boost/scss/preset-paper.scss
-theme/boost/scss/preset-readable.scss
 theme/bootstrapbase/less/bootstrap/
 theme/bootstrapbase/javascript/html5shiv.js
 theme/bootstrapbase/amd/src/bootstrap.js
\ No newline at end of file
index 2b66c9f..d98e379 100644 (file)
@@ -32,6 +32,8 @@ $getroles = optional_param('getroles', 0, PARAM_BOOL);
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
+$PAGE->set_context($context);
+
 require_login($course, false, $cm);
 require_capability('moodle/role:review', $context);
 require_sesskey();
index 5a5e6a3..093c693 100644 (file)
@@ -256,9 +256,9 @@ if ($action === 'view') {
 <input type="hidden" name="sesskey" value="<?php p(sesskey()) ?>" />
 <input type="hidden" name="return" value="<?php p($return); ?>" />
 <input type="hidden" name="resettype" value="none" />
-<div class="submit buttons">
-    <input type="submit" name="savechanges" value="<?php p($submitlabel); ?>" />
-    <input type="submit" name="cancel" value="<?php print_string('cancel'); ?>" />
+<div class="submitbuttons">
+    <input type="submit" name="savechanges" class="btn btn-primary" value="<?php p($submitlabel); ?>" />
+    <input type="submit" name="cancel" class="btn btn-secondary" value="<?php print_string('cancel'); ?>" />
 </div>
     <?php
 }
@@ -271,9 +271,9 @@ if ($action === 'view') {
     echo '</div>';
 } else {
     ?>
-<div class="submit buttons">
-    <input type="submit" name="savechanges" value="<?php p($submitlabel); ?>" />
-    <input type="submit" name="cancel" value="<?php print_string('cancel'); ?>" />
+<div class="submitbuttons">
+    <input type="submit" name="savechanges" class="btn btn-primary" value="<?php p($submitlabel); ?>" />
+    <input type="submit" name="cancel" class="btn btn-secondary" value="<?php print_string('cancel'); ?>" />
 </div>
 </div></form>
 <?php
index bf00ea7..cf7565c 100644 (file)
 }}
 <div class="form-password">
     <span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
-        <noscript>
-            <!-- Backwards compatability for Behat -->
-            <input  type="password"
-                    name="{{ name }}"
-                    id="{{ id }}"
-                    value="{{ value }}"
-                    size="{{ size }}"
-                    >
-        </noscript>
         <span class="visibleifjs">
             <span data-passwordunmask="editor">
-                <!-- The input in the noscript will be moved here as part of the page load -->
+                <input  type="hidden"
+                        name="{{ name }}"
+                        id="{{ id }}"
+                        value="{{ value }}"
+                        size="{{ size }}"
+                        >
             </span>
             <a href="#" data-passwordunmask="edit" title="{{ edithint }}">
                 <span data-passwordunmask="displayvalue">{{> core_form/element-passwordunmask-fill }}</span>
                 {{# str }} passwordunmaskinstructions, form {{/ str }}
             </span>
         </span>
+        <noscript>
+            <!-- Backwards compatability for Behat -->
+            <input  type="password"
+                    name="{{ name }}"
+                    id="{{ id }}"
+                    value="{{ value }}"
+                    size="{{ size }}"
+                    >
+        </noscript>
     </span>
 </div>
 {{#js}}
index 28de24c..74bb0c8 100644 (file)
@@ -56,7 +56,7 @@ class user_evidence extends persistent {
         $mform->addElement('editor', 'description', get_string('userevidencedescription', 'tool_lp'), array('rows' => 10));
         $mform->setType('description', PARAM_RAW);
 
-        $mform->addElement('url', 'url', get_string('userevidenceurl', 'tool_lp'), array(), array('usefilepicker' => false));
+        $mform->addElement('url', 'url', get_string('userevidenceurl', 'tool_lp'), array('size' => '60'), array('usefilepicker' => false));
         $mform->setType('url', PARAM_RAW_TRIMMED);      // Can not use PARAM_URL, it silently converts bad URLs to ''.
         $mform->addHelpButton('url', 'userevidenceurl', 'tool_lp');
 
index 813b096..9064eb2 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['competencyscale'] = 'Competency Scale: {$a}';
+$string['competencyscale'] = 'Competency scale: {$a}';
 $string['competencyscaledescription'] = 'Competency scale created by import';
-$string['confirmcolumnmappings'] = 'Confirm the columns mappings';
+$string['confirmcolumnmappings'] = 'Confirm the column mappings';
 $string['confirm'] = 'Confirm';
 $string['csvdelimiter'] = 'CSV delimiter';
-$string['csvdelimiter_help'] = 'CSV delimiter of the CSV file.';
+$string['csvdelimiter_help'] = 'The CSV delimiter is normally a comma.';
 $string['description'] = 'Description';
 $string['descriptionformat'] = 'Description format';
 $string['encoding'] = 'Encoding';
-$string['encoding_help'] = 'Encoding of the CSV file.';
+$string['encoding_help'] = 'The CSV file encoding is usually UTF-8.';
 $string['export'] = 'Export';
-$string['exportid'] = 'Exported id (optional)';
+$string['exportid'] = 'Exported ID (optional)';
 $string['exportnavlink'] = 'Export competency framework';
-$string['idnumber'] = 'Id number';
+$string['idnumber'] = 'ID number';
 $string['importfile'] = 'CSV framework description file';
 $string['import'] = 'Import';
 $string['invalidimportfile'] = 'File format is invalid.';
 $string['isframework'] = 'Is framework';
 $string['noframeworks'] = 'No competency frameworks have been created yet';
-$string['parentidnumber'] = 'Parent id number';
+$string['parentidnumber'] = 'Parent ID number';
 $string['pluginname'] = 'Import competency framework';
-$string['relatedidnumbers'] = 'Cross referenced competency id numbers';
+$string['relatedidnumbers'] = 'Cross-referenced competency ID numbers';
 $string['ruleconfig'] = 'Rule config (optional)';
 $string['ruleoutcome'] = 'Rule outcome (optional)';
 $string['ruletype'] = 'Rule type (optional)';
 $string['scaleconfiguration'] = 'Scale configuration';
 $string['scalevalues'] = 'Scale values';
-$string['shortname'] = 'Shortname';
+$string['shortname'] = 'Short name';
 $string['taxonomy'] = 'Taxonomy';
index a352f7c..0e26c58 100644 (file)
@@ -920,7 +920,7 @@ class manager {
         $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
         $addressmanager->set_data($record->id);
 
-        $eventdata = new \stdClass();
+        $eventdata = new \core\message\message();
         $eventdata->component           = 'tool_messageinbound';
         $eventdata->name                = 'invalidrecipienthandler';
 
@@ -930,6 +930,7 @@ class manager {
         $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
 
         // The message will be sent from the intended user.
+        $eventdata->courseid            = SITEID;
         $eventdata->userfrom            = \core_user::get_support_user();
         $eventdata->userto              = $USER;
         $eventdata->subject             = $this->get_reply_subject($this->currentmessagedata->envelope->subject);
@@ -970,7 +971,8 @@ class manager {
         $messagedata->subject = $this->currentmessagedata->envelope->subject;
         $messagedata->error = $error;
 
-        $eventdata = new \stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid            = SITEID;
         $eventdata->component           = 'tool_messageinbound';
         $eventdata->name                = 'messageprocessingerror';
         $eventdata->userfrom            = $userfrom;
@@ -1029,7 +1031,8 @@ class manager {
         $messagedata = new \stdClass();
         $messagedata->subject = $this->currentmessagedata->envelope->subject;
 
-        $eventdata = new \stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid            = SITEID;
         $eventdata->component           = 'tool_messageinbound';
         $eventdata->name                = 'messageprocessingsuccess';
         $eventdata->userfrom            = $userfrom;
index f8c0347..441a19a 100644 (file)
@@ -27,12 +27,12 @@ $string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed to site admi
 $string['clickheretolaunchtheapp'] = 'Click here if the app does not open automatically.';
 $string['enablesmartappbanners'] = 'Enable Smart App Banners';
 $string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in Mobile Safari.';
-$string['forcedurlscheme'] = 'The URL scheme allows to open the mobile app from other apps like the browser. Use this setting if you want to allow only your custom branded app to be opened by the browser.';
+$string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
 $string['forcedurlscheme_key'] = 'URL scheme';
 $string['httpsrequired'] = 'HTTPS required';
 $string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
 $string['iosappid'] = 'App\'s unique identifier';
-$string['iosappid_desc'] = 'You only need to change this value if you have a custom iOS app';
+$string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
 $string['loginintheapp'] = 'Via the app';
 $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
index 6e36841..a65ef04 100644 (file)
@@ -83,7 +83,8 @@ class notification_task extends \core\task\adhoc_task {
         $template = $subscription->template;
         $template = $this->replace_placeholders($template, $subscription, $eventobj, $context);
         $htmlmessage = format_text($template, $subscription->templateformat, array('context' => $context));
-        $msgdata = new \stdClass();
+        $msgdata = new \core\message\message();
+        $msgdata->courseid          = empty($subscription->courseid) ? SITEID : $subscription->courseid;
         $msgdata->component         = 'tool_monitor'; // Your component name.
         $msgdata->name              = 'notification'; // This is the message name from messages.php.
         $msgdata->userfrom          = \core_user::get_noreply_user();
index 23b7dcf..0d4f5c7 100644 (file)
@@ -111,7 +111,7 @@ class api {
      *
      * @param string $component The component that holds the template.
      * @param string $template The name of the template.
-     * @return string the template
+     * @return string the template or false if template doesn't exist.
      */
     public static function load_canonical_template($component, $template) {
         // Get the list of possible template directories.
@@ -133,7 +133,8 @@ class api {
         }
 
         if ($filename === false) {
-            throw new moodle_exception('filenotfound', 'error');
+            // There are occasions where we don't have a core template.
+            return false;
         }
 
         $templatestr = file_get_contents($filename);
index be583a2..2b5126c 100644 (file)
@@ -107,7 +107,7 @@ class external extends external_api {
      *
      * @param string $component The component that holds the template.
      * @param string $template The name of the template.
-     * @return string the template
+     * @return string the template, false if template doesn't exist.
      */
     public static function load_canonical_template($component, $template) {
         $params = self::validate_parameters(self::load_canonical_template_parameters(),
index 96bc6ea..a64ee83 100644 (file)
@@ -36,6 +36,13 @@ use context;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class role extends base {
+    /**
+     * The Site Admin pseudo-role.
+     *
+     * @var ROLE_SITEADMIN int
+     */
+    const ROLE_SITEADMIN = -1;
+
     /**
      * The name of the filter.
      *
@@ -52,7 +59,24 @@ class role extends base {
      *                                  And whose values are the values to display
      */
     public static function get_filter_options() {
-        return role_get_names(null, ROLENAME_ALIAS, true);
+        $allroles = role_get_names(null, ROLENAME_ALIAS);
+
+        $roles = [];
+        foreach ($allroles as $role) {
+            if ($role->archetype === 'guest') {
+                // No point in including the 'guest' role as it isn't possible to show tours to a guest.
+                continue;
+            }
+            $roles[$role->shortname] = $role->localname;
+        }
+
+        // Add the Site Administrator pseudo-role.
+        $roles[self::ROLE_SITEADMIN] = get_string('administrator', 'core');
+
+        // Sort alphabetically too.
+        \core_collator::asort($roles);
+
+        return $roles;
     }
 
     /**
@@ -73,14 +97,19 @@ class role extends base {
             return true;
         }
 
-        if (is_siteadmin()) {
-            return true;
-        }
-
         // Presence within the array is sufficient. Ignore any value.
         $values = array_flip($values);
 
+        if (isset($values[self::ROLE_SITEADMIN]) && is_siteadmin()) {
+            // This tour has been restricted to a role including site admin, and this user is a site admin.
+            return true;
+        }
+
+        // Use a request cache to save on DB queries.
+        // We may be checking multiple tours and they'll all be for the same userid, and contextid
         $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'tool_usertours', 'filter_role');
+
+        // Get all of the roles used in this context, including special roles such as user, and frontpageuser.
         $cachekey = "{$USER->id}_{$context->id}";
         $userroles = $cache->get($cachekey);
         if ($userroles === false) {
@@ -88,8 +117,20 @@ class role extends base {
             $cache->set($cachekey, $userroles);
         }
 
+        // Some special roles do not include the shortname.
+        // Therefore we must fetch all roles too. Thankfully these don't actually change based on context.
+        // They do require a DB call, so let's cache it.
+        $cachekey = "allroles";
+        $allroles = $cache->get($cachekey);
+        if ($allroles === false) {
+            $allroles = get_all_roles();
+            $cache->set($cachekey, $allroles);
+        }
+
+        // Now we can check whether any of the user roles are in the list of allowed roles for this filter.
         foreach ($userroles as $role) {
-            if (isset($values[$role->roleid])) {
+            $shortname = $allroles[$role->roleid]->shortname;
+            if (isset($values[$shortname])) {
                 return true;
             }
         }
index 90e72ee..31f74bf 100644 (file)
@@ -47,7 +47,7 @@ You can enter a content in the following formats:
     <dt>Moodle Translated string</dt>
     <dd>A value found in a standard Moodle language file in the format identifier,component</dd>
 </dl>';
-$string['cssselector'] = 'CSS Selector';
+$string['cssselector'] = 'CSS selector';
 $string['defaultvalue'] = 'Default ({$a})';
 $string['delay'] = 'Delay before showing the step';
 $string['done'] = 'Done';
@@ -60,12 +60,11 @@ $string['event_tour_ended'] = 'Tour ended';
 $string['event_step_shown'] = 'Step shown';
 $string['exporttour'] = 'Export tour';
 $string['filter_header'] = 'Tour filters';
-$string['filter_help'] = 'Your can choose which conditions your tour will be shown under.
-All of the filters must match for a tour to be shown to that user.';
+$string['filter_help'] = 'Select the conditions under which the tour will be shown. All of the filters must match for a tour to be shown to a user.';
 $string['filter_theme'] = 'Theme';
 $string['filter_theme_help'] = 'Show the tour when the user is using one of the selected themes.';
 $string['filter_role'] = 'Role';
-$string['filter_role_help'] = 'Only show the tour to users with one of the specified roles.';
+$string['filter_role_help'] = 'A tour may be restricted to users with selected roles in the context where the tour is shown. For example, restricting a Dashboard tour to users with the role of student won\'t work if users have the role of student in a course (as is generally the case). A Dashboard tour can only be restricted to users with a system role.';
 $string['importtour'] = 'Import tour';
 $string['left'] = 'Left';
 $string['movestepdown'] = 'Move step down';
@@ -89,7 +88,7 @@ Some example values include:
 * /mod/forum/view.php% - to match the forum discussion list
 * /user/profile.php% - to match the user profile page';
 $string['placement'] = 'Placement';
-$string['pluginname'] = 'User Tours';
+$string['pluginname'] = 'User tours';
 $string['resettouronpage'] = 'Reset user tour on this page';
 $string['right'] = 'Right';
 $string['select_block'] = 'Select a block';
@@ -149,7 +148,7 @@ $string['selecttype'] = 'Select step type';
 $string['sharedtourslink'] = 'Tour repository';
 $string['usertours'] = 'User tours';
 $string['usertours:managetours'] = 'Create, edit, and remove user tours';
-$string['target_selector_targetvalue'] = 'CSS Selectors';
+$string['target_selector_targetvalue'] = 'CSS selectors';
 $string['target_selector_targetvalue_help'] = 'You can use a "CSS Selector" to target almost any element on the page.
 
 CSS Selectors are very powerful and you can easily find parts of the page by building up the selector gradually.
index ade49dd..940e517 100644 (file)
@@ -102,7 +102,7 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
         $context = \context_course::instance($this->course->id);
 
         $roles = [
-            $this->roles['student'],
+            'student',
         ];
 
         // Note: No need to persist this tour.
@@ -119,9 +119,9 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
             }
         }
 
-        // The admin should always be able to view too.
+        // The admin can't view this one either.
         $this->setAdminUser();
-        $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+        $this->assertFalse(\tool_usertours\local\filter\role::filter_matches($tour, $context));
     }
 
     /**
@@ -131,8 +131,8 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
         $context = \context_course::instance($this->course->id);
 
         $roles = [
-            $this->roles['teacher'],
-            $this->roles['editingteacher'],
+            'teacher',
+            'editingteacher',
         ];
 
         // Note: No need to persist this tour.
@@ -149,9 +149,9 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
             }
         }
 
-        // The admin should always be able to view too.
+        // The admin can't view this one either.
         $this->setAdminUser();
-        $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+        $this->assertFalse(\tool_usertours\local\filter\role::filter_matches($tour, $context));
     }
 
     /**
@@ -161,7 +161,7 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
         $context = \context_course::instance($this->course->id);
 
         $roles = [
-            $this->roles['student'],
+            'student',
         ];
 
         $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->roles['teacher']);
@@ -181,8 +181,109 @@ class tool_usertours_role_filter_testcase extends advanced_testcase {
             }
         }
 
-        // The admin should always be able to view too.
+        // The admin can't view this one either.
+        $this->setAdminUser();
+        $this->assertFalse(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+    }
+
+    /**
+     * Test the filter_matches function when it is targetted at an admin.
+     */
+    public function test_filter_matches_multiple_role_only_admin() {
+        $context = \context_course::instance($this->course->id);
+
+        $roles = [
+            \tool_usertours\local\filter\role::ROLE_SITEADMIN,
+        ];
+
+        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->roles['teacher']);
+
+        // Note: No need to persist this tour.
+        $tour = new \tool_usertours\tour();
+        $tour->set_filter_values('role', $roles);
+
+
+        // Note: The role filter does not use the context.
+        foreach ($this->testroles as $role) {
+            $this->setUser($this->$role);
+            $this->assertFalse(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+        }
+
+        // The admin can view this one because it's only aimed at them.
+        $this->setAdminUser();
+        $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+    }
+
+    /**
+     * Test the filter_matches function when multiple roles are set, including an admin user.
+     */
+    public function test_filter_matches_multiple_role_including_admin() {
+        $context = \context_course::instance($this->course->id);
+
+        $roles = [
+            \tool_usertours\local\filter\role::ROLE_SITEADMIN,
+            'teacher',
+            'editingteacher',
+        ];
+
+        // Note: No need to persist this tour.
+        $tour = new \tool_usertours\tour();
+        $tour->set_filter_values('role', $roles);
+
+        // Note: The role filter does not use the context.
+        foreach ($this->testroles as $role) {
+            $this->setUser($this->$role);
+            if ($role === 'student') {
+                $this->assertFalse(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+            } else {
+                $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+            }
+        }
+
+        // The admin can view this one because it's only aimed at them.
         $this->setAdminUser();
         $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
     }
+
+    /**
+     * Test the filter_matches function when an admin user has multiple roles.
+     */
+    public function test_filter_matches_multiple_role_admin_user() {
+        global $USER;
+
+        $context = \context_course::instance($this->course->id);
+
+        $roles = [
+            \tool_usertours\local\filter\role::ROLE_SITEADMIN,
+        ];
+
+        $this->setAdminUser();
+        $this->getDataGenerator()->enrol_user($USER->id, $this->course->id, $this->roles['student']);
+
+        // Note: No need to persist this tour.
+        $tour = new \tool_usertours\tour();
+        $tour->set_filter_values('role', $roles);
+
+        // The admin can view this one because it's only aimed at them.
+        $this->assertTrue(\tool_usertours\local\filter\role::filter_matches($tour, $context));
+    }
+
+    /**
+     * Test that the get_filter_options function does not include the guest roles.
+     */
+    public function test_get_filter_options_no_guest_roles() {
+        create_role('Test Role', 'testrole', 'This is a test role', 'guest');
+
+        $allroles = role_get_names(null, ROLENAME_ALIAS);
+        $options = \tool_usertours\local\filter\role::get_filter_options();
+
+        foreach ($allroles as $role) {
+            $hasrole = isset($options[$role->shortname]);
+            if ($role->archetype === 'guest') {
+                $this->assertFalse($hasrole);
+            } else {
+                $this->assertTrue($hasrole);
+            }
+        }
+    }
 }
index dd410f4..ee71a22 100644 (file)
@@ -101,10 +101,12 @@ class user_bulk_form extends moodleform {
         $objs = array();
         $objs[] =& $mform->createElement('submit', 'addsel', get_string('addsel', 'bulkusers'));
         $objs[] =& $mform->createElement('submit', 'removesel', get_string('removesel', 'bulkusers'));
+        $grp =& $mform->addElement('group', 'buttonsgrp', get_string('selectedlist', 'bulkusers'), $objs, null, false);
+        $mform->addHelpButton('buttonsgrp', 'selectedlist', 'bulkusers');
+        $objs = array();
         $objs[] =& $mform->createElement('submit', 'addall', get_string('addall', 'bulkusers'));
         $objs[] =& $mform->createElement('submit', 'removeall', get_string('removeall', 'bulkusers'));
-        $grp =& $mform->addElement('group', 'buttonsgrp', get_string('selectedlist', 'bulkusers'), $objs, array(' ', '<br />'), false);
-        $mform->addHelpButton('buttonsgrp', 'selectedlist', 'bulkusers');
+        $grp =& $mform->addElement('group', 'buttonsgrp2', '', $objs, null, false);
 
         $renderer =& $mform->defaultRenderer();
         $template = '<label class="qflabel" style="vertical-align:top">{label}</label> {element}';
index 83161a7..008367b 100644 (file)
@@ -9,7 +9,7 @@ $confirm = optional_param('confirm', 0, PARAM_BOOL);
 
 require_login();
 admin_externalpage_setup('userbulk');
-require_capability('moodle/site:readallmessages', context_system::instance());
+require_capability('moodle/site:manageallmessaging', context_system::instance());
 
 $return = $CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk.php';
 
index 6a1b387..4ecff91 100644 (file)
@@ -169,7 +169,7 @@ class auth_email_external extends external_api {
                 ),
                 'recaptchapublickey' => new external_value(PARAM_RAW, 'Recaptcha public key', VALUE_OPTIONAL),
                 'recaptchachallengehash' => new external_value(PARAM_RAW, 'Recaptcha challenge hash', VALUE_OPTIONAL),
-                'recaptchachallengeimage' => new external_value(PARAM_URL, 'Recaptcha challenge <noscript> image', VALUE_OPTIONAL),
+                'recaptchachallengeimage' => new external_value(PARAM_URL, 'Recaptcha challenge noscript image', VALUE_OPTIONAL),
                 'recaptchachallengejs' => new external_value(PARAM_URL, 'Recaptcha challenge js url', VALUE_OPTIONAL),
                 'warnings'  => new external_warnings(),
             )
index 7ca3fb2..6f23523 100644 (file)
@@ -77,7 +77,7 @@ $string['auth_ldap_opt_deref'] = 'Determines how aliases are handled during sear
 $string['auth_ldap_opt_deref_key'] = 'Dereference aliases';
 $string['auth_ldap_passtype'] = 'Specify the format of new or changed passwords in LDAP server.';
 $string['auth_ldap_passtype_key'] = 'Password format';
-$string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiration settings.';
+$string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiration settings';
 $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
 $string['auth_ldap_preventpassindb_key'] = 'Don\'t cache passwords';
 $string['auth_ldap_search_sub'] = 'Search users from subcontexts.';
@@ -134,7 +134,7 @@ $string['ntlmsso_isdisabled'] = 'NTLM SSO is disabled.';
 $string['ntlmsso_unknowntype'] = 'Unknown ntlmsso type!';
 $string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support or you have configured Moodle to use LDAP protocol version 2)';
 $string['pagesize'] = 'Make sure this value is smaller than your LDAP server result set size limit (the maximum number of entries that can be returned in a single query)';
-$string['pagesize_key'] = 'Page Size';
+$string['pagesize_key'] = 'Page size';
 $string['pluginname'] = 'LDAP server';
 $string['pluginnotenabled'] = 'Plugin not enabled!';
 $string['renamingnotallowed'] = 'User renaming not allowed in LDAP';
index 8b6eeb8..fc62cfc 100644 (file)
@@ -264,7 +264,8 @@ abstract class backup_cron_automated_helper {
             $subject = $prefix.get_string('automatedbackupstatus', 'backup');
 
             //Send the message
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = SITEID;
             $eventdata->modulename        = 'moodle';
             $eventdata->userfrom          = $admin;
             $eventdata->userto            = $admin;
index a0bee0f..fc51a8d 100644 (file)
@@ -140,7 +140,8 @@ function badge_assemble_notification(stdClass $badge) {
         }
 
         // Create a message object.
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = SITEID;
         $eventdata->component         = 'moodle';
         $eventdata->name              = 'badgecreatornotice';
         $eventdata->userfrom          = $userfrom;
index 9c12087..2e0dcb7 100644 (file)
@@ -36,7 +36,7 @@ class blog_edit_external_form extends moodleform {
 
         $mform =& $this->_form;
 
-        $mform->addElement('url', 'url', get_string('url', 'blog'), array('size' => 50), array('usefilepicker' => false));
+        $mform->addElement('url', 'url', get_string('url', 'blog'), array('size' => 60), array('usefilepicker' => false));
         $mform->setType('url', PARAM_URL);
         $mform->addRule('url', get_string('emptyurl', 'blog'), 'required', null, 'client');
         $mform->addHelpButton('url', 'url', 'blog');
index 1b925a4..bcc9ca1 100644 (file)
@@ -258,10 +258,21 @@ class cachestore_dummy extends cache_store {
      */
     public static function initialise_test_instance(cache_definition $definition) {
         $cache = new cachestore_dummy('Dummy store test');
-        $cache->initialise($definition);
+        if ($cache->is_ready()) {
+            $cache->initialise($definition);
+        }
         return $cache;
     }
 
+    /**
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
+     */
+    public static function unit_test_configuration() {
+        return [];
+    }
+
     /**
      * Returns the name of this instance.
      * @return string
index ac20bec..4ba6565 100644 (file)
@@ -277,6 +277,9 @@ class cache_factory {
         if (!array_key_exists($name, $this->stores)) {
             // Properties: name, plugin, configuration, class.
             $class = $details['class'];
+            if (!$class::are_requirements_met()) {
+                return false;
+            }
             $store = new $class($details['name'], $details['configuration']);
             $this->stores[$name] = $store;
         }
index c25369e..30f8235 100644 (file)
@@ -502,15 +502,18 @@ class cache_helper {
         $store = $stores[$storename];
         $class = $store['class'];
 
+
+        // We check are_requirements_met although we expect is_ready is going to check as well.
+        if (!$class::are_requirements_met()) {
+            return false;
+        }
         // Found the store: is it ready?
         /* @var cache_store $instance */
         $instance = new $class($store['name'], $store['configuration']);
-        // We check are_requirements_met although we expect is_ready is going to check as well.
-        if (!$instance::are_requirements_met() || !$instance->is_ready()) {
+        if (!$instance->is_ready()) {
             unset($instance);
             return false;
         }
-
         foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
             $definition = cache_definition::load($id, $definition);
             $definitioninstance = clone($instance);
index eec660f..4fcb03f 100644 (file)
@@ -81,15 +81,11 @@ interface cache_store_interface {
     public static function initialise_test_instance(cache_definition $definition);
 
     /**
-     * Initialises a test instance for unit tests.
+     * Generates the appropriate configuration required for unit testing.
      *
-     * This differs from initialise_test_instance in that it doesn't rely on interacting with the config table.
-     *
-     * @since 2.8
-     * @param cache_definition $definition
-     * @return cache_store|false
+     * @return array Array of unit test configuration data to be used by initialise().
      */
-    public static function initialise_unit_test_instance(cache_definition $definition);
+    public static function unit_test_configuration();
 }
 
 /**
@@ -369,20 +365,6 @@ abstract class cache_store implements cache_store_interface {
         return clone($this);
     }
 
-    /**
-     * Initialises a test instance for unit tests.
-     *
-     * This differs from initialise_test_instance in that it doesn't rely on interacting with the config table.
-     * By default however it calls initialise_test_instance to support backwards compatibility.
-     *
-     * @since 2.8
-     * @param cache_definition $definition
-     * @return cache_store|false
-     */
-    public static function initialise_unit_test_instance(cache_definition $definition) {
-        return static::initialise_test_instance($definition);
-    }
-
     /**
      * Can be overridden to return any warnings this store instance should make to the admin.
      *
index e97dd56..90a8a8b 100644 (file)
@@ -684,33 +684,36 @@ abstract class cache_administration_helper extends cache_helper {
         $locks = $instance->get_locks();
         foreach ($stores as $name => $details) {
             $class = $details['class'];
-            $store = new $class($details['name'], $details['configuration']);
+            $store = false;
+            if ($class::are_requirements_met()) {
+                $store = new $class($details['name'], $details['configuration']);
+            }
             $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
             $record = array(
                 'name' => $name,
                 'plugin' => $details['plugin'],
                 'default' => $details['default'],
-                'isready' => $store->is_ready(),
+                'isready' => $store ? $store->is_ready() : false,
                 'requirementsmet' => $class::are_requirements_met(),
                 'mappings' => 0,
                 'lock' => $lock,
                 'modes' => array(
                     cache_store::MODE_APPLICATION =>
-                        ($store->get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
+                        ($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
                     cache_store::MODE_SESSION =>
-                        ($store->get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
+                        ($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
                     cache_store::MODE_REQUEST =>
-                        ($store->get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
+                        ($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
                 ),
                 'supports' => array(
-                    'multipleidentifiers' => $store->supports_multiple_identifiers(),
-                    'dataguarantee' => $store->supports_data_guarantee(),
-                    'nativettl' => $store->supports_native_ttl(),
+                    'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
+                    'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
+                    'nativettl' => $store ? $store->supports_native_ttl() : false,
                     'nativelocking' => ($store instanceof cache_is_lockable),
                     'keyawareness' => ($store instanceof cache_is_key_aware),
                     'searchable' => ($store instanceof cache_is_searchable)
                 ),
-                'warnings' => $store->get_warnings()
+                'warnings' => $store ? $store->get_warnings() : array()
             );
             if (empty($details['default'])) {
                 $return[$name] = $record;
index 95608a6..9bdf82d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['clusternotice'] = 'Please be aware that APCu only a suitable choice for single node sites or caches that can be stored locally.
-For more information see <a href="{$a}">Moodle docs</a>';
+$string['clusternotice'] = 'Please be aware that APCu is only a suitable choice for single node sites or caches that can be stored locally.
+For more information, see the <a href="{$a}">APC user cache documentation</a>.';
 $string['notice'] = 'Notice';
-$string['pluginname'] = 'APC User Cache (APCu)';
+$string['pluginname'] = 'APC user cache (APCu)';
 $string['prefix'] = 'Prefix';
 $string['prefix_help'] = 'The above prefix gets used for all keys being stored in this APC store instance. By default the database prefix is used.';
 $string['prefixinvalid'] = 'The prefix you have selected is invalid. You can only use a-z A-Z 0-9-_.';
 $string['prefixnotunique'] = 'The prefix you have selected is not unique. Please choose a unique prefix.';
 $string['testperformance'] = 'Test performance';
-$string['testperformance_desc'] = 'If enabled APCu performance will be included when viewing the Test performance page in the administration block. Enabling this on a production site is not recommended.';
+$string['testperformance_desc'] = 'If enabled, APCu performance will be included when viewing the Test performance page. Enabling this on a production site is not recommended.';
index 27cd2e6..077a5d7 100644 (file)
@@ -155,15 +155,6 @@ class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_i
         return ($this->definition !== null);
     }
 
-    /**
-     * Returns true if this cache store instance is ready to use.
-     * @return bool
-     */
-    public function is_ready() {
-        // No set up is actually required, providing apc is installed and enabled.
-        return true;
-    }
-
     /**
      * Prepares the given key for use.
      *
@@ -325,6 +316,7 @@ class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_i
         }
         $name = 'APCu test';
         $cache = new cachestore_apcu($name);
+        // No need to check if is_ready() as this has already being done by requirement check.
         $cache->initialise($definition);
         return $cache;
     }
@@ -369,23 +361,12 @@ class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_i
     }
 
     /**
-     * Generates an instance of the cache store that can be used for testing.
+     * Generates the appropriate configuration required for unit testing.
      *
-     * @param cache_definition $definition
-     * @return cachestore_apcu|false
+     * @return array Array of unit test configuration data to be used by initialise().
      */
-    public static function initialise_unit_test_instance(cache_definition $definition) {
-        if (!self::are_requirements_met()) {
-            return false;
-        }
-
-        $store = new cachestore_apcu('Test APCu', array('prefix' => 'phpunit'));
-        if (!$store->is_ready()) {
-            return false;
-        }
-        $store->initialise($definition);
-
-        return $store;
+    public static function unit_test_configuration() {
+        return array('prefix' => 'phpunit');
     }
 
     /**
@@ -416,4 +397,16 @@ class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_i
         }
         $editform->set_data($data);
     }
+
+    /**
+     * Returns true if this cache store instance is both suitable for testing, and ready for testing.
+     *
+     * Cache stores that support being used as the default store for unit and acceptance testing should
+     * override this function and return true if there requirements have been met.
+     *
+     * @return bool
+     */
+    public static function ready_to_be_used_for_testing() {
+        return true;
+    }
 }
index 36fc1ae..4903d3b 100644 (file)
@@ -57,7 +57,8 @@ class cachestore_apcu_test extends cachestore_tests {
      */
     public function test_cross_application_interaction() {
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test');
-        $instance = cachestore_apcu::initialise_unit_test_instance($definition);
+        $instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
+        $instance->initialise($definition);
 
         // Test purge with custom data.
         $this->assertTrue($instance->set('test', 'monster'));
@@ -75,9 +76,12 @@ class cachestore_apcu_test extends cachestore_tests {
 
     public function test_different_caches_have_different_prefixes() {
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test');
-        $instance = cachestore_apcu::initialise_unit_test_instance($definition);
+        $instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
+        $instance->initialise($definition);
+
         $definition2 = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test2');
-        $instance2 = cachestore_apcu::initialise_unit_test_instance($definition2);
+        $instance2 = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
+        $instance2->initialise($definition2);
 
         $instance->set('test1', 1);
         $this->assertFalse($instance2->get('test1'));
index 67a6642..dd5e058 100644 (file)
@@ -673,10 +673,21 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         $name = 'File test';
         $path = make_cache_directory('cachestore_file_test');
         $cache = new cachestore_file($name, array('path' => $path));
-        $cache->initialise($definition);
+        if ($cache->is_ready()) {
+            $cache->initialise($definition);
+        }
         return $cache;
     }
 
+    /**
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
+     */
+    public static function unit_test_configuration() {
+        return array();
+    }
+
     /**
      * Writes your madness to a file.
      *
index e4ead7f..60ee49b 100644 (file)
@@ -576,30 +576,24 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         }
 
         $store = new cachestore_memcache('Test memcache', $configuration);
-        $store->initialise($definition);
+        if ($store->is_ready()) {
+            $store->initialise($definition);
+        }
 
         return $store;
     }
 
     /**
-     * Creates a test instance for unit tests if possible.
-     * @param cache_definition $definition
-     * @return bool|cachestore_memcache
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
      */
-    public static function initialise_unit_test_instance(cache_definition $definition) {
-        if (!self::are_requirements_met()) {
-            return false;
-        }
+    public static function unit_test_configuration() {
+        // If the configuration is not defined correctly, return only the configuration know about.
         if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
-            return false;
+            return [];
         }
-        $configuration = array();
-        $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHE_TESTSERVERS);
-
-        $store = new cachestore_memcache('Test memcache', $configuration);
-        $store->initialise($definition);
-
-        return $store;
+        return ['servers' => explode("\n", TEST_CACHESTORE_MEMCACHE_TESTSERVERS)];
     }
 
     /**
index 0cba975..f5d4f08 100644 (file)
@@ -57,11 +57,13 @@ class cachestore_memcache_test extends cachestore_tests {
         $this->resetAfterTest(true);
 
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
-        $instance = cachestore_memcache::initialise_unit_test_instance($definition);
+        $instance = new cachestore_memcache('Memcache Test', cachestore_memcache::unit_test_configuration());
 
-        if (!$instance) { // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
+        if (!$instance->is_ready()) {
+            // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
             $this->markTestSkipped();
         }
+        $instance->initialise($definition);
 
         $keys = array(
             // Alphanumeric.
index 84ec315..56df937 100644 (file)
@@ -733,31 +733,25 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
         }
 
         $store = new cachestore_memcached($name, $configuration);
-        $store->initialise($definition);
+        // If store is ready then only initialise.
+        if ($store->is_ready()) {
+            $store->initialise($definition);
+        }
 
         return $store;
     }
 
     /**
-     * Creates a test instance for unit tests if possible.
-     * @param cache_definition $definition
-     * @return bool|cachestore_memcached
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
      */
-    public static function initialise_unit_test_instance(cache_definition $definition) {
-        if (!self::are_requirements_met()) {
-            return false;
-        }
+    public static function unit_test_configuration() {
+        // If the configuration is not defined correctly, return only the configuration know about.
         if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
-            return false;
+            return [];
         }
-
-        $configuration = array();
-        $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);
-
-        $store = new cachestore_memcached('Test memcached', $configuration);
-        $store->initialise($definition);
-
-        return $store;
+        return ['servers' => explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS)];
     }
 
     /**
index 6a58844..bef5cbe 100644 (file)
@@ -61,11 +61,13 @@ class cachestore_memcached_test extends cachestore_tests {
         $this->resetAfterTest(true);
 
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
-        $instance = cachestore_memcached::initialise_unit_test_instance($definition);
+        $instance = new cachestore_memcached('Memcached Test', cachestore_memcached::unit_test_configuration());
 
-        if (!$instance) { // Something prevented memcached store to be inited (extension, TEST_CACHESTORE_MEMCACHED_TESTSERVERS...).
+        if (!$instance->is_ready()) {
+            // Something prevented memcached store to be inited (extension, TEST_CACHESTORE_MEMCACHED_TESTSERVERS...).
             $this->markTestSkipped();
         }
+        $instance->initialise($definition);
 
         $keys = array(
             // Alphanumeric.
@@ -168,7 +170,7 @@ class cachestore_memcached_test extends cachestore_tests {
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
         $instance = cachestore_memcached::initialise_test_instance($definition);
 
-        if (!$instance) {
+        if (!$instance->is_ready()) {
             $this->markTestSkipped();
         }
 
@@ -181,7 +183,7 @@ class cachestore_memcached_test extends cachestore_tests {
             set_config('testname', $testserver, 'cachestore_memcached');
             set_config('testservers', $testserver, 'cachestore_memcached');
             $checkinstance = cachestore_memcached::initialise_test_instance($definition);
-            if (!$checkinstance) {
+            if (!$checkinstance->is_ready()) {
                 $this->markTestSkipped();
             }
             $checkinstances[] = $checkinstance;
index 641b051..0f1d352 100644 (file)
@@ -571,25 +571,16 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
      * @param cache_definition $definition
      * @return false
      */
-    public static function initialise_unit_test_instance(cache_definition $definition) {
-        if (!self::are_requirements_met()) {
-            return false;
-        }
-        if (!defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
-            return false;
-        }
-
+    public static function unit_test_configuration() {
         $configuration = array();
-        $configuration['servers'] = explode("\n", TEST_CACHESTORE_MONGODB_TESTSERVER);
         $configuration['usesafe'] = 1;
 
-        $store = new cachestore_mongodb('Test mongodb', $configuration);
-        if (!$store->is_ready()) {
-            return false;
+        // If the configuration is not defined correctly, return only the configuration know about.
+        if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
+            $configuration['servers'] = explode("\n", TEST_CACHESTORE_MONGODB_TESTSERVER);
         }
-        $store->initialise($definition);
 
-        return $store;
+        return $configuration;
     }
 
     /**
index 5186175..8e97822 100644 (file)
@@ -56,11 +56,12 @@ class cachestore_mongodb_test extends cachestore_tests {
     public function test_collection_name() {
         // This generates a definition that has a hash starting with a number. MDL-46208.
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_mongodb', 'abc');
-        $instance = cachestore_mongodb::initialise_unit_test_instance($definition);
+        $instance = new cachestore_mongodb('MongoDB_Test', cachestore_mongodb::unit_test_configuration());
 
-        if (!$instance) {
+        if (!$instance->is_ready()) {
             $this->markTestSkipped();
         }
+        $instance->initialise($definition);
 
         $this->assertTrue($instance->set(1, 'alpha'));
         $this->assertTrue($instance->set(2, 'beta'));
diff --git a/cache/stores/redis/README.md b/cache/stores/redis/README.md
new file mode 100755 (executable)
index 0000000..5fa50f6
--- /dev/null
@@ -0,0 +1,6 @@
+Redis Cache Store for Moodle
+============================
+
+A Moodle cache store plugin for [Redis](http://redis.io).
+
+This plugin requires the [PhpRedis](https://github.com/nicolasff/phpredis) extension.  The PhpRedis extension can be installed via PECL with `pecl install redis`.
diff --git a/cache/stores/redis/addinstanceform.php b/cache/stores/redis/addinstanceform.php
new file mode 100755 (executable)
index 0000000..537cbef
--- /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/>.
+
+/**
+ * Redis Cache Store - Add instance form
+ *
+ * @package   cachestore_redis
+ * @copyright 2013 Adam Durana
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/cache/forms.php');
+
+/**
+ * Form for adding instance of Redis Cache Store.
+ *
+ * @copyright   2013 Adam Durana
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_redis_addinstance_form extends cachestore_addinstance_form {
+    /**
+     * Builds the form for creating an instance.
+     */
+    protected function configuration_definition() {
+        $form = $this->_form;
+
+        $form->addElement('text', 'server', get_string('server', 'cachestore_redis'), array('size' => 24));
+        $form->setType('server', PARAM_TEXT);
+        $form->addHelpButton('server', 'server', 'cachestore_redis');
+        $form->addRule('server', get_string('required'), 'required');
+
+        $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_redis'), array('size' => 16));
+        $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
+        $form->addHelpButton('prefix', 'prefix', 'cachestore_redis');
+        $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_redis'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/redis/lang/en/cachestore_redis.php b/cache/stores/redis/lang/en/cachestore_redis.php
new file mode 100755 (executable)
index 0000000..2526bed
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Redis Cache Store - English language strings
+ *
+ * @package   cachestore_redis
+ * @copyright 2013 Adam Durana
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['pluginname'] = 'Redis';
+$string['prefix'] = 'Key prefix';
+$string['prefix_help'] = 'This prefix is used for all key names on the Redis server.
+* If you only have one Moodle instance using this server, you can leave this value default.
+* Due to key length restrictions, a maximum of 5 characters is permitted.';
+$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
+$string['test_server'] = 'Test Server';
+$string['test_server_desc'] = 'Redis server to use for testing.';
+$string['server'] = 'Server';
+$string['server_help'] = 'This sets the hostname or IP address of the Redis server to use.';
\ No newline at end of file
diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php
new file mode 100755 (executable)
index 0000000..16ad4c2
--- /dev/null
@@ -0,0 +1,494 @@
+<?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/>.
+
+/**
+ * Redis Cache Store - Main library
+ *
+ * @package   cachestore_redis
+ * @copyright 2013 Adam Durana
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Redis Cache Store
+ *
+ * To allow separation of definitions in Moodle and faster purging, each cache
+ * is implemented as a Redis hash.  That is a trade-off between having functionality of TTL
+ * and being able to manage many caches in a single redis instance.  Given the recommendation
+ * not to use TTL if at all possible and the benefits of having many stores in Redis using the
+ * hash configuration, the hash implementation has been used.
+ *
+ * @copyright   2013 Adam Durana
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_redis extends cache_store implements cache_is_key_aware, cache_is_lockable,
+        cache_is_configurable, cache_is_searchable {
+    /**
+     * Name of this store.
+     *
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * The definition hash, used for hash key
+     *
+     * @var string
+     */
+    protected $hash;
+
+    /**
+     * Flag for readiness!
+     *
+     * @var boolean
+     */
+    protected $isready = false;
+
+    /**
+     * Cache definition for this store.
+     *
+     * @var cache_definition
+     */
+    protected $definition = null;
+
+    /**
+     * Connection to Redis for this store.
+     *
+     * @var Redis
+     */
+    protected $redis;
+
+    /**
+     * Determines if the requirements for this type of store are met.
+     *
+     * @return bool
+     */
+    public static function are_requirements_met() {
+        return class_exists('Redis');
+    }
+
+    /**
+     * Determines if this type of store supports a given mode.
+     *
+     * @param int $mode
+     * @return bool
+     */
+    public static function is_supported_mode($mode) {
+        return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
+    }
+
+    /**
+     * Get the features of this type of cache store.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_features(array $configuration = array()) {
+        return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE;
+    }
+
+    /**
+     * Get the supported modes of this type of cache store.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_modes(array $configuration = array()) {
+        return self::MODE_APPLICATION + self::MODE_SESSION;
+    }
+
+    /**
+     * Constructs an instance of this type of store.
+     *
+     * @param string $name
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array()) {
+        $this->name = $name;
+
+        if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
+            return;
+        }
+        $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
+        $this->redis = $this->new_redis($configuration['server'], $prefix);
+    }
+
+    /**
+     * Create a new Redis instance and
+     * connect to the server.
+     *
+     * @param string $server The server connection string
+     * @param string $prefix The key prefix
+     * @return Redis
+     */
+    protected function new_redis($server, $prefix = '') {
+        $redis = new Redis();
+        $port = null;
+        if (strpos($server, ':')) {
+            $serverconf = explode(':', $server);
+            $server = $serverconf[0];
+            $port = $serverconf[1];
+        }
+        if ($redis->connect($server, $port)) {
+            $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
+            if (!empty($prefix)) {
+                $redis->setOption(Redis::OPT_PREFIX, $prefix);
+            }
+            // Database setting option...
+            $this->isready = $this->ping($redis);
+        } else {
+            $this->isready = false;
+        }
+        return $redis;
+    }
+
+    /**
+     * See if we can ping Redis server
+     *
+     * @param Redis $redis
+     * @return bool
+     */
+    protected function ping(Redis $redis) {
+        try {
+            if ($redis->ping() === false) {
+                return false;
+            }
+        } catch (Exception $e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Get the name of the store.
+     *
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
+
+    /**
+     * Initialize the store.
+     *
+     * @param cache_definition $definition
+     * @return bool
+     */
+    public function initialise(cache_definition $definition) {
+        $this->definition = $definition;
+        $this->hash       = $definition->generate_definition_hash();
+        return true;
+    }
+
+    /**
+     * Determine if the store is initialized.
+     *
+     * @return bool
+     */
+    public function is_initialised() {
+        return ($this->definition !== null);
+    }
+
+    /**
+     * Determine if the store is ready for use.
+     *
+     * @return bool
+     */
+    public function is_ready() {
+        return $this->isready;
+    }
+
+    /**
+     * Get the value associated with a given key.
+     *
+     * @param string $key The key to get the value of.
+     * @return mixed The value of the key, or false if there is no value associated with the key.
+     */
+    public function get($key) {
+        return $this->redis->hGet($this->hash, $key);
+    }
+
+    /**
+     * Get the values associated with a list of keys.
+     *
+     * @param array $keys The keys to get the values of.
+     * @return array An array of the values of the given keys.
+     */
+    public function get_many($keys) {
+        return $this->redis->hMGet($this->hash, $keys);
+    }
+
+    /**
+     * Set the value of a key.
+     *
+     * @param string $key The key to set the value of.
+     * @param mixed $value The value.
+     * @return bool True if the operation succeeded, false otherwise.
+     */
+    public function set($key, $value) {
+        return ($this->redis->hSet($this->hash, $key, $value) !== false);
+    }
+
+    /**
+     * Set the values of many keys.
+     *
+     * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array
+     *      with two keys, 'key' and 'value'.
+     * @return int The number of key/value pairs successfuly set.
+     */
+    public function set_many(array $keyvaluearray) {
+        $pairs = [];
+        foreach ($keyvaluearray as $pair) {
+            $pairs[$pair['key']] = $pair['value'];
+        }
+        if ($this->redis->hMSet($this->hash, $pairs)) {
+            return count($pairs);
+        }
+        return 0;
+    }
+
+    /**
+     * Delete the given key.
+     *
+     * @param string $key The key to delete.
+     * @return bool True if the delete operation succeeds, false otherwise.
+     */
+    public function delete($key) {
+        return ($this->redis->hDel($this->hash, $key) > 0);
+    }
+
+    /**
+     * Delete many keys.
+     *
+     * @param array $keys The keys to delete.
+     * @return int The number of keys successfully deleted.
+     */
+    public function delete_many(array $keys) {
+        // Redis needs the hash as the first argument, so we have to put it at the start of the array.
+        array_unshift($keys, $this->hash);
+        return call_user_func_array(array($this->redis, 'hDel'), $keys);
+    }
+
+    /**
+     * Purges all keys from the store.
+     *
+     * @return bool
+     */
+    public function purge() {
+        return ($this->redis->del($this->hash) !== false);
+    }
+
+    /**
+     * Cleans up after an instance of the store.
+     */
+    public function instance_deleted() {
+        $this->purge();
+        $this->redis->close();
+        unset($this->redis);
+    }
+
+    /**
+     * Determines if the store has a given key.
+     *
+     * @see cache_is_key_aware
+     * @param string $key The key to check for.
+     * @return bool True if the key exists, false if it does not.
+     */
+    public function has($key) {
+        return $this->redis->hExists($this->hash, $key);
+    }
+
+    /**
+     * Determines if the store has any of the keys in a list.
+     *
+     * @see cache_is_key_aware
+     * @param array $keys The keys to check for.
+     * @return bool True if any of the keys are found, false none of the keys are found.
+     */
+    public function has_any(array $keys) {
+        foreach ($keys as $key) {
+            if ($this->has($key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determines if the store has all of the keys in a list.
+     *
+     * @see cache_is_key_aware
+     * @param array $keys The keys to check for.
+     * @return bool True if all of the keys are found, false otherwise.
+     */
+    public function has_all(array $keys) {
+        foreach ($keys as $key) {
+            if (!$this->has($key)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Tries to acquire a lock with a given name.
+     *
+     * @see cache_is_lockable
+     * @param string $key Name of the lock to acquire.
+     * @param string $ownerid Information to identify owner of lock if acquired.
+     * @return bool True if the lock was acquired, false if it was not.
+     */
+    public function acquire_lock($key, $ownerid) {
+        return $this->redis->setnx($key, $ownerid);
+    }
+
+    /**
+     * Checks a lock with a given name and owner information.
+     *
+     * @see cache_is_lockable
+     * @param string $key Name of the lock to check.
+     * @param string $ownerid Owner information to check existing lock against.
+     * @return mixed True if the lock exists and the owner information matches, null if the lock does not
+     *      exist, and false otherwise.
+     */
+    public function check_lock_state($key, $ownerid) {
+        $result = $this->redis->get($key);
+        if ($result === $ownerid) {
+            return true;
+        }
+        if ($result === false) {
+            return null;
+        }
+        return false;
+    }
+
+    /**
+     * Finds all of the keys being used by this cache store instance.
+     *
+     * @return array of all keys in the hash as a numbered array.
+     */
+    public function find_all() {
+        return $this->redis->hKeys($this->hash);
+    }
+
+    /**
+     * Finds all of the keys whose keys start with the given prefix.
+     *
+     * @param string $prefix
+     *
+     * @return array List of keys that match this prefix.
+     */
+    public function find_by_prefix($prefix) {
+        $return = [];
+        foreach ($this->find_all() as $key) {
+            if (strpos($key, $prefix) === 0) {
+                $return[] = $key;
+            }
+        }
+        return $return;
+    }
+
+    /**
+     * Releases a given lock if the owner information matches.
+     *
+     * @see cache_is_lockable
+     * @param string $key Name of the lock to release.
+     * @param string $ownerid Owner information to use.
+     * @return bool True if the lock is released, false if it is not.
+     */
+    public function release_lock($key, $ownerid) {
+        if ($this->check_lock_state($key, $ownerid)) {
+            return ($this->redis->del($key) !== false);
+        }
+        return false;
+    }
+
+    /**
+     * Creates a configuration array from given 'add instance' form data.
+     *
+     * @see cache_is_configurable
+     * @param stdClass $data
+     * @return array
+     */
+    public static function config_get_configuration_array($data) {
+        return array('server' => $data->server, 'prefix' => $data->prefix);
+    }
+
+    /**
+     * Sets form data from a configuration array.
+     *
+     * @see cache_is_configurable
+     * @param moodleform $editform
+     * @param array $config
+     */
+    public static function config_set_edit_form_data(moodleform $editform, array $config) {
+        $data = array();
+        $data['server'] = $config['server'];
+        $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
+        $editform->set_data($data);
+    }
+
+
+    /**
+     * Creates an instance of the store for testing.
+     *
+     * @param cache_definition $definition
+     * @return mixed An instance of the store, or false if an instance cannot be created.
+     */
+    public static function initialise_test_instance(cache_definition $definition) {
+        if (!self::are_requirements_met()) {
+            return false;
+        }
+        $config = get_config('cachestore_redis');
+        if (empty($config->test_server)) {
+            return false;
+        }
+        $cache = new cachestore_redis('Redis test', ['server' => $config->test_server]);
+        $cache->initialise($definition);
+
+        return $cache;
+    }
+
+    /**
+     * Return configuration to use when unit testing.
+     *
+     * @return array
+     */
+    public static function unit_test_configuration() {
+        global $DB;
+
+        if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
+            throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
+        }
+
+        return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
+                'prefix' => $DB->get_prefix(),
+        ];
+    }
+
+    /**
+     * Returns true if this cache store instance is both suitable for testing, and ready for testing.
+     *
+     * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
+     *
+     * @return bool
+     */
+    public static function ready_to_be_used_for_testing() {
+        return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
+    }
+}
diff --git a/cache/stores/redis/settings.php b/cache/stores/redis/settings.php
new file mode 100755 (executable)
index 0000000..1d10cf8
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Redis Cache Store - Settings
+ *
+ * @package   cachestore_redis
+ * @copyright 2013 Adam Durana
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$settings->add(
+  new admin_setting_configtext(
+    'cachestore_redis/test_server',
+    get_string('test_server', 'cachestore_redis'),
+    get_string('test_server_desc', 'cachestore_redis'),
+    '',
+    PARAM_TEXT,
+    16
+  )
+);
diff --git a/cache/stores/redis/tests/redis_test.php b/cache/stores/redis/tests/redis_test.php
new file mode 100644 (file)
index 0000000..1fa3861
--- /dev/null
@@ -0,0 +1,126 @@
+<?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/>.
+
+/**
+ * Redis cache test.
+ *
+ * If you wish to use these unit tests all you need to do is add the following definition to
+ * your config.php file.
+ *
+ * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
+ *
+ * @package   cachestore_redis
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/../../../tests/fixtures/stores.php');
+require_once(__DIR__.'/../lib.php');
+
+/**
+ * Redis cache test.
+ *
+ * @package   cachestore_redis
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_redis_test extends cachestore_tests {
+    /**
+     * @var cachestore_redis
+     */
+    protected $store;
+
+    /**
+     * Returns the MongoDB class name
+     *
+     * @return string
+     */
+    protected function get_class_name() {
+        return 'cachestore_redis';
+    }
+
+    public function setUp() {
+        if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
+        }
+        parent::setUp();
+    }
+    protected function tearDown() {
+        parent::tearDown();
+
+        if ($this->store instanceof cachestore_redis) {
+            $this->store->purge();
+        }
+    }
+
+    /**
+     * Creates the required cachestore for the tests to run against Redis.
+     *
+     * @return cachestore_redis
+     */
+    protected function create_cachestore_redis() {
+        /** @var cache_definition $definition */
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
+        $store = new cachestore_redis('Test', cachestore_redis::unit_test_configuration());
+        $store->initialise($definition);
+
+        $this->store = $store;
+
+        if (!$store) {
+            $this->markTestSkipped();
+        }
+
+        return $store;
+    }
+
+    public function test_has() {
+        $store = $this->create_cachestore_redis();
+
+        $this->assertTrue($store->set('foo', 'bar'));
+        $this->assertTrue($store->has('foo'));
+        $this->assertFalse($store->has('bat'));
+    }
+
+    public function test_has_any() {
+        $store = $this->create_cachestore_redis();
+
+        $this->assertTrue($store->set('foo', 'bar'));
+        $this->assertTrue($store->has_any(array('bat', 'foo')));
+        $this->assertFalse($store->has_any(array('bat', 'baz')));
+    }
+
+    public function test_has_all() {
+        $store = $this->create_cachestore_redis();
+
+        $this->assertTrue($store->set('foo', 'bar'));
+        $this->assertTrue($store->set('bat', 'baz'));
+        $this->assertTrue($store->has_all(array('foo', 'bat')));
+        $this->assertFalse($store->has_all(array('foo', 'bat', 'this')));
+    }
+
+    public function test_lock() {
+        $store = $this->create_cachestore_redis();
+
+        $this->assertTrue($store->acquire_lock('lock', '123'));
+        $this->assertTrue($store->check_lock_state('lock', '123'));
+        $this->assertFalse($store->check_lock_state('lock', '321'));
+        $this->assertNull($store->check_lock_state('notalock', '123'));
+        $this->assertFalse($store->release_lock('lock', '321'));
+        $this->assertTrue($store->release_lock('lock', '123'));
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/redis/version.php b/cache/stores/redis/version.php
new file mode 100755 (executable)
index 0000000..af51d62
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * Redis Cache Store - Version information
+ *
+ * @package   cachestore_redis
+ * @copyright 2013 Adam Durana
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version    = 2016012600;
+$plugin->requires   = 2015111604; // Requires this Moodle version (3.0.4).
+$plugin->maturity   = MATURITY_STABLE;
+$plugin->component  = 'cachestore_redis';
+$plugin->release    = '3.0.4 (Build: 20160509)';
index d320b8c..f1284f1 100644 (file)
@@ -516,6 +516,14 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
         return $cache;
     }
 
+    /**
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
+     */
+    public static function unit_test_configuration() {
+        return array();
+    }
     /**
      * Returns the name of this instance.
      * @return string
index d1f6f71..ecf06dd 100644 (file)
@@ -492,6 +492,15 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
         return $cache;
     }
 
+    /**
+     * Generates the appropriate configuration required for unit testing.
+     *
+     * @return array Array of unit test configuration data to be used by initialise().
+     */
+    public static function unit_test_configuration() {
+        return array();
+    }
+
     /**
      * Returns the name of this instance.
      * @return string
index 255cbf3..eedc37d 100644 (file)
@@ -1315,8 +1315,10 @@ class core_cache_testcase extends advanced_testcase {
 
         $configfile = $CFG->dataroot.'/muc/config.php';
 
-        // That's right, we're deleting the config file.
-        $this->assertTrue(@unlink($configfile));
+        // The config file will not exist yet as we've not done anything with the cache.
+        // reset_all_data removes the file and without a call to create a configuration it doesn't exist
+        // as yet.
+        $this->assertFileNotExists($configfile);
 
         // Disable the cache
         cache_phpunit_factory::phpunit_disable();
index 1bdf7d7..c6b405b 100644 (file)
@@ -73,13 +73,13 @@ class cache_config_testing extends cache_config_writer {
             if (class_exists($class) && $class::ready_to_be_used_for_testing()) {
                 /* @var cache_store $class */
                 $writer->configstores['test_application'] = array(
-                    'use_test_store' => true,
                     'name' => 'test_application',
                     'plugin' => $expectedstore,
-                    'alt' => $writer->configstores[$defaultapplication],
                     'modes' => $class::get_supported_modes(),
-                    'features' => $class::get_supported_features()
+                    'features' => $class::get_supported_features(),
+                    'configuration' => $class::unit_test_configuration()
                 );
+
                 $defaultapplication = 'test_application';
             }
         }
@@ -535,47 +535,4 @@ class cache_phpunit_factory extends cache_factory {
     public static function phpunit_disable() {
         parent::disable();
     }
-
-    /**
-     * @var bool Whether the warning notice about alternative cache store used has been displayed.
-     */
-    protected $altcachestorenotice = false;
-
-    /**
-     * Creates a store instance given its name and configuration.
-     *
-     * If the store has already been instantiated then the original object will be returned. (reused)
-     *
-     * @param string $name The name of the store (must be unique remember)
-     * @param array $details
-     * @param cache_definition $definition The definition to instantiate it for.
-     * @return boolean|cache_store
-     */
-    public function create_store_from_config($name, array $details, cache_definition $definition) {
-
-        if (isset($details['use_test_store'])) {
-            // name, plugin, alt
-            $class = 'cachestore_'.$details['plugin'];
-            $method = 'initialise_unit_test_instance';
-            if (class_exists($class) && method_exists($class, $method)) {
-                $instance = $class::$method($definition);
-
-                if ($instance) {
-                    return $instance;
-                }
-            }
-
-            // Notify user that alternative store is being used, so action can be taken.
-            if (!$this->altcachestorenotice) {
-                echo PHP_EOL . "++ WARNING: " . 'Failed to use "' . $details['plugin'] . '" cache store, alt "' .
-                    $details['alt']['plugin'] . '" cache store is used.' . PHP_EOL . PHP_EOL;
-                $this->altcachestorenotice = true;
-            }
-            $details = $details['alt'];
-            $details['class'] = 'cachestore_'.$details['plugin'];
-            $name = $details['name'];
-        }
-
-        return parent::create_store_from_config($name, $details, $definition);
-    }
 }
\ No newline at end of file
index 1dbe75a..0eee86e 100644 (file)
@@ -43,39 +43,53 @@ abstract class cachestore_tests extends advanced_testcase {
     abstract protected function get_class_name();
 
     /**
-     * Run the unit tests for the store.
+     * Sets up the fixture, for example, open a network connection.
+     * This method is called before a test is executed.
      */
-    public function test_test_instance() {
+    public function setUp() {
         $class = $this->get_class_name();
-        if (!class_exists($class) || !method_exists($class, 'initialise_test_instance') || !$class::are_requirements_met()) {
+        if (!class_exists($class) || !$class::are_requirements_met()) {
             $this->markTestSkipped('Could not test '.$class.'. Requirements are not met.');
         }
+        parent::setUp();
+    }
+    /**
+     * Run the unit tests for the store.
+     */
+    public function test_test_instance() {
+        $class = $this->get_class_name();
 
         $modes = $class::get_supported_modes();
         if ($modes & cache_store::MODE_APPLICATION) {
             $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
-            $instance = $class::initialise_unit_test_instance($definition);
-            if (!$instance) {
+            $instance = new $class($class.'_test', $class::unit_test_configuration());
+
+            if (!$instance->is_ready()) {
                 $this->markTestSkipped('Could not test '.$class.'. No test instance configured for application caches.');
             } else {
+                $instance->initialise($definition);
                 $this->run_tests($instance);
             }
         }
         if ($modes & cache_store::MODE_SESSION) {
             $definition = cache_definition::load_adhoc(cache_store::MODE_SESSION, $class, 'phpunit_test');
-            $instance = $class::initialise_unit_test_instance($definition);
-            if (!$instance) {
+            $instance = new $class($class.'_test', $class::unit_test_configuration());
+
+            if (!$instance->is_ready()) {
                 $this->markTestSkipped('Could not test '.$class.'. No test instance configured for session caches.');
             } else {
+                $instance->initialise($definition);
                 $this->run_tests($instance);
             }
         }
         if ($modes & cache_store::MODE_REQUEST) {
             $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $class, 'phpunit_test');
-            $instance = $class::initialise_unit_test_instance($definition);
-            if (!$instance) {
+            $instance = new $class($class.'_test', $class::unit_test_configuration());
+
+            if (!$instance->is_ready()) {
                 $this->markTestSkipped('Could not test '.$class.'. No test instance configured for request caches.');
             } else {
+                $instance->initialise($definition);
                 $this->run_tests($instance);
             }
         }
@@ -92,12 +106,25 @@ abstract class cachestore_tests extends advanced_testcase {
         $this->assertTrue($instance->set('test1', 'test1'));
         $this->assertTrue($instance->set('test2', 'test2'));
         $this->assertTrue($instance->set('test3', '3'));
+        $this->assertTrue($instance->set('other3', '3'));
 
         // Test get with a string.
         $this->assertSame('test1', $instance->get('test1'));
         $this->assertSame('test2', $instance->get('test2'));
         $this->assertSame('3', $instance->get('test3'));
 
+        // Test find and find with prefix if this class implements the searchable interface.
+        if ($instance->is_searchable()) {
+            // Extra settings here ignore the return order of the array.
+            $this->assertEquals(['test3', 'test1', 'test2', 'other3'], $instance->find_all(), '', 0, 1, true);
+
+            // Extra settings here ignore the return order of the array.
+            $this->assertEquals(['test2', 'test1', 'test3'], $instance->find_by_prefix('test'), '', 0, 1, true);
+            $this->assertEquals(['test2'], $instance->find_by_prefix('test2'));
+            $this->assertEquals(['other3'], $instance->find_by_prefix('other'));
+            $this->assertEquals([], $instance->find_by_prefix('nothere'));
+        }
+
         // Test set with an int.
         $this->assertTrue($instance->set('test1', 1));
         $this->assertTrue($instance->set('test2', 2));
index c2649c0..e995cff 100644 (file)
@@ -13,6 +13,8 @@ Information provided here is intended especially for developers.
   - cache_store::cleanup()
 * cachestore_dummy::cleanup() has been deprecated.
 * cachestore_dummy::instance_deleted() implemented in lieu of cachestore_dummy::cleanup().
+* Added cache_store::unit_test_configuration() to calculate unit testing configuration.
+* Remove cache_store:initialise_unit_test_instance() as it is incompatible with cache_helper purge functions.
 
 === 3.1 ===
 * Cache stores has a new feature DEREFERENCES_OBJECTS.
index 7352bb8..cf0a4f0 100644 (file)
@@ -103,14 +103,17 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
 }
 
 // Add search form.
-$search  = html_writer::start_tag('form', array('id'=>'searchcohortquery', 'method'=>'get'));
-$search .= html_writer::start_tag('div');
-$search .= html_writer::label(get_string('searchcohort', 'cohort'), 'cohort_search_q'); // No : in form labels!
-$search .= html_writer::empty_tag('input', array('id'=>'cohort_search_q', 'type'=>'text', 'name'=>'search', 'value'=>$searchquery));
-$search .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('search', 'cohort')));
+$search  = html_writer::start_tag('form', array('id'=>'searchcohortquery', 'method'=>'get', 'class' => 'form-inline'));
+$search .= html_writer::start_div('m-b-1');
+$search .= html_writer::label(get_string('searchcohort', 'cohort'), 'cohort_search_q', true,
+        array('class' => 'm-r-1')); // No : in form labels!
+$search .= html_writer::empty_tag('input', array('id' => 'cohort_search_q', 'type' => 'text', 'name' => 'search',
+        'value' => $searchquery, 'class' => 'form-control m-r-1'));
+$search .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('search', 'cohort'),
+        'class' => 'btn btn-secondary'));
 $search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'contextid', 'value'=>$contextid));
 $search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'showall', 'value'=>$showall));
-$search .= html_writer::end_tag('div');
+$search .= html_writer::end_div();
 $search .= html_writer::end_tag('form');
 echo $search;
 
index e900774..7472db1 100644 (file)
@@ -116,6 +116,7 @@ function core_competency_comment_add($comment, $params) {
         }
 
         $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->component = 'moodle';
         $message->name = 'competencyusercompcomment';
         $message->notification = 1;
@@ -184,6 +185,7 @@ function core_competency_comment_add($comment, $params) {
         }
 
         $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->component = 'moodle';
         $message->name = 'competencyplancomment';
         $message->notification = 1;
index b884fc3..5536326 100644 (file)
@@ -2636,7 +2636,7 @@ class course_request {
         $a = new stdClass();
         $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
         $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
-        $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a));
+        $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
 
         return $course->id;
     }
@@ -2672,9 +2672,11 @@ class course_request {
      * @param string $name
      * @param string $subject
      * @param string $message
+     * @param int|null $courseid
      */
-    protected function notify($touser, $fromuser, $name='courserequested', $subject, $message) {
-        $eventdata = new stdClass();
+    protected function notify($touser, $fromuser, $name='courserequested', $subject, $message, $courseid = null) {
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = empty($courseid) ? SITEID : $courseid;
         $eventdata->component         = 'moodle';
         $eventdata->name              = $name;
         $eventdata->userfrom          = $fromuser;
index a1c9922..8861928 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 $string['encoding'] = 'File encoding';
-$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction'] = 'Enrolment expiry action';
 $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['filelockedmail'] = 'The text file you are using for file-based enrolments ({$a}) can not be deleted by the cron process.  This usually means the permissions are wrong on it.  Please fix the permissions so that Moodle can delete the file, otherwise it might be processed repeatedly.';
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
index 57438fb..4bff2a9 100644 (file)
@@ -186,7 +186,8 @@ class enrol_flatfile_plugin extends enrol_plugin {
 
         if ($processed and $mailadmins) {
             if ($log = $buffer->get_buffer()) {
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid          = SITEID;
                 $eventdata->modulename        = 'moodle';
                 $eventdata->component         = 'enrol_flatfile';
                 $eventdata->name              = 'flatfile_enrolment';
@@ -340,7 +341,8 @@ class enrol_flatfile_plugin extends enrol_plugin {
         }
 
         if (!unlink($filelocation)) {
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = SITEID;
             $eventdata->modulename        = 'moodle';
             $eventdata->component         = 'enrol_flatfile';
             $eventdata->name              = 'flatfile_enrolment';
@@ -463,7 +465,8 @@ class enrol_flatfile_plugin extends enrol_plugin {
                 $a->profileurl = "$CFG->wwwroot/user/view.php?id=$user->id&amp;course=$course->id";
                 $subject = get_string('enrolmentnew', 'enrol', format_string($course->shortname, true, array('context' => $context)));
 
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid          = $course->id;
                 $eventdata->modulename        = 'moodle';
                 $eventdata->component         = 'enrol_flatfile';
                 $eventdata->name              = 'flatfile_enrolment';
@@ -494,7 +497,8 @@ class enrol_flatfile_plugin extends enrol_plugin {
                 $a->user = fullname($user);
                 $subject = get_string('enrolmentnew', 'enrol', format_string($course->shortname, true, array('context' => $context)));
 
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid          = $course->id;
                 $eventdata->modulename        = 'moodle';
                 $eventdata->component         = 'enrol_flatfile';
                 $eventdata->name              = 'flatfile_enrolment';
index ede2700..573606f 100644 (file)
@@ -222,7 +222,8 @@ class enrol_imsenterprise_plugin extends enrol_plugin {
                 $msg .= "Logging is currently not active.";
             }
 
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = SITEID;
             $eventdata->modulename        = 'moodle';
             $eventdata->component         = 'enrol_imsenterprise';
             $eventdata->name              = 'imsenterprise_enrolment';
index 70a3d3e..5d8b877 100644 (file)
@@ -29,7 +29,7 @@ $string['autocreate'] = '<p>Courses can be created automatically if there are en
 $string['autocreate_key'] = 'Auto create';
 $string['autocreation_settings'] = 'Automatic course creation settings';
 $string['autoupdate_settings'] = 'Automatic course update settings';
-$string['autoupdate_settings_desc'] = '<p>Select fields to update when synchronization script is running (enrol/ldap/cli/sync.php).</p><p>When at least one field is selected an update will occur.</p>';
+$string['autoupdate_settings_desc'] = '<p>Select fields to update when synchronisation script is running (enrol/ldap/cli/sync.php).</p><p>When at least one field is selected an update will occur.</p>';
 $string['bind_dn'] = 'If you want to use a bind user to search users, specify it here. Someting like \'cn=ldapuser,ou=public,o=org\'';
 $string['bind_dn_key'] = 'Bind user distinguished name';
 $string['bind_pw'] = 'Password for the bind user';
@@ -47,7 +47,7 @@ $string['couldnotfinduser'] = "Could not find user '{\$a}', skipping\n";
 $string['coursenotexistskip'] = "Course '{\$a}' does not exist and autocreation disabled, skipping\n";
 $string['course_fullname'] = 'Optional: LDAP attribute to get the full name from';
 $string['course_fullname_key'] = 'Full name';
-$string['course_fullname_updateonsync'] = 'Update full name during synchronization script';
+$string['course_fullname_updateonsync'] = 'Update full name during synchronisation script';
 $string['course_fullname_updateonsync_key'] = 'Update full name';
 $string['course_idnumber'] = 'LDAP attribute to get the course ID number from. Usually \'cn\' or \'uid\'.';
 $string['course_idnumber_key'] = 'ID number';
@@ -56,11 +56,11 @@ $string['course_search_sub_key'] = 'Search subcontexts';
 $string['course_settings'] = 'Course enrolment settings';
 $string['course_shortname'] = 'Optional: LDAP attribute to get the shortname from';
 $string['course_shortname_key'] = 'Short name';
-$string['course_shortname_updateonsync'] = 'Update short name during synchronization script';
+$string['course_shortname_updateonsync'] = 'Update short name during synchronisation script';
 $string['course_shortname_updateonsync_key'] = 'Update short name';
 $string['course_summary'] = 'Optional: LDAP attribute to get the summary from';
 $string['course_summary_key'] = 'Summary';
-$string['course_summary_updateonsync'] = 'Update summary during synchronization script';
+$string['course_summary_updateonsync'] = 'Update summary during synchronisation script';
 $string['course_summary_updateonsync_key'] = 'Update summary';
 $string['createcourseextid'] = 'CREATE User enrolled to a nonexistant course \'{$a->courseextid}\'';
 $string['createnotcourseextid'] = 'User enrolled to a nonexistant course \'{$a->courseextid}\'';
index 0ceec89..c832b1b 100644 (file)
@@ -287,6 +287,9 @@ class tool_provider extends ToolProvider {
             helper::update_user_profile_image($user->id, $image);
         }
 
+        // Check if we need to force the page layout to embedded.
+        $isforceembed = $this->resourceLink->getSetting('custom_force_embed') == 1;
+
         // Check if we are an instructor.
         $isinstructor = $this->user->isStaff() || $this->user->isAdmin();
 
@@ -294,25 +297,27 @@ class tool_provider extends ToolProvider {
             $courseid = $context->instanceid;
             $urltogo = new moodle_url('/course/view.php', ['id' => $courseid]);
 
-            // May still be set from previous session, so unset it.
-            unset($SESSION->forcepagelayout);
         } else if ($context->contextlevel == CONTEXT_MODULE) {
             $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
             $urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]);
 
             // If we are a student in the course module context we do not want to display blocks.
-            if (!$isinstructor) {
-                // Force the page layout.
-                $SESSION->forcepagelayout = 'embedded';
-            } else {
-                // May still be set from previous session, so unset it.
-                unset($SESSION->forcepagelayout);
+            if (!$isforceembed && !$isinstructor) {
+                $isforceembed = true;
             }
         } else {
             print_error('invalidcontext');
             exit();
         }
 
+        // Force page layout to embedded if necessary.
+        if ($isforceembed) {
+            $SESSION->forcepagelayout = 'embedded';
+        } else {
+            // May still be set from previous session, so unset it.
+            unset($SESSION->forcepagelayout);
+        }
+
         // Enrol the user in the course with no role.
         $result = helper::enrol_user($tool, $user->id);
 
index 7b126b8..3cf57e8 100644 (file)
@@ -47,7 +47,9 @@ $string['maxenrolled'] = 'Maximum enrolled users';
 $string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.';
 $string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.';
 $string['membersync'] = 'User synchronisation';
-$string['membersync_help'] = 'Whether an account is created for each remote user and the user is enrolled in this course.';
+$string['membersync_help'] = 'Whether a scheduled task synchronises enrolled users in the remote system with enrolments in this course, creating an account for each remote user as necessary, and enrolling or unenrolling them as required.
+
+If set to no, at the moment when a remote user accesses the tool, an account will be created for them and they will be automatically enrolled.';
 $string['membersyncmode'] = 'User synchronisation mode';
 $string['membersyncmode_help'] = 'Whether remote users should be enrolled and/or unenrolled from this course.';
 $string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing users';
index a018532..f3436cf 100644 (file)
@@ -74,7 +74,7 @@ if ($ltiversion === \IMSGlobal\LTI\ToolProvider\ToolProvider::LTI_VERSION1) {
     $consumer->ltiVersion = \IMSGlobal\LTI\ToolProvider\ToolProvider::LTI_VERSION1;
     // For LTIv1, set the tool secret as the consumer secret.
     $consumer->secret = $tool->secret;
-    $consumer->name = optional_param('tool_consumer_instance_name', null, PARAM_TEXT);
+    $consumer->name = optional_param('tool_consumer_instance_name', '', PARAM_TEXT);
     $consumer->consumerName = $consumer->name;
     $consumer->consumerGuid = optional_param('tool_consumer_instance_guid', null, PARAM_TEXT);
     $consumer->consumerVersion = optional_param('tool_consumer_info_version', null, PARAM_TEXT);
index b325f4a..2b3c572 100644 (file)
@@ -37,7 +37,7 @@ $string['deleteselectedusers'] = 'Delete selected user enrolments';
 $string['editselectedusers'] = 'Edit selected user enrolments';
 $string['enrolledincourserole'] = 'Enrolled in "{$a->course}" as "{$a->role}"';
 $string['enrolusers'] = 'Enrol users';
-$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction'] = 'Enrolment expiry action';
 $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['expirymessageenrollersubject'] = 'Enrolment expiry notification';
 $string['expirymessageenrollerbody'] = 'Enrolment in the course \'{$a->course}\' will expire within the next {$a->threshold} for the following users:
index e563b60..1415f57 100644 (file)
@@ -332,7 +332,7 @@ class enrol_meta_plugin extends enrol_plugin {
 
         $options = array(
             'requiredcapabilities' => array('enrol/meta:selectaslinked'),
-            'multiple' => true,
+            'multiple' => empty($instance->id),  // We only accept multiple values on creation.
             'exclude' => $excludelist
         );
         $mform->addElement('course', 'customint1', get_string('linkedcourse', 'enrol_meta'), $options);
@@ -362,15 +362,24 @@ class enrol_meta_plugin extends enrol_plugin {
         $c = false;
 
         if (!empty($data['customint1'])) {
-            foreach ($data['customint1'] as $courseid) {
+            $courses = is_array($data['customint1']) ? $data['customint1'] : [$data['customint1']];
+            foreach ($courses as $courseid) {
                 $c = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
                 $coursecontext = context_course::instance($c->id);
-                $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
+
+                $sqlexists = 'enrol = :meta AND courseid = :currentcourseid AND customint1 = :courseid AND id != :id';
+                $existing = $DB->record_exists_select('enrol', $sqlexists, [
+                    'meta' => 'meta',
+                    'currentcourseid' => $thiscourseid,
+                    'courseid' => $c->id,
+                    'id' => $instance->id
+                ]);
+
                 if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
                     $errors['customint1'] = get_string('error');
                 } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
                     $errors['customint1'] = get_string('error');
-                } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
+                } else if ($c->id == SITEID or $c->id == $thiscourseid or $existing) {
                     $errors['customint1'] = get_string('error');
                 }
             }
index a07ae24..6f91abd 100644 (file)
@@ -51,7 +51,8 @@ final class util {
             $message .= "$key => $value\n";
         }
 
-        $eventdata = new \stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = empty($data->courseid) ? SITEID : $data->courseid;
         $eventdata->modulename        = 'moodle';
         $eventdata->component         = 'enrol_paypal';
         $eventdata->name              = 'paypal_enrolment';
index 809cdb7..ccbcd0b 100644 (file)
@@ -85,7 +85,7 @@ if (! $course = $DB->get_record("course", array("id"=>$data->courseid))) {
 }
 
 if (! $context = context_course::instance($course->id, IGNORE_MISSING)) {
-    message_paypal_error_to_admin("Not a valid context id", $data);
+    \enrol_paypal\util::message_paypal_error_to_admin("Not a valid context id", $data);
     die;
 }
 
@@ -152,7 +152,8 @@ if (strlen($result) > 0) {
         // Email user to let them know. Email admin.
 
         if ($data->payment_status == "Pending" and $data->pending_reason != "echeck") {
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = empty($data->courseid) ? SITEID : $data->courseid;
             $eventdata->modulename        = 'moodle';
             $eventdata->component         = 'enrol_paypal';
             $eventdata->name              = 'paypal_enrolment';
@@ -259,7 +260,8 @@ if (strlen($result) > 0) {
             $a->coursename = format_string($course->fullname, true, array('context' => $coursecontext));
             $a->profileurl = "$CFG->wwwroot/user/view.php?id=$user->id";
 
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = $course->id;
             $eventdata->modulename        = 'moodle';
             $eventdata->component         = 'enrol_paypal';
             $eventdata->name              = 'paypal_enrolment';
@@ -278,7 +280,8 @@ if (strlen($result) > 0) {
             $a->course = format_string($course->fullname, true, array('context' => $coursecontext));
             $a->user = fullname($user);
 
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = $course->id;
             $eventdata->modulename        = 'moodle';
             $eventdata->component         = 'enrol_paypal';
             $eventdata->name              = 'paypal_enrolment';
@@ -297,7 +300,8 @@ if (strlen($result) > 0) {
             $a->user = fullname($user);
             $admins = get_admins();
             foreach ($admins as $admin) {
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid          = $course->id;
                 $eventdata->modulename        = 'moodle';
                 $eventdata->component         = 'enrol_paypal';
                 $eventdata->name              = 'paypal_enrolment';
@@ -319,4 +323,3 @@ if (strlen($result) > 0) {
 }
 
 exit;
-
index e3f8e0b..aa9c7a5 100644 (file)
@@ -39,7 +39,7 @@ $string['enrolperiod_desc'] = 'Default length of time that the enrolment is vali
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can be enrolled from this date onward only.';
-$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction'] = 'Enrolment expiry action';
 $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['mailadmins'] = 'Notify admin';
 $string['mailstudents'] = 'Notify students';
index 480489f..583955a 100644 (file)
@@ -48,7 +48,7 @@ $string['enrolperiod_desc'] = 'Default length of time that the enrolment is vali
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves. If disabled, the enrolment duration will be unlimited.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can enrol themselves from this date onward only.';
-$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction'] = 'Enrolment expiry action';
 $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['expirymessageenrollersubject'] = 'Self enrolment expiry notification';
 $string['expirymessageenrollerbody'] = 'Self enrolment in the course \'{$a->course}\' will expire within the next {$a->threshold} for the following users:
index 721f141..2e3280d 100644 (file)
@@ -11,7 +11,8 @@
         }
 
         // Send the message and redirect.
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid         = SITEID;
         $eventdata->component        = 'moodle';
         $eventdata->name             = 'errors';
         $eventdata->userfrom          = $USER;
index 7f23471..9fd0882 100644 (file)
@@ -1083,7 +1083,7 @@ $string['timezoneisforcedto'] = 'Force all users to use';
 $string['timezonenotforced'] = 'Users can choose their own timezone';
 $string['timezonephpdefault'] = 'Default PHP timezone ({$a})';
 $string['timezoneserver'] = 'Server timezone ({$a})';
-$string['tlswarning'] = 'No PHP/cURL extension with TLSv1.2 support has been detected. Some services may not work. It is strongly recommentd that you upgrade your TLS libraries.';
+$string['tlswarning'] = 'No PHP/cURL extension with TLSv1.2 support has been detected. Some services may not work. It is strongly recommended to upgrade your TLS libraries.';
 $string['tokenizerrecommended'] = 'Installing the optional PHP Tokenizer extension is recommended -- it improves Moodle Networking functionality.';
 $string['tools'] = 'Admin tools';
 $string['toolsmanage'] = 'Manage admin tools';
index f540be7..fdebdc1 100644 (file)
@@ -47,13 +47,13 @@ $string['auth_passwordwillexpire'] = 'Your password will expire in {$a} days. Do
 $string['auth_remove_delete'] = 'Full delete internal';
 $string['auth_remove_keep'] = 'Keep internal';
 $string['auth_remove_suspend'] = 'Suspend internal';
-$string['auth_remove_user'] = 'Specify what to do with internal user account during mass synchronization when user was removed from external source. Only suspended users are automatically revived if they reappear in ext source.';
+$string['auth_remove_user'] = 'Specify what to do with internal user account during mass synchronisation when user was removed from external source. Only suspended users are automatically restored if they reappear in the external source.';
 $string['auth_remove_user_key'] = 'Removed ext user';
 $string['auth_sync_suspended']  = 'When enabled, the suspended attribute will be used to update the local user account\'s suspension status.';
-$string['auth_sync_suspended_key'] = 'Synchronize local user suspension status';
-$string['auth_sync_script'] = 'User account syncronisation';
+$string['auth_sync_suspended_key'] = 'Synchronise local user suspension status';
+$string['auth_sync_script'] = 'User account synchronisation';
 $string['auth_updatelocal'] = 'Update local';
-$string['auth_updatelocal_expl'] = '<p><b>Update local:</b> If enabled, the field will be updated (from external auth) every time the user logs in or there is a user synchronization. Fields set to update locally should be locked.</p>';
+$string['auth_updatelocal_expl'] = '<p><b>Update local:</b> If enabled, the field will be updated (from external auth) every time the user logs in or there is a user synchronisation. Fields set to update locally should be locked.</p>';
 $string['auth_updateremote'] = 'Update external';
 $string['auth_updateremote_expl'] = '<p><b>Update external:</b> If enabled, the external auth will be updated when the user record is updated. Fields should be unlocked to allow edits.</p>';
 $string['auth_updateremote_ldap'] = '<p><b>Note:</b> Updating external LDAP data requires that you set binddn and bindpw to a bind-user with editing privileges to all the user records. It currently does not preserve multi-valued attributes, and will remove extra values on update. </p>';
index 27b8921..6ea99fe 100644 (file)
@@ -393,7 +393,8 @@ $string['site:doanything'] = 'Allowed to do everything';
 $string['site:doclinks'] = 'Show links to offsite docs';
 $string['site:forcelanguage'] = 'Override course language';
 $string['site:import'] = 'Import other courses into a course';
-$string['site:maintenanceaccess'] = 'Allowed access when maintenance mode is enabled.';
+$string['site:maintenanceaccess'] = 'Access site while in maintenance mode';
+$string['site:manageallmessaging'] = 'Can perform all messaging actions on site';
 $string['site:manageblocks'] = 'Manage blocks on a page';
 $string['site:mnetloginfromremote'] = 'Login from a remote application via MNet';
 $string['site:mnetlogintoremote'] = 'Roam to a remote application via MNet';
index 6fe2947..372f328 100644 (file)
Binary files a/lib/amd/build/inplace_editable.min.js and b/lib/amd/build/inplace_editable.min.js differ
index 4170825..bc79fda 100644 (file)
@@ -129,7 +129,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                         attr('id', uniqueId('id_inplacevalue_', 20)).
                         attr('value', el.attr('data-value')).
                         attr('aria-describedby', instr.attr('id')).
-                        addClass('ignoredirty'),
+                        addClass('ignoredirty').
+                        addClass('form-control'),
                     lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>').
                         attr('for', inputelement.attr('id'));
                 el.html('').append(instr).append(lbl).append(inputelement);
@@ -163,8 +164,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
 
         var turnEditingOnSelect = function(el, options) {
             var i,
-                inputelement = $('<select></select>')
-                    .attr('id', uniqueId('id_inplacevalue_', 20)),
+                inputelement = $('<select></select>').
+                    attr('id', uniqueId('id_inplacevalue_', 20)).
+                    addClass('custom-select'),
                 lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>')
                     .attr('for', inputelement.attr('id'));
             for (i in options) {
index d77d739..70ba3ba 100644 (file)
@@ -722,7 +722,8 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
     $plaintext = html_to_text($message);
 
     // Notify recipient.
-    $eventdata = new stdClass();
+    $eventdata = new \core\message\message();
+    $eventdata->courseid          = is_null($badge->courseid) ? SITEID : $badge->courseid; // Profile/site come with no courseid.
     $eventdata->component         = 'moodle';
     $eventdata->name              = 'badgerecipientnotice';
     $eventdata->userfrom          = $userfrom;
@@ -758,7 +759,8 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
         $creatormessage = get_string('creatorbody', 'badges', $a);
         $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
 
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = $badge->courseid;
         $eventdata->component         = 'moodle';
         $eventdata->name              = 'badgecreatornotice';
         $eventdata->userfrom          = $userfrom;
index 5f4186c..a93e56d 100644 (file)
@@ -122,7 +122,8 @@ abstract class scanner {
         $subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
         $admins = get_admins();
         foreach ($admins as $admin) {
-            $eventdata = new \stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid          = SITEID;
             $eventdata->component         = 'moodle';
             $eventdata->name              = 'errors';
             $eventdata->userfrom          = get_admin();
index 0d58ddb..e330919 100644 (file)
@@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die();
  *      Extra information about event.
  *
  *      - int messageid: the id of the message.
+ *      - int courseid: the id of the related course.
  * }
  *
  * @package    core
@@ -43,12 +44,14 @@ defined('MOODLE_INTERNAL') || die();
 class message_sent extends base {
     /**
      * Create event using ids.
+     * @todo MDL-55449 Make $courseid mandatory in Moodle 3.6
      * @param int $userfromid
      * @param int $usertoid
      * @param int $messageid
+     * @param int|null $courseid course id the event is related with. Use SITEID if no relation exists.
      * @return message_sent
      */
-    public static function create_from_ids($userfromid, $usertoid, $messageid) {
+    public static function create_from_ids($userfromid, $usertoid, $messageid, $courseid = null) {
         // We may be sending a message from the 'noreply' address, which means we are not actually sending a
         // message from a valid user. In this case, we will set the userid to 0.
         // Check if the userid is valid.
@@ -56,6 +59,15 @@ class message_sent extends base {
             $userfromid = 0;
         }
 
+        // TODO: MDL-55449 Make $courseid mandatory in Moodle 3.6.
+        if (is_null($courseid)) {
+            // Arrived here with not defined $courseid to associate the event with.
+            // Let's default to SITEID and perform debugging so devs are aware. MDL-47162.
+            $courseid = SITEID;
+            debugging('message_sent::create_from_ids() needs a $courseid to be passed, nothing was detected. Please, change ' .
+                    'the call to include it, using SITEID if the message is unrelated to any real course.', DEBUG_DEVELOPER);
+        }
+
         $event = self::create(array(
             'userid' => $userfromid,
             'context' => \context_system::instance(),
@@ -64,7 +76,8 @@ class message_sent extends base {
                 // In earlier versions it can either be the id in the 'message_read' or 'message' table.
                 // Now it is always the id from 'message' table. Please note that the record is still moved
                 // to the 'message_read' table later when message marked as read.
-                'messageid' => $messageid
+                'messageid' => $messageid,
+                'courseid' => $courseid
             )
         ));
 
@@ -143,6 +156,10 @@ class message_sent extends base {
         if (!isset($this->other['messageid'])) {
             throw new \coding_exception('The \'messageid\' value must be set in other.');
         }
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
     }
 
     public static function get_objectid_mapping() {
@@ -155,6 +172,7 @@ class message_sent extends base {
         $othermapped = array();
         // The messages table could vary for older events - so cannot be mapped.
         $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['courseid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
index 234cc93..21e046a 100644 (file)
@@ -50,7 +50,8 @@ class manager {
      *
      * NOTE: to be used from message_send() only.
      *
-     * @param \stdClass|\core\message\message $eventdata fully prepared event data for processors
+     * @todo MDL-55449 Drop support for stdClass in Moodle 3.6
+     * @param \core\message\message $eventdata fully prepared event data for processors
      * @param \stdClass $savemessage the message saved in 'message' table
      * @param array $processorlist list of processors for target user
      * @return int $messageid the id from 'message' or 'message_read' table (false is not returned)
@@ -58,16 +59,31 @@ class manager {
     public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) {
         global $CFG;
 
+        // TODO MDL-55449 Drop support for stdClass in Moodle 3.6.
         if (!($eventdata instanceof \stdClass) && !($eventdata instanceof message)) {
             // Not a valid object.
             throw new \coding_exception('Message should be of type stdClass or \core\message\message');
         }
 
+        // TODO MDL-55449 Drop support for stdClass in Moodle 3.6.
+        if ($eventdata instanceof \stdClass) {
+            if (!isset($eventdata->courseid)) {
+                $eventdata->courseid = null;
+            }
+
+            debugging('eventdata as \stdClass is deprecated. Please use \core\message\message instead.', DEBUG_DEVELOPER);
+        }
+
         require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 
         if (empty($processorlist)) {
             // Trigger event for sending a message - we need to do this before marking as read!
-            \core\event\message_sent::create_from_ids($eventdata->userfrom->id, $eventdata->userto->id, $savemessage->id)->trigger();
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+                )->trigger();
 
             if ($savemessage->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
@@ -132,7 +148,12 @@ class manager {
         }
 
         // Trigger event for sending a message - must be done before marking as read.
-        \core\event\message_sent::create_from_ids($eventdata->userfrom->id, $eventdata->userto->id, $savemessage->id)->trigger();
+        \core\event\message_sent::create_from_ids(
+            $eventdata->userfrom->id,
+            $eventdata->userto->id,
+            $savemessage->id,
+            $eventdata->courseid
+            )->trigger();
 
         if (empty($CFG->messaging)) {
             // If messaging is disabled and they previously had forum notifications handled by the popup processor
index f75cce9..107335d 100644 (file)
@@ -56,6 +56,12 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class message {
+    /** @var int Course id. */
+    private $courseid;
+
+    /** @var string Module name. */
+    private $modulename;
+
     /** @var string Component name. */
     private $component;
 
@@ -104,10 +110,31 @@ class message {
     /** @var  string Name of the attachment. Note:- not all processors support this.*/
     private $attachname;
 
+    /** @var  int The time the message was created.*/
+    private $timecreated;
+
     /** @var array a list of properties that is allowed for each message. */
-    private $properties = array('component', 'name', 'userfrom', 'userto', 'subject', 'fullmessage', 'fullmessageformat',
-                                'fullmessagehtml', 'smallmessage', 'notification', 'contexturl', 'contexturlname', 'savedmessageid',
-                                'replyto', 'attachment', 'attachname');
+    private $properties = array(
+        'courseid',
+        'modulename',
+        'component',
+        'name',
+        'userfrom',
+        'userto',
+        'subject',
+        'fullmessage',
+        'fullmessageformat',
+        'fullmessagehtml',
+        'smallmessage',
+        'notification',
+        'contexturl',
+        'contexturlname',
+        'replyto',
+        'savedmessageid',
+        'attachment',
+        'attachname',
+        'timecreated'
+        );
 
     /** @var array property to store any additional message processor specific content */
     private $additionalcontent = array();
index 047ad73..9f56863 100644 (file)
@@ -1744,7 +1744,7 @@ class core_plugin_manager {
             ),
 
             'cachestore' => array(
-                'file', 'memcache', 'memcached', 'mongodb', 'session', 'static', 'apcu'
+                'file', 'memcache', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
             ),
 
             'calendartype' => array(
index b5c701a..ba19c15 100644 (file)
@@ -798,7 +798,8 @@ class checker {
             array('style' => 'font-size:smaller; color:#333;')));
 
         foreach ($admins as $admin) {
-            $message = new \stdClass();
+            $message = new \core\message\message();
+            $message->courseid          = SITEID;
             $message->component         = 'moodle';
             $message->name              = 'availableupdate';
             $message->userfrom          = get_admin();
index f371558..4f529bd 100644 (file)
@@ -77,6 +77,17 @@ $capabilities = array(
         )
     ),
 
+    'moodle/site:manageallmessaging' => array(
+
+        'riskbitmask' => RISK_PERSONAL,
+
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW
+        )
+    ),
+
     'moodle/site:deleteanymessage' => array(
 
         'riskbitmask' => RISK_DATALOSS,
index 6ef9c71..4849b07 100644 (file)
@@ -2655,7 +2655,8 @@ abstract class enrol_plugin {
         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = $ue->courseid;
         $message->notification      = 1;
         $message->component         = 'enrol_'.$name;
         $message->name              = 'expiry_notification';
@@ -2716,7 +2717,8 @@ abstract class enrol_plugin {
         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = $course->id;
         $message->notification      = 1;
         $message->component         = 'enrol_'.$name;
         $message->name              = 'expiry_notification';
index 787db39..0e501ab 100644 (file)
@@ -121,8 +121,11 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
         // Enhance the select with javascript.
         $this->_generateId();
         $id = $this->getAttribute('id');
-        $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#' . $id, $this->tags, $this->ajax,
-            $this->placeholder, $this->casesensitive, $this->showsuggestions, $this->noselectionstring));
+
+        if (!$this->isFrozen()) {
+            $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#' . $id, $this->tags, $this->ajax,
+                $this->placeholder, $this->casesensitive, $this->showsuggestions, $this->noselectionstring));
+        }
 
         return parent::toHTML();
     }
index 0d83294..2409966 100644 (file)
@@ -408,6 +408,9 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
         }
         $context['hasformats'] = count($formats) > 1;
         $context['formats'] = [];
+        if (($format === '' || $format === null) && count($formats)) {
+            $format = key($formats);
+        }
         foreach ($formats as $formatvalue => $formattext) {
             $context['formats'][] = ['value' => $formatvalue, 'text' => $formattext, 'selected' => ($formatvalue == $format)];
         }
index 3796d01..da9359d 100644 (file)
@@ -166,6 +166,7 @@ class MoodleQuickForm_group extends HTML_QuickForm_group implements templatable
 
         $elements = [];
         $name = $this->getName();
+        $i = 0;
         foreach ($this->_elements as $key => $element) {
             $elementname = '';
             if ($this->_appendName) {
@@ -186,12 +187,30 @@ class MoodleQuickForm_group extends HTML_QuickForm_group implements templatable
                 $element->accept($renderer);
                 $out = $renderer->toHtml();
             }
-            $elements[] = $out;
+
+            // Replicates the separator logic from 'pear/HTML/QuickForm/Renderer/Default.php'.
+            $separator = '';
+            if ($i > 0) {
+                if (is_array($this->_separator)) {
+                    $separator = $this->_separator[($i - 1) % count($this->_separator)];
+                } else if ($this->_separator === null) {
+                    $separator = '&nbsp;';
+                } else {
+                    $separator = (string) $this->_separator;
+                }
+            }
+
+            $elements[] = [
+                'separator' => $separator,
+                'html' => $out
+            ];
+
             // Restore the element's name.
             if ($this->_appendName) {
                 $element->setName($elementname);
             }
 
+            $i++;
         }
 
         $context['elements'] = $elements;
index d0a03dc..e0f8cf9 100644 (file)
@@ -200,6 +200,8 @@ class MoodleQuickForm_select extends HTML_QuickForm_select implements templatabl
         $context = $this->export_for_template_base($output);
 
         $options = [];
+        // Standard option attributes.
+        $standardoptionattributes = ['text', 'value', 'selected', 'disabled'];
         foreach ($this->_options as $option) {
             if (is_array($this->_values) && in_array( (string) $option['attr']['value'], $this->_values)) {
                 $this->_updateAttrArray($option['attr'], ['selected' => 'selected']);
@@ -207,8 +209,17 @@ class MoodleQuickForm_select extends HTML_QuickForm_select implements templatabl
             $o = [
                 'text' => $option['text'],
                 'value' => $option['attr']['value'],
-                'selected' => !empty($option['attr']['selected'])
+                'selected' => !empty($option['attr']['selected']),
+                'disabled' => !empty($option['attr']['disabled']),
             ];
+            // Set other attributes.
+            $otheroptionattributes = [];
+            foreach ($option['attr'] as $attr => $value) {
+                if (!in_array($attr, $standardoptionattributes) && $attr != 'class' && !is_object($value)) {
+                    $otheroptionattributes[] = $attr . '="' . s($value) . '"';
+                }
+            }
+            $o['optionattributes'] = implode(' ', $otheroptionattributes);
             $options[] = $o;
         }
         $context['options'] = $options;
index ab547cf..9ed4d05 100644 (file)
@@ -522,6 +522,8 @@ class MoodleQuickForm_selectgroups extends HTML_QuickForm_element implements tem
             ];
         }
 
+        // Standard option attributes.
+        $standardoptionattributes = ['text', 'value', 'selected', 'disabled'];
         foreach ($this->_optGroups as $group) {
             $options = [];
 
@@ -533,6 +535,15 @@ class MoodleQuickForm_selectgroups extends HTML_QuickForm_element implements tem
                     $o['selected'] = false;
                 }
                 $o['text'] = $option['text'];
+                $o['disabled'] = !empty($option['attr']['disabled']);
+                // Set other attributes.
+                $otheroptionattributes = [];
+                foreach ($option['attr'] as $attr => $value) {
+                    if (!in_array($attr, $standardoptionattributes) && $attr != 'class' && !is_object($value)) {
+                        $otheroptionattributes[] = $attr . '="' . s($value) . '"';
+                    }
+                }
+                $o['optionattributes'] = implode(' ', $otheroptionattributes);
                 $options[] = $o;
             }
 
index 1643e6f..fc4a915 100644 (file)
@@ -231,6 +231,8 @@ class MoodleQuickForm_selectwithlink extends HTML_QuickForm_select implements te
         $context = $this->export_for_template_base($output);
 
         $options = [];
+        // Standard option attributes.
+        $standardoptionattributes = ['text', 'value', 'selected', 'disabled'];
         foreach ($this->_options as $option) {
             if (is_array($this->_values) && in_array( (string) $option['attr']['value'], $this->_values)) {
                 $this->_updateAttrArray($option['attr'], ['selected' => 'selected']);
@@ -238,8 +240,17 @@ class MoodleQuickForm_selectwithlink extends HTML_QuickForm_select implements te
             $o = [
                 'text' => $option['text'],
                 'value' => $option['attr']['value'],
-                'selected' => !empty($option['attr']['selected'])
+                'selected' => !empty($option['attr']['selected']),
+                'disabled' => !empty($option['attr']['disabled']),
             ];
+            // Set other attributes.
+            $otheroptionattributes = [];
+            foreach ($option['attr'] as $attr => $value) {
+                if (!in_array($attr, $standardoptionattributes) && $attr != 'class' && !is_object($value)) {
+                    $otheroptionattributes[] = $attr . '="' . s($value) . '"';
+                }
+            }
+            $o['optionattributes'] = implode(' ', $otheroptionattributes);
             $options[] = $o;
         }
         $context['options'] = $options;
index bd82163..835e89e 100644 (file)
@@ -91,6 +91,7 @@ class MoodleQuickForm_static extends HTML_QuickForm_static implements templatabl
     public function export_for_template(renderer_base $output) {
         $context = $this->export_for_template_base($output);
         $context['html'] = $this->toHtml();
+        $context['staticlabel'] = true;
         return $context;
     }
 }
index 22e8a1e..f091157 100644 (file)
@@ -1,16 +1,47 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_form/editor_textarea
+
+    Displays an editor field in a form.
+
+    Example context (json):
+    {
+        "name": "test",
+        "id": "test0",
+        "rows": 4,
+        "cols": 20,
+        "value": "Sample text",
+        "format": 3
+    }
+}}
 <div>
 <textarea id="{{id}}" name="{{name}}[text]" rows="{{rows}}" cols="{{cols}}" spellcheck="true" {{#changelistener}} onblur="{{onblur}}"
     onchange="{{onchange}}" {{/changelistener}}>{{value}}</textarea>
 </div>
 <div>
-    {{^onlyoneformat}}
+    {{#hasformats}}
         <select name="{{name}}[format]" id="menu{{name}}format">
         {{#formats}}
             <option value="{{value}}" {{#selected}}selected{{/selected}}>{{text}}</option>
         {{/formats}}
         </select>
-    {{/onlyoneformat}}
-    {{#onlyoneformat}}
+    {{/hasformats}}
+    {{^hasformats}}
         <input name="{{name}}[format]" type="hidden" value="{{format}}"/>
-    {{/onlyoneformat}}
+    {{/hasformats}}
 </div>
index 546babc..c114298 100644 (file)
@@ -185,7 +185,7 @@ EOD;
 
     public function export_for_template(renderer_base $output) {
         $context = $this->export_for_template_base($output);
-        $context['filepickerhtml'] = $this->getFilePickerHTML();
+        $context['filepickerhtml'] = !empty($this->_options['usefilepicker']) ? $this->getFilePickerHTML() : '';
         return $context;
     }
 
index 3c3a529..34e7c38 100644 (file)
@@ -50,13 +50,23 @@ require_once(__DIR__ . '/../message/lib.php');
  * Note: processor failure is is not reported as false return value,
  *       earlier versions did not do it consistently either.
  *
+ * @todo MDL-55449 Drop support for stdClass in Moodle 3.6
  * @category message
- * @param stdClass|\core\message\message $eventdata information about the message (component, userfrom, userto, ...)
+ * @param \core\message\message $eventdata information about the message (component, userfrom, userto, ...)
  * @return mixed the integer ID of the new message or false if there was a problem with submitted data
  */
 function message_send($eventdata) {
     global $CFG, $DB;
 
+    // TODO MDL-55449 Drop support for stdClass in Moodle 3.6.
+    if ($eventdata instanceof \stdClass) {
+        if (!isset($eventdata->courseid)) {
+            $eventdata->courseid = null;
+        }
+
+        debugging('eventdata as \stdClass is deprecated. Please use core\message\message instead.', DEBUG_DEVELOPER);
+    }
+
     //new message ID to return
     $messageid = false;
 
@@ -119,6 +129,7 @@ function message_send($eventdata) {
 
     // Create the message object
     $savemessage = new stdClass();
+    $savemessage->courseid          = $eventdata->courseid;
     $savemessage->useridfrom        = $eventdata->userfrom->id;
     $savemessage->useridto          = $eventdata->userto->id;
     $savemessage->subject           = $eventdata->subject;
index 9ac6a65..14a3d01 100644 (file)
@@ -656,8 +656,8 @@ class core_renderer extends renderer_base {
         if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
             $timeleft = $CFG->maintenance_later - time();
             // If timeleft less than 30 sec, set the class on block to error to highlight.
-            $errorclass = ($timeleft < 30) ? 'error' : 'warning';
-            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning');
+            $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
+            $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-a-1 alert');
             $a = new stdClass();
             $a->hour = (int)($timeleft / 3600);
             $a->min = (int)(($timeleft / 60) % 60);
@@ -3195,7 +3195,7 @@ EOD;
             array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
         $searchinput = html_writer::tag('form', $contents, $formattrs);
 
-        return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper', 'id' => $id));
+        return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper nav-link', 'id' => $id));
     }
 
     /**
index df546f5..7254202 100644 (file)
@@ -344,6 +344,33 @@ class moodle_page {
      */
     protected $_popup_notification_allowed = true;
 
+    /**
+     * @var bool Is the settings menu being forced to display on this page (activities / resources only).
+     * This is only used by themes that use the settings menu.
+     */
+    protected $_forcesettingsmenu = false;
+
+    /**
+     * Force the settings menu to be displayed on this page. This will only force the
+     * settings menu on an activity / resource page that is being displayed on a theme that
+     * uses a settings menu.
+     *
+     * @param bool $forced default of true, can be sent false to turn off the force.
+     */
+    public function force_settings_menu($forced = true) {
+        $this->_forcesettingsmenu = $forced;
+    }
+
+    /**
+     * Check to see if the settings menu is forced to display on this activity / resource page.
+     * This only applies to themes that use the settings menu.
+     *
+     * @return bool True if the settings menu is forced to display.
+     */
+    public function is_settings_menu_forced() {
+        return $this->_forcesettingsmenu;
+    }
+
     // Magic getter methods =============================================================
     // Due to the __get magic below, you normally do not call these as $PAGE->magic_get_x
     // methods, but instead use the $PAGE->x syntax.
index 344a5bd..e3dc965 100644 (file)
@@ -424,7 +424,8 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $user2 = $this->getDataGenerator()->create_user();
 
         // Any core message will do here.
-        $message1 = new stdClass();
+        $message1 = new \core\message\message();
+        $message1->courseid          = 1;
         $message1->component         = 'moodle';
         $message1->name              = 'instantmessage';
         $message1->userfrom          = $user1;
@@ -436,7 +437,8 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $message1->smallmessage      = 'small message';
         $message1->notification      = 0;
 
-        $message2 = new stdClass();
+        $message2 = new \core\message\message();
+        $message2->courseid          = 1;
         $message2->component         = 'moodle';
         $message2->name              = 'instantmessage';
         $message2->userfrom          = $user2;
@@ -501,7 +503,8 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
 
         $sink = $this->redirectMessages();
 
-        $message3 = new stdClass();
+        $message3 = new \core\message\message();
+        $message3->courseid          = 1;
         $message3->component         = 'xxxx_yyyyy';
         $message3->name              = 'instantmessage';
         $message3->userfrom          = $user2;
@@ -547,7 +550,8 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $this->assertTrue(phpunit_util::is_redirecting_messages());
         $this->assertEquals(1, $sink->count());
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = get_admin();
index 73390c9..fe90873 100644 (file)
@@ -202,7 +202,8 @@ final class portfolio_admin_form extends moodleform {
         if (portfolio_static_function($this->plugin, 'has_admin_config')) {
             require_once($CFG->libdir . '/portfolio/plugin.php');
             require_once($CFG->dirroot . '/portfolio/' . $this->plugin .  '/lib.php');
-            call_user_func(array('portfolio_plugin_' . $this->plugin, 'admin_config_form'), $mform);
+            $classname = 'portfolio_plugin_' . $this->plugin;
+            $classname::admin_config_form($mform);
         }
 
         // and set the data if we have some.
index 8033546..edadee7 100644 (file)
@@ -1109,7 +1109,8 @@ function portfolio_insane_notify_admins($insane, $instances=false) {
     $smallbody = get_string('insanebodysmall', 'portfolio', $a);
 
     foreach ($admins as $admin) {
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid = SITEID;
         $eventdata->modulename = 'portfolio';
         $eventdata->component = 'portfolio';
         $eventdata->name = 'notices';
index 824c293..17a9701 100644 (file)
@@ -36,7 +36,7 @@
 <div class="popover-region collapsed {{$classes}}{{/classes}}"
     {{$attributes}}{{/attributes}}
     data-region="popover-region">
-    <div class="popover-region-toggle"
+    <div class="popover-region-toggle nav-link"
         data-region="popover-region-toggle"
         aria-role="button"
         aria-controls="popover-region-container-{{uniqid}}"
index f3c04a3..4a8b682 100644 (file)
@@ -814,12 +814,13 @@ abstract class testing_util {
         make_temp_directory('');
         make_cache_directory('');
         make_localcache_directory('');
-        // Reset the cache API so that it recreates it's required directories as well.
-        cache_factory::reset();
-        // Purge all data from the caches. This is required for consistency.
+        // Purge all data from the caches. This is required for consistency between tests.
         // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
-        // and now we will purge any other caches as well.
+        // and now we will purge any other caches as well.  This must be done before the cache_factory::reset() as that
+        // removes all definitions of caches and purge does not have valid caches to operate on.
         cache_helper::purge_all();
+        // Reset the cache API so that it recreates it's required directories as well.
+        cache_factory::reset();
     }
 
     /**
index 7377f89..6cd05d0 100644 (file)
@@ -48,6 +48,7 @@ class core_message_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $USER;
@@ -82,6 +83,7 @@ class core_message_testcase extends advanced_testcase {
 
         $stdclass = $message->get_eventobject_for_processor('test');
 
+        $this->assertSame($message->courseid, $stdclass->courseid);
         $this->assertSame($message->component, $stdclass->component);
         $this->assertSame($message->name, $stdclass->name);
         $this->assertSame($message->userfrom, $stdclass->userfrom);
@@ -143,6 +145,7 @@ class core_message_testcase extends advanced_testcase {
 
         // Extra content for all types of messages.
         $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -171,9 +174,19 @@ class core_message_testcase extends advanced_testcase {
         $this->assertRegExp('/test message body test/', $email->body);
         $sink->clear();
 
+        // Test that event fired includes the courseid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $event = reset($events);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+        $eventsink->clear();
+        $sink->clear();
+
         // Extra content for small message only. Shouldn't show up in emails as we sent fullmessage and fullmessagehtml only in
         // the emails.
         $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -199,6 +212,14 @@ class core_message_testcase extends advanced_testcase {
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $this->assertNotRegExp('/test message body test/', $email->body);
+
+        // Test that event fired includes the courseid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $event = reset($events);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+        $eventsink->close();
         $sink->close();
     }
 }
index ecc3dc5..c79d3cf 100644 (file)
@@ -37,7 +37,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $preferences = get_message_output_default_preferences();
         $this->assertTrue($preferences->$disableprovidersetting == 1);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = get_admin();
@@ -189,7 +190,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $user2 = $this->getDataGenerator()->create_user();
 
         // Test basic message redirection.
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $user1;
@@ -225,7 +227,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertFalse($DB->record_exists('message', array()));
         $DB->delete_records('message_read', array());
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $user1->id;
@@ -263,7 +266,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         // Test phpunit problem detection.
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'xxxxx';
         $message->name = 'instantmessage';
         $message->userfrom = $user1;
@@ -298,7 +302,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         // Invalid users.
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $user1;
@@ -314,7 +319,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertFalse($messageid);
         $this->assertDebuggingCalled('Attempt to send msg to unknown user');
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = -1;
@@ -330,7 +336,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertFalse($messageid);
         $this->assertDebuggingCalled('Attempt to send msg from unknown user');
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $user1;
@@ -349,7 +356,8 @@ class core_messagelib_testcase extends advanced_testcase {
         // Some debugging hints for devs.
 
         unset($user2->emailstop);
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid = 1;
         $message->component = 'moodle';
         $message->name = 'instantmessage';
         $message->userfrom = $user1;
@@ -393,7 +401,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -420,7 +429,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         $CFG->messaging = 0;
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -447,7 +457,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         $CFG->messaging = 1;
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -474,7 +485,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -504,7 +516,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -537,7 +550,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email,popup', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -570,7 +584,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'popup', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -604,7 +619,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -635,7 +651,8 @@ class core_messagelib_testcase extends advanced_testcase {
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -716,7 +733,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -778,7 +796,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = $user1;
@@ -835,7 +854,8 @@ class core_messagelib_testcase extends advanced_testcase {
         );
         $file = $fs->create_file_from_string($filerecord, 'Test content');
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = 1;
         $message->component         = 'moodle';
         $message->name              = 'instantmessage';
         $message->userfrom          = get_admin();
index 1f659ac..f557986 100644 (file)
@@ -119,6 +119,12 @@ information provided here is intended especially for developers.
 * Webservice function mod_assign_get_submissions returns a new field 'gradingstatus' from each submission.
 * The return signature for the antivirus::scan_file() function has changed.
   The calling function will now handle removal of infected files from Moodle based on the new integer return value.
+* The first parameter $eventdata of both message_send() and \core\message\manager::send_message() should
+  be \core\message\message. Use of stdClass is deprecated.
+* The message_sent event now expects other[courseid] to be always set, exception otherwise. For BC with contrib code,
+  message_sent::create_from_ids() will show a debugging notice if the \core\message\message being sent is missing
+  the courseid property, defaulting to SITEID automatically. In Moodle 3.6 (MDL-55449) courseid will be fully mandatory
+  for all messages sent.
 
 === 3.1 ===
 
index 7d9caa3..b584c62 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js differ
index 6e2c93e..81f74b4 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js differ
index 7d9caa3..b584c62 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js differ
index 430f159..434bb70 100644 (file)
Binary files a/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-debug.js and b/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-debug.js differ
index 9435047..1a107a0 100644 (file)
Binary files a/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js and b/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js differ
index 430f159..434bb70 100644 (file)
Binary files a/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer.js and b/lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer.js differ
index c88507c..f4545f4 100644 (file)
@@ -69,7 +69,7 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
          */
         store_initial_value: function(e) {
             var thisevent;
-            if (e.target.hasClass('ignoredirty')) {
+            if (e.target.hasClass('ignoredirty') || e.target.ancestor('.ignoredirty')) {
                 // Don't warn on elements with the ignoredirty class
                 return;
             }
@@ -118,7 +118,7 @@ M.core_formchangechecker.stateinformation = [];
  * Set the form changed state to true
  */
 M.core_formchangechecker.set_form_changed = function(e) {
-    if (e && e.target && e.target.hasClass('ignoredirty')) {
+    if (e && e.target && (e.target.hasClass('ignoredirty') || e.target.ancestor('.ignoredirty'))) {
         // Don't warn on elements with the ignoredirty class
         return;
     }
index 9d4899d..a35d518 100644 (file)
@@ -47,11 +47,13 @@ Y.extend(MAINTENANCEMODETIMER, Y.Base, {
         }
         // Set error class to highlight the importance.
         if (this.timeleftinsec < 30) {
-            this.maintenancenode.addClass('error')
-                    .removeClass('warning');
+            this.maintenancenode.addClass('alert-error')
+                    .addClass('alert-danger')
+                    .removeClass('alert-warning');
         } else {
-            this.maintenancenode.addClass('warning')
-                    .removeClass('error');
+            this.maintenancenode.addClass('alert-warning')
+                    .removeClass('alert-error')
+                    .removeClass('alert-danger');
         }
     }
 });
index 83818bb..cd44e0b 100644 (file)
@@ -25,6 +25,7 @@
  */
 
 require('../config.php');
+require_once($CFG->libdir . '/authlib.php');
 
 $data = optional_param('data', '', PARAM_RAW);  // Formatted as:  secret/username
 
index ef07b75..7ff3d01 100644 (file)
Binary files a/message/amd/build/message_area_contacts.min.js and b/message/amd/build/message_area_contacts.min.js differ
index c1aa29e..237cb65 100644 (file)
@@ -535,8 +535,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
         Contacts.prototype._setSelectedUser = function(selector) {
             // Remove the 'selected' class from any other contact.
             this.messageArea.find(SELECTORS.CONTACT).removeClass('selected');
+            this.messageArea.find(SELECTORS.CONTACT).removeAttr('tabindex');
             // Set the tab for the user to selected.
             this.messageArea.find(SELECTORS.CONTACT + selector).addClass('selected');
+            this.messageArea.find(SELECTORS.CONTACT + selector).attr('tabIndex', 0);
         };
 
         /**
index 23f0feb..4398fba 100644 (file)
@@ -196,14 +196,19 @@ class api {
         }
 
         // Now, let's get the courses.
+        // Make sure to limit searches to enrolled courses.
+        $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev'));
         $courses = array();
-        if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum))) {
+        if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum),
+                array('moodle/course:viewparticipants'))) {
             foreach ($arrcourses as $course) {
-                $data = new \stdClass();
-                $data->id = $course->id;
-                $data->shortname = $course->shortname;
-                $data->fullname = $course->fullname;
-                $courses[] = $data;
+                if (isset($enrolledcourses[$course->id])) {
+                    $data = new \stdClass();
+                    $data->id = $course->id;
+                    $data->shortname = $course->shortname;
+                    $data->fullname = $course->fullname;
+                    $courses[] = $data;
+                }
             }
         }
 
index a22831c..6e81c4f 100644 (file)
@@ -214,13 +214,26 @@ class core_message_external extends external_api {
      * @since Moodle 2.5
      */
     public static function create_contacts($userids, $userid = 0) {
-        global $CFG;
+        global $CFG, $USER;
 
         // Check if messaging is enabled.
         if (empty($CFG->messaging)) {
             throw new moodle_exception('disabled', 'message');
         }
 
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
         $params = array('userids' => $userids, 'userid' => $userid);
         $params = self::validate_parameters(self::create_contacts_parameters(), $params);
 
@@ -276,13 +289,26 @@ class core_message_external extends external_api {
      * @since Moodle 2.5
      */
     public static function delete_contacts($userids, $userid = 0) {
-        global $CFG;
+        global $CFG, $USER;
 
         // Check if messaging is enabled.
         if (empty($CFG->messaging)) {
             throw new moodle_exception('disabled', 'message');
         }
 
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
         $params = array('userids' => $userids, 'userid' => $userid);
         $params = self::validate_parameters(self::delete_contacts_parameters(), $params);
 
@@ -331,13 +357,26 @@ class core_message_external extends external_api {
      * @since Moodle 2.5
      */
     public static function block_contacts($userids, $userid = 0) {
-        global $CFG;
+        global $CFG, $USER;
 
         // Check if messaging is enabled.
         if (empty($CFG->messaging)) {
             throw new moodle_exception('disabled', 'message');
         }
 
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
         $params = array('userids' => $userids, 'userid' => $userid);
         $params = self::validate_parameters(self::block_contacts_parameters(), $params);
 
@@ -393,13 +432,26 @@ class core_message_external extends external_api {
      * @since Moodle 2.5
      */
     public static function unblock_contacts($userids, $userid = 0) {
-        global $CFG;
+        global $CFG, $USER;
 
         // Check if messaging is enabled.
         if (empty($CFG->messaging)) {
             throw new moodle_exception('disabled', 'message');
         }
 
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
         $params = array('userids' => $userids, 'userid' => $userid);
         $params = self::validate_parameters(self::unblock_contacts_parameters(), $params);
 
@@ -1590,7 +1642,12 @@ class core_message_external extends external_api {
      * @return external_description
      */
     public static function get_unread_conversations_count($useridto) {
-        global $USER;
+        global $USER, $CFG;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
 
         $params = self::validate_parameters(
             self::get_unread_conversations_count_parameters(),
@@ -1656,6 +1713,11 @@ class core_message_external extends external_api {
     public static function get_blocked_users($userid) {
         global $CFG, $USER, $PAGE;
 
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
         // Warnings array, it can be empty at the end but is mandatory.
         $warnings = array();
 
@@ -1679,8 +1741,9 @@ class core_message_external extends external_api {
         core_user::require_active_user($user);
 
         // Check if we have permissions for retrieve the information.
-        if ($userid != $USER->id and !has_capability('moodle/site:readallmessages', $context)) {
-            throw new moodle_exception('accessdenied', 'admin');
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
         // Now, we can get safely all the blocked users.
@@ -1843,7 +1906,12 @@ class core_message_external extends external_api {
      * @return external_description
      */
     public static function mark_all_messages_as_read($useridto, $useridfrom) {
-        global $USER;
+        global $USER, $CFG;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
 
         $params = self::validate_parameters(
             self::mark_all_messages_as_read_parameters(),
@@ -2092,7 +2160,12 @@ class core_message_external extends external_api {
      * @since 3.2
      */
     public static function message_processor_config_form($userid, $name, $formvalues) {
-        global $USER;
+        global $USER, $CFG;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
 
         $params = self::validate_parameters(
             self::message_processor_config_form_parameters(),
@@ -2162,7 +2235,12 @@ class core_message_external extends external_api {
      * @since 3.2
      */
     public static function get_message_processor($userid = 0, $name) {
-        global $USER, $PAGE;
+        global $USER, $PAGE, $CFG;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
 
         $params = self::validate_parameters(
             self::get_message_processor_parameters(),
@@ -2226,7 +2304,7 @@ class core_message_external extends external_api {
      * @since 3.2
      */
     public static function get_user_notification_preferences($userid = 0) {
-        global $USER, $PAGE;
+        global $USER, $PAGE, $CFG;
 
         $params = self::validate_parameters(
             self::get_user_notification_preferences_parameters(),
index cda6995..641a14e 100644 (file)
@@ -830,7 +830,8 @@ function message_format_contexturl($message) {
 function message_post_message($userfrom, $userto, $message, $format) {
     global $SITE, $CFG, $USER;
 
-    $eventdata = new stdClass();
+    $eventdata = new \core\message\message();
+    $eventdata->courseid         = 1;
     $eventdata->component        = 'moodle';
     $eventdata->name             = 'instantmessage';
     $eventdata->userfrom         = $userfrom;
index 1cf3fc6..aeac026 100644 (file)
         <td class="align-bottom">
             <div class="container-fluid">
                 <div class="row-fluid">
-                    <div class="span6">
+                    <div class="span6 col-xs-6">
                         {{#str}} loggedin, message {{/str}}
                         {{#onlinehelphtml}}{{{.}}}{{/onlinehelphtml}}
                     </div>
-                    <div class="span6">
+                    <div class="span6 col-xs-6">
                         {{#str}} loggedoff, message {{/str}}
                         {{#offlinehelphtml}}{{{.}}}{{/offlinehelphtml}}
                     </div>
index 0e35d4a..43c38ef 100644 (file)
@@ -69,7 +69,7 @@
             <form>
                 <div class="container-fluid">
                     <div class="row-fluid">
-                        <div class="span6">
+                        <div class="span6 col-xs-6">
                             {{#loggedin}}
                                 {{< core/hover_tooltip }}
                                     {{$anchor}}
@@ -94,7 +94,7 @@
                                 {{/ core/hover_tooltip }}
                             {{/loggedin}}
                         </div>
-                        <div class="span6">
+                        <div class="span6 col-xs-6">
                             {{#loggedoff}}
                                 {{< core/hover_tooltip }}
                                     {{$anchor}}
index c220e71..59f5295 100644 (file)
         <td class="align-bottom">
             <div class="container-fluid">
                 <div class="row-fluid">
-                    <div class="span6">
+                    <div class="span6 col-xs-6">
                         {{#str}} loggedin, message {{/str}}
                         {{#onlinehelphtml}}{{{.}}}{{/onlinehelphtml}}
                     </div>
-                    <div class="span6">
+                    <div class="span6 col-xs-6">
                         {{#str}} loggedoff, message {{/str}}
                         {{#offlinehelphtml}}{{{.}}}{{/offlinehelphtml}}
                     </div>
index acc39f8..0f10767 100644 (file)
@@ -67,7 +67,7 @@
                 <form>
                     <div class="container-fluid">
                         <div class="row-fluid">
-                            <div class="span6">
+                            <div class="span6 col-xs-6">
                                 {{#loggedin}}
                                     {{< core/hover_tooltip }}
                                         {{$anchor}}
@@ -92,7 +92,7 @@
                                     {{/ core/hover_tooltip }}
                                 {{/loggedin}}
                             </div>
-                            <div class="span6">
+                            <div class="span6 col-xs-6">
                                 {{#loggedoff}}
                                     {{< core/hover_tooltip }}
                                         {{$anchor}}
index d983273..48c7826 100644 (file)
@@ -174,6 +174,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Tests searching users.
      */
     public function test_search_users() {
+        global $DB;
+
         // Create some users.
         $user1 = new stdClass();
         $user1->firstname = 'User';
@@ -224,11 +226,32 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $course3->shortname = 'Three search';
         $course3 = $this->getDataGenerator()->create_course($course3);
 
+        $course4 = new stdClass();
+        $course4->fullname = 'Course Four';
+        $course4->shortname = 'CF100';
+        $course4 = $this->getDataGenerator()->create_course($course4);
+
+        $course5 = new stdClass();
+        $course5->fullname = 'Course';
+        $course5->shortname = 'Five search';
+        $course5 = $this->getDataGenerator()->create_course($course5);
+
+        $role = $DB->get_record('role', ['shortname' => 'student']);
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course3->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course5->id, $role->id);
+
         // Add some users as contacts.
         message_add_contact($user2->id, 0, $user1->id);
         message_add_contact($user3->id, 0, $user1->id);
         message_add_contact($user4->id, 0, $user1->id);
 
+        // Remove the viewparticipants capability from one of the courses.
+        $course5context = context_course::instance($course5->id);
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $role->id, $course5context->id);
+        $course5context->mark_dirty();
+
         // Perform a search.
         list($contacts, $courses, $noncontacts) = \core_message\api::search_users($user1->id, 'search');
 
index 1e3bc9e..4006738 100644 (file)
@@ -201,7 +201,8 @@ class core_message_events_testcase extends advanced_testcase {
             'context'  => context_system::instance(),
             'relateduserid' => 2,
             'other' => array(
-                'messageid' => 3
+                'messageid' => 3,
+                'courseid' => 4
             )
         ));
 
@@ -218,8 +219,66 @@ class core_message_events_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expected, $event);
         $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEquals(3, $event->other['messageid']);
+        $this->assertEquals(4, $event->other['courseid']);
     }
 
+    public function test_mesage_sent_without_other_courseid() {
+
+        // Creating a message_sent event without other[courseid] leads to exception.
+        $this->expectException('coding_exception');
+        $this->expectExceptionMessage('The \'courseid\' value must be set in other');
+
+        $event = \core\event\message_sent::create(array(
+            'userid' => 1,
+            'context'  => context_system::instance(),
+            'relateduserid' => 2,
+            'other' => array(
+                'messageid' => 3,
+            )
+        ));
+    }
+
+    public function test_mesage_sent_via_create_from_ids() {
+        // Containing courseid.
+        $event = \core\event\message_sent::create_from_ids(1, 2, 3, 4);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\message_sent', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+        $expected = array(SITEID, 'message', 'write', 'index.php?user=1&id=2&history=1#m3', 1);
+        $this->assertEventLegacyLogData($expected, $event);
+        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
+        $this->assertEquals($url, $event->get_url());
+        $this->assertEquals(3, $event->other['messageid']);
+        $this->assertEquals(4, $event->other['courseid']);
+    }
+
+    public function test_mesage_sent_via_create_from_ids_without_other_courseid() {
+
+        // Creating a message_sent event without courseid leads to debugging + SITEID.
+        // TODO: MDL-55449 Ensure this leads to exception instead of debugging in Moodle 3.6.
+        $event = \core\event\message_sent::create_from_ids(1, 2, 3);
+
+        // Trigger and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertDebuggingCalled();
+        $this->assertEquals(SITEID, $event->other['courseid']);
+    }
+
+
+
+
     /**
      * Test the message viewed event.
      */
index 8c371e0..e4d740c 100644 (file)
@@ -161,6 +161,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $return = array_pop($return);
         $this->assertEquals($return['warningcode'], 'contactnotcreated');
         $this->assertEquals($return['itemid'], 99999);
+
+        // Try to add a contact to another user, should throw an exception.
+        // All assertions must be added before this point.
+        $this->expectException('required_capability_exception');
+        core_message_external::create_contacts(array($user2->id), $user3->id);
     }
 
     /**
@@ -198,6 +203,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Removing mixed valid and invalid data.
         $return = core_message_external::delete_contacts(array($user6->id, 99999));
         $this->assertNull($return);
+
+        // Try to delete a contact of another user contact list, should throw an exception.
+        // All assertions must be added before this point.
+        $this->expectException('required_capability_exception');
+        core_message_external::delete_contacts(array($user2->id), $user3->id);
     }
 
     /**
@@ -244,6 +254,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $return = array_pop($return);
         $this->assertEquals($return['warningcode'], 'contactnotblocked');
         $this->assertEquals($return['itemid'], 99999);
+
+        // Try to block a contact of another user contact list, should throw an exception.
+        // All assertions must be added before this point.
+        $this->expectException('required_capability_exception');
+        core_message_external::block_contacts(array($user2->id), $user3->id);
     }
 
     /**
@@ -282,6 +297,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $return = core_message_external::unblock_contacts(array($user6->id, 99999));
         $this->assertNull($return);
 
+        // Try to unblock a contact of another user contact list, should throw an exception.
+        // All assertions must be added before this point.
+        $this->expectException('required_capability_exception');
+        core_message_external::unblock_contacts(array($user2->id), $user3->id);
     }
 
     /**
@@ -492,7 +511,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // We are creating fake notifications but based on real ones.
 
         // This one omits notification = 1.
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = $course->id;
         $eventdata->modulename        = 'moodle';
         $eventdata->component         = 'enrol_paypal';
         $eventdata->name              = 'paypal_enrolment';
@@ -505,7 +525,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $eventdata->smallmessage      = '';
         message_send($eventdata);
 
-        $message = new stdClass();
+        $message = new \core\message\message();
+        $message->courseid          = $course->id;
         $message->notification      = 1;
         $message->component         = 'enrol_manual';
         $message->name              = 'expiry_notification';
@@ -522,7 +543,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         $userfrom = core_user::get_noreply_user();
         $userfrom->maildisplay = true;
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = $course->id;
         $eventdata->component         = 'moodle';
         $eventdata->name              = 'badgecreatornotice';
         $eventdata->userfrom          = $userfrom;
@@ -535,7 +557,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $eventdata->smallmessage      = $eventdata->subject;
         message_send($eventdata);
 
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid         = $course->id;
         $eventdata->name             = 'submission';
         $eventdata->component        = 'mod_feedback';
         $eventdata->userfrom         = $user1;
@@ -1172,6 +1195,15 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $course3->shortname = 'Three search';
         $course3 = $this->getDataGenerator()->create_course($course3);
 
+        $course4 = new stdClass();
+        $course4->fullname = 'Course Four';
+        $course4->shortname = 'CF100';
+        $course4 = $this->getDataGenerator()->create_course($course4);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'student');
+        $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 'student');
+
         // Add some users as contacts.
         message_add_contact($user2->id, 0, $user1->id);
         message_add_contact($user3->id, 0, $user1->id);
@@ -1283,9 +1315,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($user2->id, $contacts[1]['userid']);
 
         // Check that we retrieved the correct courses.
-        $this->assertCount(2, $courses);
-        $this->assertEquals($course3->id, $courses[0]['id']);
-        $this->assertEquals($course1->id, $courses[1]['id']);
+        $this->assertCount(0, $courses);
 
         // Check that we retrieved the correct non-contacts.
         $this->assertCount(1, $noncontacts);
index 7b41803..191850e 100644 (file)
@@ -72,7 +72,8 @@ class message_received_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
@@ -129,7 +130,8 @@ class message_received_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
@@ -175,7 +177,8 @@ class message_received_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
index 9938ae1..7e9c887 100644 (file)
@@ -72,7 +72,8 @@ class message_sent_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
@@ -129,7 +130,8 @@ class message_sent_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
@@ -175,7 +177,8 @@ class message_sent_search_testcase extends advanced_testcase {
         $this->preventResetByRollback();
         $sink = $this->redirectMessages();
 
-        $message = new StdClass();
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
         $message->userfrom = $user1;
         $message->userto = $user2;
         $message->subject = "Test Subject";
index 69fa58e..9dda169 100644 (file)
@@ -96,6 +96,12 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
             $this->includesubmission = false;
         }
 
+        // Reset revealidentities if blindmarking with no user data (MDL-43796).
+        $userinfo = $this->get_setting_value('userinfo');
+        if (!$userinfo && $data->blindmarking) {
+            $data->revealidentities = 0;
+        }
+
         if (!empty($data->teamsubmissiongroupingid)) {
             $data->teamsubmissiongroupingid = $this->get_mappingid('grouping',
                                                                    $data->teamsubmissiongroupingid);
index b815f75..b190040 100644 (file)
@@ -1506,7 +1506,7 @@ class mod_assign_external extends external_api {
      * @param int $userid The id of the user the submission belongs to.
      * @param string $jsonformdata The data from the form, encoded as a json array.
      * @return array of warnings to indicate any errors.
-     * @since Moodle 2.6
+     * @since Moodle 3.1
      */
     public static function submit_grading_form($assignmentid, $userid, $jsonformdata) {
         global $CFG, $USER;
@@ -1537,6 +1537,12 @@ class mod_assign_external extends external_api {
             'gradingpanel' => true
         );
 
+        if (WS_SERVER) {
+            // Assume form submission if coming from WS.
+            $USER->ignoresesskey = true;
+            $data['_qf__mod_assign_grade_form_'.$params['userid']] = 1;
+        }
+
         $customdata = (object) $data;
         $formparams = array($assignment, $customdata, $options);
 
@@ -2370,6 +2376,9 @@ class mod_assign_external extends external_api {
                                                                             $lastattempt->submissiongroupmemberswhoneedtosubmit);
             }
 
+            // Can edit its own submission?
+            $lastattempt->caneditowner = $assign->submissions_open($user->id) && $assign->is_any_submission_plugin_enabled();
+
             $result['lastattempt'] = $lastattempt;
         }
 
@@ -2464,6 +2473,7 @@ class mod_assign_external extends external_api {
                         'locked' => new external_value(PARAM_BOOL, 'Whether new submissions are locked.'),
                         'graded' => new external_value(PARAM_BOOL, 'Whether the submission is graded.'),
                         'canedit' => new external_value(PARAM_BOOL, 'Whether the user can edit the current submission.'),
+                        'caneditowner' => new external_value(PARAM_BOOL, 'Whether the owner of the submission can edit it.'),
                         'cansubmit' => new external_value(PARAM_BOOL, 'Whether the user can submit.'),
                         'extensionduedate' => new external_value(PARAM_INT, 'Extension due date.'),
                         'blindmarking' => new external_value(PARAM_BOOL, 'Whether blind marking is enabled.'),
index 32c0e78..9f71623 100644 (file)
@@ -206,7 +206,14 @@ class assign_grading_table extends table_sql implements renderable {
             } else if ($filter == ASSIGN_FILTER_REQUIRE_GRADING) {
                 $where .= ' AND (s.timemodified IS NOT NULL AND
                                  s.status = :submitted AND
-                                 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL))';
+                                 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL';
+
+                if ($this->assignment->get_grade_item()->gradetype == GRADE_TYPE_SCALE) {
+                    // Scale grades are set to -1 when not graded.
+                    $where .= ' OR g.grade = -1';
+                }
+
+                $where .= '))';
                 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
 
             } else if (strpos($filter, ASSIGN_FILTER_SINGLE_USER) === 0) {
index 700b4a3..09cea5f 100644 (file)
@@ -137,8 +137,8 @@ $string['deleteallsubmissions'] = 'Delete all submissions';
 $string['description'] = 'Description';
 $string['downloadall'] = 'Download all submissions';
 $string['download all submissions'] = 'Download all submissions in a zip file.';
-$string['downloadasfolders'] = 'Download as separate folders';
-$string['downloadasfolders_help'] = 'When enabled downloaded files will be placed in separate folders and files will not be renamed.';
+$string['downloadasfolders'] = 'Download submissions in folders';
+$string['downloadasfolders_help'] = 'If the assignment submission is more than a single file, then submissions may be downloaded in folders. Each submission is put in a separate folder, with the folder structure kept for any subfolders, and files are not renamed.';
 $string['downloadselectedsubmissions'] = 'Download selected submissions';
 $string['duedate'] = 'Due date';
 $string['duedatecolon'] = 'Due date: {$a}';
index d8e49ab..5f07def 100644 (file)
@@ -5475,7 +5475,8 @@ class assign {
                                                                $assignmentname);
         }
 
-        $eventdata = new stdClass();
+        $eventdata = new \core\message\message();
+        $eventdata->courseid         = $course->id;
         $eventdata->modulename       = 'assign';
         $eventdata->userfrom         = $userfrom;
         $eventdata->userto           = $userto;
index d4e7c48..4d753ba 100644 (file)
@@ -43,7 +43,7 @@
     font-weight: 900;
 }
 
-.path-mod-assign.jsenabled .gradingoptionsform .fsubmit {
+.path-mod-assign.jsenabled .gradingoptionsform [type=submit] {
     display: none;
 }
 
index 6b189d9..f799e9f 100644 (file)
@@ -1246,6 +1246,40 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
     }
 
+    public function test_cron_message_includes_courseid() {
+        // First run cron so there are no messages waiting to be sent (from other tests).
+        cron_setup_user();
+        assign::cron();
+
+        // Now create an assignment.
+        $this->setUser($this->editingteachers[0]);
+        $assign = $this->create_instance(array('sendstudentnotifications' => 1));
+
+        // Simulate adding a grade.
+        $this->setUser($this->teachers[0]);
+        $data = new stdClass();
+        $data->grade = '50.0';
+        $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
+
+        $this->preventResetByRollback();
+        $sink = $this->redirectEvents();
+        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
+
+        assign::cron();
+
+        $events = $sink->get_events();
+        // Two messages are sent, one to student and one to teacher. This generates
+        // four events:
+        // core\event\message_sent
+        // core\event\message_viewed
+        // core\event\message_sent
+        // core\event\message_viewed.
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\message_sent', $event);
+        $this->assertEquals($assign->get_course()->id, $event->other['courseid']);
+        $sink->close();
+    }
+
     public function test_is_graded() {
         $this->setUser($this->editingteachers[0]);
         $assign = $this->create_instance();
index 9649451..fb322db 100644 (file)
         $PAGE->navbar->add(get_string('search'));
     }
 
+    $PAGE->force_settings_menu();
     $PAGE->set_title($title);
     $PAGE->set_heading($course->fullname);
 
index 48a0b6a..aef9e21 100644 (file)
@@ -111,7 +111,6 @@ class mod_feedback_complete_form extends moodleform {
                     array('class' => 'form-submit'));
             $buttonarray[] = &$mform->createElement('submit', 'savevalues', get_string('save_entries', 'feedback'),
                     array('class' => 'form-submit'));
-            $buttonarray[] = &$mform->createElement('static', 'buttonsseparator', '', '<br>');
             $buttonarray[] = &$mform->createElement('cancel');
             $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
             $mform->closeHeaderBefore('buttonar');
index 4d81cb8..586d266 100644 (file)
@@ -120,8 +120,7 @@ class feedback_edit_create_template_form extends moodleform {
 
         if (has_capability('mod/feedback:createpublictemplate', context_system::instance())) {
             $elementgroup[] = $mform->createElement('checkbox',
-                                                     'ispublic',
-                                                     get_string('public', 'feedback'),
+                                                     'ispublic', '',
                                                      get_string('public', 'feedback'));
         }
 
index 45edfba..cee0cc6 100644 (file)
@@ -3027,7 +3027,8 @@ function feedback_send_email($cm, $feedback, $course, $user) {
             }
 
             if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid         = $course->id;
                 $eventdata->name             = 'submission';
                 $eventdata->component        = 'mod_feedback';
                 $eventdata->userfrom         = $user;
@@ -3037,9 +3038,13 @@ function feedback_send_email($cm, $feedback, $course, $user) {
                 $eventdata->fullmessageformat = FORMAT_PLAIN;
                 $eventdata->fullmessagehtml  = $posthtml;
                 $eventdata->smallmessage     = '';
+                $eventdata->courseid         = $course->id;
+                $eventdata->contexturl       = $info->url;
+                $eventdata->contexturlname   = $info->feedback;
                 message_send($eventdata);
             } else {
-                $eventdata = new stdClass();
+                $eventdata = new \core\message\message();
+                $eventdata->courseid         = $course->id;
                 $eventdata->name             = 'submission';
                 $eventdata->component        = 'mod_feedback';
                 $eventdata->userfrom         = $teacher;
@@ -3049,6 +3054,9 @@ function feedback_send_email($cm, $feedback, $course, $user) {
                 $eventdata->fullmessageformat = FORMAT_PLAIN;
                 $eventdata->fullmessagehtml  = $posthtml;
                 $eventdata->smallmessage     = '';
+                $eventdata->courseid         = $course->id;
+                $eventdata->contexturl       = $info->url;
+                $eventdata->contexturlname   = $info->feedback;
                 message_send($eventdata);
             }
         }
@@ -3097,7 +3105,8 @@ function feedback_send_email_anonym($cm, $feedback, $course) {
                 $posthtml = '';
             }
 
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid         = $course->id;
             $eventdata->name             = 'submission';
             $eventdata->component        = 'mod_feedback';
             $eventdata->userfrom         = $teacher;
@@ -3107,6 +3116,9 @@ function feedback_send_email_anonym($cm, $feedback, $course) {
             $eventdata->fullmessageformat = FORMAT_PLAIN;
             $eventdata->fullmessagehtml  = $posthtml;
             $eventdata->smallmessage     = '';
+            $eventdata->courseid         = $course->id;
+            $eventdata->contexturl       = $info->url;
+            $eventdata->contexturlname   = $info->feedback;
             message_send($eventdata);
         }
     }
index 7984486..50f89fd 100644 (file)
@@ -97,7 +97,8 @@ if ($action == 'sendmessage' AND has_capability('moodle/course:bulkmessaging', $
     if (is_array($messageuser)) {
         foreach ($messageuser as $userid) {
             $senduser = $DB->get_record('user', array('id'=>$userid));
-            $eventdata = new stdClass();
+            $eventdata = new \core\message\message();
+            $eventdata->courseid         = $course->id;
             $eventdata->name             = 'message';
             $eventdata->component        = 'mod_feedback';
             $eventdata->userfrom         = $USER;
@@ -107,6 +108,9 @@ if ($action == 'sendmessage' AND has_capability('moodle/course:bulkmessaging', $
             $eventdata->fullmessageformat = FORMAT_PLAIN;
             $eventdata->fullmessagehtml  = $htmlmessage;
             $eventdata->smallmessage     = '';
+            $eventdata->courseid         = $course->id;
+            $eventdata->contexturl       = $link3;
+            $eventdata->contexturlname   = $feedback->name;
             $good = $good && message_send($eventdata);
         }
         if (!empty($good)) {