Merge branch 'MDL-55382-master' of git://github.com/damyon/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 15 Jan 2018 09:05:05 +0000 (10:05 +0100)
committerDavid Monllao <davidm@moodle.com>
Mon, 15 Jan 2018 09:05:05 +0000 (10:05 +0100)
250 files changed:
admin/category.php
admin/roles/allow.php
admin/roles/classes/allow_assign_page.php
admin/roles/classes/allow_override_page.php
admin/roles/classes/allow_switch_page.php
admin/roles/classes/allow_view_page.php [new file with mode: 0644]
admin/roles/classes/define_role_table_advanced.php
admin/roles/classes/preset.php
admin/roles/classes/preset_form.php
admin/roles/define.php
admin/roles/managetabs.php
admin/roles/role_schema.xml
admin/roles/tests/preset_test.php
admin/search.php
admin/searchareas.php
admin/searchreindex.php [new file with mode: 0644]
admin/settings.php
admin/settings/grades.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/recyclebin/tests/behat/basic_functionality.feature
admin/tool/task/cli/schedule_task.php
admin/tool/task/lang/en/tool_task.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/src/tour.js
admin/tool/usertours/tests/behat/tour_filter.feature
admin/tool/usertours/thirdpartylibs.xml
admin/tool/xmldb/actions/edit_field/edit_field.js
admin/tool/xmldb/actions/edit_field_save/edit_field_save.class.php
admin/tool/xmldb/lang/en/tool_xmldb.php
analytics/tests/model_test.php
auth/db/auth.php
auth/db/lang/en/auth_db.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/auth.php
auth/upgrade.txt
backup/cc/entity11.resource.class.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/tests/moodle2_test.php
backup/util/plan/restore_step.class.php
backup/util/ui/backup_moodleform.class.php
badges/backpack.js
badges/criteria/award_criteria_manual.php
badges/tests/behat/role_visibility.feature [new file with mode: 0644]
blocks/calendar_month/block_calendar_month.php
blocks/course_list/block_course_list.php
blocks/myoverview/classes/output/main.php
blocks/online_users/block_online_users.php
blocks/online_users/lang/en/block_online_users.php
blocks/online_users/tests/behat/block_online_users_course.feature
blocks/online_users/tests/behat/block_online_users_dashboard.feature
blocks/online_users/tests/behat/block_online_users_frontpage.feature
blocks/rss_client/templates/item.mustache
blocks/tags/block_tags.php
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_mini.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_mini.js
calendar/classes/external/month_exporter.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/calendar_mini.mustache
calendar/templates/month_detailed.mustache
calendar/tests/lib_test.php
competency/classes/external.php
competency/tests/external_test.php
completion/classes/edit_base_form.php
course/dndupload.js
course/dnduploadlib.php
course/edit_form.php
course/moodleform_mod.php
course/renderer.php
course/reset_form.php
enrol/category/lang/en/enrol_category.php
enrol/locallib.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/src/quickenrolment.js
enrol/manual/tests/externallib_test.php
enrol/tests/behat/role_visibility.feature [new file with mode: 0644]
enrol/tests/enrollib_test.php
enrol/upgrade.txt
enrol/users_forms.php
grade/export/grade_export_form.php
group/lib.php
group/tests/behat/role_visibility.feature [new file with mode: 0644]
install/lang/he/moodle.php
install/lang/hi/admin.php
iplookup/tests/geoip_test.php
iplookup/tests/geoplugin_test.php
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/badges.php
lang/en/grades.php
lang/en/hub.php
lang/en/langconfig.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lang/en/search.php
lib/accesslib.php
lib/authlib.php
lib/classes/event/role_allow_view_updated.php [new file with mode: 0644]
lib/classes/external/exporter.php
lib/classes/files/curl_security_helper.php
lib/db/install.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/mssql_sql_generator.php
lib/ddl/mysql_sql_generator.php
lib/ddl/oracle_sql_generator.php
lib/ddl/tests/ddl_test.php
lib/ddl/tests/fixtures/xmldb_table.xml
lib/deprecatedlib.php
lib/dml/mssql_native_moodle_database.php
lib/enrollib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/stored_file.php
lib/form/amd/build/defaultcustom.min.js
lib/form/amd/src/defaultcustom.js
lib/form/defaultcustom.php
lib/form/filemanager.js
lib/form/form.js
lib/formslib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/statslib.php
lib/testing/generator/data_generator.php
lib/tests/accesslib_test.php
lib/tests/curl_security_helper_test.php
lib/tests/externallib_test.php
lib/tests/filelib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/xmldb/xmldb_field.php
mod/assign/classes/output/grading_app.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/renderable.php
mod/assign/renderer.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/templates/grading_navigation.mustache
mod/assign/tests/behat/assign_group_override.feature
mod/assign/tests/behat/assign_user_override.feature
mod/assign/tests/locallib_test.php
mod/chat/backup/moodle2/backup_chat_stepslib.php
mod/chat/backup/moodle2/restore_chat_stepslib.php
mod/chat/chat_ajax.php
mod/chat/chatd.php
mod/chat/classes/external.php
mod/chat/db/install.xml
mod/chat/db/upgrade.php
mod/chat/lib.php
mod/chat/report.php
mod/chat/tests/format_message_test.php
mod/chat/version.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/renderer.php
mod/choice/tests/behat/publish_info.feature [new file with mode: 0644]
mod/choice/tests/lib_test.php
mod/choice/view.php
mod/feedback/lang/en/feedback.php
mod/feedback/show_entries.php
mod/feedback/styles.css
mod/feedback/tests/behat/anonymous.feature
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/multichoice.feature
mod/feedback/tests/behat/multipleattempt.feature
mod/feedback/tests/behat/non_anonymous.feature
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/feedback/version.php
mod/feedback/view.php
mod/forum/classes/search/post.php
mod/forum/lib.php
mod/forum/rsslib.php
mod/forum/tests/lib_test.php
mod/forum/tests/search_test.php
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/multichoice.php
mod/lti/locallib.php
mod/quiz/classes/external.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/edit.php
mod/quiz/mod_form.php
mod/quiz/styles.css
mod/quiz/tests/behat/editing_add.feature
mod/quiz/tests/behat/settings_form_fields_disableif.feature
mod/quiz/tests/external_test.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php
question/type/calculated/questiontype.php
question/type/numerical/db/install.xml
question/type/numerical/db/upgrade.php
question/type/numerical/version.php
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js
question/yui/src/qbankmanager/js/qbankmanager.js
report/outline/classes/filter_form.php [new file with mode: 0644]
report/outline/db/access.php
report/outline/index.php
report/outline/lang/en/report_outline.php
report/outline/lib.php
report/outline/tests/behat/behat_report_outline.php [new file with mode: 0644]
report/outline/tests/behat/filter.feature [new file with mode: 0644]
report/outline/tests/lib_test.php
report/outline/version.php
report/progress/index.php
report/stats/locallib.php
repository/filepicker.js
repository/lib.php
repository/manage_instances.php
repository/repository_ajax.php
search/classes/base.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/document.php
search/classes/manager.php
search/classes/output/renderer.php
search/templates/index_requests.mustache [new file with mode: 0644]
search/tests/base_activity_test.php
search/tests/base_block_test.php
search/tests/base_test.php
search/tests/manager_test.php
search/upgrade.txt
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/search.scss
theme/boost/templates/mod_assign/grading_navigation.mustache
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/search.less
theme/bootstrapbase/style/moodle.css
user/classes/output/user_roles_editable.php
user/classes/participants_table.php
user/profile/lib.php
user/renderer.php
version.php
webservice/xmlrpc/lib.php
webservice/xmlrpc/tests/lib_test.php

index 0a80394..c87022b 100644 (file)
@@ -54,14 +54,16 @@ $statusmsg = '';
 $errormsg  = '';
 
 if ($data = data_submitted() and confirm_sesskey()) {
-    if (admin_write_settings($data)) {
-        $statusmsg = get_string('changessaved');
-    }
-
+    $count = admin_write_settings($data);
     if (empty($adminroot->errors)) {
-        switch ($return) {
-            case 'site': redirect("$CFG->wwwroot/");
-            case 'admin': redirect("$CFG->wwwroot/$CFG->admin/");
+        // No errors. Did we change any setting?  If so, then indicate success.
+        if ($count) {
+            $statusmsg = get_string('changessaved');
+        } else {
+            switch ($return) {
+                case 'site': redirect("$CFG->wwwroot/");
+                case 'admin': redirect("$CFG->wwwroot/$CFG->admin/");
+            }
         }
     } else {
         $errormsg = get_string('errorwithsettings', 'admin');
index 916e6c2..71e9bcb 100644 (file)
@@ -29,7 +29,8 @@ $mode = required_param('mode', PARAM_ALPHANUMEXT);
 $classformode = array(
     'assign' => 'core_role_allow_assign_page',
     'override' => 'core_role_allow_override_page',
-    'switch' => 'core_role_allow_switch_page'
+    'switch' => 'core_role_allow_switch_page',
+    'view' => 'core_role_allow_view_page'
 );
 if (!isset($classformode[$mode])) {
     print_error('invalidmode', '', '', $mode);
@@ -58,6 +59,9 @@ if (optional_param('submit', false, PARAM_BOOL) && data_submitted() && confirm_s
         case 'switch':
             $event = \core\event\role_allow_switch_updated::create(array('context' => $syscontext));
             break;
+        case 'view':
+            $event = \core\event\role_allow_view_updated::create(array('context' => $syscontext));
+            break;
     }
     if ($event) {
         $event->trigger();
index 8d31cc0..a89228f 100644 (file)
@@ -33,7 +33,7 @@ class core_role_allow_assign_page extends core_role_allow_role_page {
     }
 
     protected function set_allow($fromroleid, $targetroleid) {
-        allow_assign($fromroleid, $targetroleid);
+        core_role_set_assign_allowed($fromroleid, $targetroleid);
     }
 
     protected function get_cell_tooltip($fromrole, $targetrole) {
index a277abb..4b160ee 100644 (file)
@@ -33,7 +33,7 @@ class core_role_allow_override_page extends core_role_allow_role_page {
     }
 
     protected function set_allow($fromroleid, $targetroleid) {
-        allow_override($fromroleid, $targetroleid);
+        core_role_set_override_allowed($fromroleid, $targetroleid);
     }
 
     protected function get_cell_tooltip($fromrole, $targetrole) {
index 934bda3..5b22e1e 100644 (file)
@@ -41,7 +41,7 @@ class core_role_allow_switch_page extends core_role_allow_role_page {
     }
 
     protected function set_allow($fromroleid, $targetroleid) {
-        allow_switch($fromroleid, $targetroleid);
+        core_role_set_switch_allowed($fromroleid, $targetroleid);
     }
 
     protected function is_allowed_target($targetroleid) {
diff --git a/admin/roles/classes/allow_view_page.php b/admin/roles/classes/allow_view_page.php
new file mode 100644 (file)
index 0000000..f1a1031
--- /dev/null
@@ -0,0 +1,77 @@
+<?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/>.
+
+/**
+ * Role view matrix.
+ *
+ * @package    core_role
+ * @copyright  2016 onwards Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Subclass of role_allow_role_page for the Allow views tab.
+ *
+ * @package    core_role
+ * @copyright  2016 onwards Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_role_allow_view_page extends core_role_allow_role_page {
+    /** @var array */
+    protected $allowedtargetroles;
+
+    /**
+     * core_role_allow_view_page constructor.
+     */
+    public function __construct() {
+        parent::__construct('role_allow_view', 'allowview');
+    }
+
+
+    /**
+     * Allow from role to view target role.
+     * @param int $fromroleid
+     * @param int $targetroleid
+     */
+    protected function set_allow($fromroleid, $targetroleid) {
+        core_role_set_view_allowed($fromroleid, $targetroleid);
+    }
+
+    /**
+     * Get tool tip for cell.
+     * @param string $fromrole
+     * @param string $targetrole
+     * @return string
+     * @throws \coding_exception
+     */
+    protected function get_cell_tooltip($fromrole, $targetrole) {
+        $a = new stdClass;
+        $a->fromrole = $fromrole->localname;
+        $a->targetrole = $targetrole->localname;
+        return get_string('allowroletoview', 'core_role', $a);
+    }
+
+    /**
+     * Get intro text for role allow view page.
+     * @return string
+     * @throws \coding_exception
+     */
+    public function get_intro_text() {
+        return get_string('configallowview', 'core_admin');
+    }
+}
index bcf6a49..957fe85 100644 (file)
@@ -42,6 +42,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     protected $allowassign;
     protected $allowoverride;
     protected $allowswitch;
+    protected $allowview;
 
     public function __construct($context, $roleid) {
         $this->roleid = $roleid;
@@ -72,6 +73,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             $this->allowassign = array_keys($this->get_allow_roles_list('assign'));
             $this->allowoverride = array_keys($this->get_allow_roles_list('override'));
             $this->allowswitch = array_keys($this->get_allow_roles_list('switch'));
+            $this->allowview = array_keys($this->get_allow_roles_list('view'));
 
         } else {
             $this->role = new stdClass;
@@ -83,6 +85,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             $this->allowassign = array();
             $this->allowoverride = array();
             $this->allowswitch = array();
+            $this->allowview = array();
         }
         parent::load_current_permissions();
     }
@@ -162,6 +165,10 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if (!is_null($allow)) {
             $this->allowswitch = $allow;
         }
+        $allow = optional_param_array('allowview', null, PARAM_INT);
+        if (!is_null($allow)) {
+            $this->allowview = $allow;
+        }
 
         // Now read the permissions for each capability.
         parent::read_submitted_permissions();
@@ -178,7 +185,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
      * @param int $roleid role id or 0 for no role
      * @param array $options array with following keys:
      *      'name', 'shortname', 'description', 'permissions', 'archetype',
-     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
+     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
+     *      'allowview'
      */
     public function force_duplicate($roleid, array $options) {
         global $DB;
@@ -215,6 +223,9 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             if ($options['allowswitch']) {
                 $this->allowswitch = array();
             }
+            if ($options['allowview']) {
+                $this->allowview = array();
+            }
 
             if ($options['permissions']) {
                 foreach ($this->capabilities as $capid => $cap) {
@@ -260,6 +271,9 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if ($options['allowswitch']) {
             $this->allowswitch = array_keys($this->get_allow_roles_list('switch', $roleid));
         }
+        if ($options['allowview']) {
+            $this->allowview = array_keys($this->get_allow_roles_list('view', $roleid));
+        }
 
         if ($options['permissions']) {
             $this->permissions = $DB->get_records_menu('role_capabilities',
@@ -280,7 +294,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
      * @param string $archetype
      * @param array $options array with following keys:
      *      'name', 'shortname', 'description', 'permissions', 'archetype',
-     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
+     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
+     *      'allowview'
      */
     public function force_archetype($archetype, array $options) {
         $archetypes = get_role_archetypes();
@@ -321,6 +336,9 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if ($options['allowswitch']) {
             $this->allowswitch = get_default_role_archetype_allows('switch', $archetype);
         }
+        if ($options['allowview']) {
+            $this->allowview = get_default_role_archetype_allows('view', $archetype);
+        }
 
         if ($options['permissions']) {
             $defaultpermissions = get_default_capabilities($archetype);
@@ -340,7 +358,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
      * @param string $xml
      * @param array $options array with following keys:
      *      'name', 'shortname', 'description', 'permissions', 'archetype',
-     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
+     *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
+     *      'allowview'
      */
     public function force_preset($xml, array $options) {
         if (!$info = core_role_preset::parse_preset($xml)) {
@@ -377,7 +396,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             }
         }
 
-        foreach (array('assign', 'override', 'switch') as $type) {
+        foreach (array('assign', 'override', 'switch', 'view') as $type) {
             if ($options['allow'.$type]) {
                 if (isset($info['allow'.$type])) {
                     $this->{'allow'.$type} = $info['allow'.$type];
@@ -438,6 +457,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $this->save_allow('assign');
         $this->save_allow('override');
         $this->save_allow('switch');
+        $this->save_allow('view');
 
         // Permissions.
         parent::save_changes();
@@ -449,7 +469,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $current = array_keys($this->get_allow_roles_list($type));
         $wanted = $this->{'allow'.$type};
 
-        $addfunction = 'allow_'.$type;
+        $addfunction = "core_role_set_{$type}_allowed";
         $deltable = 'role_allow_'.$type;
         $field = 'allow'.$type;
 
@@ -523,7 +543,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     protected function get_allow_roles_list($type, $roleid = null) {
         global $DB;
 
-        if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
+        if ($type !== 'assign' and $type !== 'switch' and $type !== 'override' and $type !== 'view') {
             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
             return array();
         }
@@ -547,11 +567,11 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     /**
      * Returns an array of roles with the allowed type.
      *
-     * @param string $type Must be one of: assign, switch, or override.
+     * @param string $type Must be one of: assign, switch, override or view.
      * @return array Am array of role names with the allowed type
      */
     protected function get_allow_role_control($type) {
-        if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
+        if ($type !== 'assign' and $type !== 'switch' and $type !== 'override' and $type !== 'view') {
             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
             return '';
         }
@@ -641,6 +661,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $this->print_field('menuallowassign', get_string('allowassign', 'core_role'), $this->get_allow_role_control('assign'));
         $this->print_field('menuallowoverride', get_string('allowoverride', 'core_role'), $this->get_allow_role_control('override'));
         $this->print_field('menuallowswitch', get_string('allowswitch', 'core_role'), $this->get_allow_role_control('switch'));
+        $this->print_field('menuallowview', get_string('allowview', 'core_role'), $this->get_allow_role_control('view'));
         if ($risks = $this->get_role_risks_info()) {
             $this->print_field('', get_string('rolerisks', 'core_role'), $risks);
         }
index 256f635..b994342 100644 (file)
@@ -84,7 +84,7 @@ class core_role_preset {
             $contextlevels->appendChild($dom->createElement('level', $name));
         }
 
-        foreach (array('assign', 'override', 'switch') as $type) {
+        foreach (array('assign', 'override', 'switch', 'view') as $type) {
             $allows = $dom->createElement('allow'.$type);
             $top->appendChild($allows);
             $records = $DB->get_records('role_allow_'.$type, array('roleid'=>$roleid), "allow$type ASC");
@@ -205,7 +205,7 @@ class core_role_preset {
             }
         }
 
-        foreach (array('assign', 'override', 'switch') as $type) {
+        foreach (array('assign', 'override', 'switch', 'view') as $type) {
             $values = self::get_node_children_values($dom, '/role/allow'.$type, 'shortname');
             if (!isset($values)) {
                 $info['allow'.$type] = null;
index ff2024d..426c3d8 100644 (file)
@@ -79,6 +79,7 @@ class core_role_preset_form extends moodleform {
             $mform->addElement('advcheckbox', 'allowassign', get_string('allowassign', 'core_role'));
             $mform->addElement('advcheckbox', 'allowoverride', get_string('allowoverride', 'core_role'));
             $mform->addElement('advcheckbox', 'allowswitch', get_string('allowswitch', 'core_role'));
+            $mform->addElement('advcheckbox', 'allowview', get_string('allowview', 'core_role'));
             $mform->addElement('advcheckbox', 'permissions', get_string('permissions', 'core_role'));
         }
 
index d670bae..de3eac1 100644 (file)
@@ -103,7 +103,8 @@ if ($action === 'add' and $resettype !== 'none') {
             'contextlevels' => 1,
             'allowassign'   => 1,
             'allowoverride' => 1,
-            'allowswitch'   => 1);
+            'allowswitch'   => 1,
+            'allowview'   => 1);
         if ($showadvanced) {
             $definitiontable = new core_role_define_role_table_advanced($systemcontext, 0);
         } else {
@@ -150,7 +151,8 @@ if ($action === 'add' and $resettype !== 'none') {
             'contextlevels' => $data->contextlevels,
             'allowassign'   => $data->allowassign,
             'allowoverride' => $data->allowoverride,
-            'allowswitch'   => $data->allowswitch);
+            'allowswitch'   => $data->allowswitch,
+            'allowview'     => $data->allowview);
         if ($showadvanced) {
             $definitiontable = new core_role_define_role_table_advanced($systemcontext, $roleid);
         } else {
index 78f9f75..a9a8385 100644 (file)
@@ -29,6 +29,7 @@ $toprow[] = new tabobject('manage', new moodle_url('/admin/roles/manage.php'), g
 $toprow[] = new tabobject('assign', new moodle_url('/admin/roles/allow.php', array('mode'=>'assign')), get_string('allowassign', 'core_role'));
 $toprow[] = new tabobject('override', new moodle_url('/admin/roles/allow.php', array('mode'=>'override')), get_string('allowoverride', 'core_role'));
 $toprow[] = new tabobject('switch', new moodle_url('/admin/roles/allow.php', array('mode'=>'switch')), get_string('allowswitch', 'core_role'));
+$toprow[] = new tabobject('view', new moodle_url('/admin/roles/allow.php', ['mode' => 'view']), get_string('allowview', 'core_role'));
 
 echo $OUTPUT->tabtree($toprow, $currenttab);
 
index f0a33c9..bb3de89 100644 (file)
@@ -11,6 +11,7 @@
                 <xs:element ref="allowassign" minOccurs="0"/>
                 <xs:element ref="allowoverride" minOccurs="0"/>
                 <xs:element ref="allowswitch" minOccurs="0"/>
+                <xs:element ref="allowview" minOccurs="0"/>
                 <xs:element ref="permissions" minOccurs="0"/>
             </xs:sequence>
         </xs:complexType>
             </xs:sequence>
         </xs:complexType>
     </xs:element>
+    <xs:element name="allowview">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" ref="shortname"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
     <xs:element name="permissions">
         <xs:complexType>
             <xs:sequence>
index 0899480..4ab0ebc 100644 (file)
@@ -44,7 +44,7 @@ class core_role_preset_testcase extends advanced_testcase {
             $contextlevels = get_role_contextlevels($role->id);
             $this->assertEquals(array_values($contextlevels), array_values($info['contextlevels']));
 
-            foreach (array('assign', 'override', 'switch') as $type) {
+            foreach (array('assign', 'override', 'switch', 'view') as $type) {
                 $records = $DB->get_records('role_allow_'.$type, array('roleid'=>$role->id), "allow$type ASC");
                 $allows = array();
                 foreach ($records as $record) {
index dfb6043..5539517 100644 (file)
@@ -32,15 +32,16 @@ $focus = '';
 // now we'll deal with the case that the admin has submitted the form with changed settings
 if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and $data->action == 'save-settings') {
     require_capability('moodle/site:config', $context);
-    if (admin_write_settings($data)) {
-        redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
-    }
-
+    $count = admin_write_settings($data);
     if (!empty($adminroot->errors)) {
         $errormsg = get_string('errorwithsettings', 'admin');
         $firsterror = reset($adminroot->errors);
         $focus = $firsterror->id;
     } else {
+        // No errors. Did we change any setting? If so, then redirect with success.
+        if ($count) {
+            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+        }
         redirect($PAGE->url);
     }
 }
index 9ce7af6..9cac439 100644 (file)
@@ -132,7 +132,17 @@ foreach ($searchareas as $area) {
                 $laststatus = '';
             }
             $columns[] = $laststatus;
-            $columns[] = html_writer::link(admin_searcharea_action_url('delete', $areaid), 'Delete index');
+            $accesshide = html_writer::span($area->get_visible_name(), 'accesshide');
+            $actions = [];
+            $actions[] = $OUTPUT->pix_icon('t/delete', '') .
+                    html_writer::link(admin_searcharea_action_url('delete', $areaid),
+                    get_string('deleteindex', 'search', $accesshide));
+            if ($area->supports_get_document_recordset()) {
+                $actions[] = $OUTPUT->pix_icon('i/reload', '') . html_writer::link(
+                        new moodle_url('searchreindex.php', ['areaid' => $areaid]),
+                        get_string('gradualreindex', 'search', $accesshide));
+            }
+            $columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
 
         } else {
             $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
@@ -165,6 +175,13 @@ echo $OUTPUT->single_button(admin_searcharea_action_url('deleteall'), get_string
 echo $OUTPUT->box_end();
 
 echo html_writer::table($table);
+
+if (empty($searchmanagererror)) {
+    // Show information about queued index requests for specific contexts.
+    $searchrenderer = $PAGE->get_renderer('core_search');
+    echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
+}
+
 echo $OUTPUT->footer();
 
 /**
diff --git a/admin/searchreindex.php b/admin/searchreindex.php
new file mode 100644 (file)
index 0000000..9fa7a3c
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Adds a search area to the queue for indexing.
+ *
+ * @package core_search
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require(__DIR__ . '/../config.php');
+
+// Check access.
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('searchareas', '', null, (new moodle_url('/admin/searchreindex.php'))->out(false));
+
+// Get area parameter and check it exists.
+$areaid = required_param('areaid', PARAM_ALPHAEXT);
+$area = \core_search\manager::get_search_area($areaid);
+if ($area === false) {
+    throw new moodle_exception('invalidrequest');
+}
+$areaname = $area->get_visible_name();
+
+// Start page output.
+$heading = get_string('gradualreindex', 'search', '');
+$PAGE->set_title($PAGE->title . ': ' . $heading);
+$PAGE->navbar->add($heading);
+echo $OUTPUT->header();
+echo $OUTPUT->heading($heading);
+
+// If sesskey is supplied, actually carry out the action.
+if (optional_param('sesskey', '', PARAM_ALPHANUM)) {
+    require_sesskey();
+
+    // Get all contexts for search area. This query can take time in large cases.
+    \core_php_time_limit::raise(0);
+    $contextiterator = $area->get_contexts_to_reindex();
+
+    $progress = new \core\progress\display_if_slow('');
+    $progress->start_progress($areaname);
+
+    // Request reindexing for each context (with low priority).
+    $count = 0;
+    foreach ($contextiterator as $context) {
+        \core_php_time_limit::raise(30);
+        \core_search\manager::request_index($context, $area->get_area_id(),
+                \core_search\manager::INDEX_PRIORITY_REINDEXING);
+        $progress->progress();
+        $count++;
+    }
+
+    // Unset the iterator which should close the recordset (if there is one).
+    unset($contextiterator);
+
+    $progress->end_progress();
+
+    $a = (object)['name' => html_writer::tag('strong', $areaname), 'count' => $count];
+    echo $OUTPUT->box(get_string('gradualreindex_queued', 'search', $a));
+
+    echo $OUTPUT->continue_button(new moodle_url('/admin/searchareas.php'));
+} else {
+    // Display confirmation prompt.
+    echo $OUTPUT->confirm(get_string('gradualreindex_confirm', 'search', html_writer::tag('strong', $areaname)),
+            new single_button(new moodle_url('/admin/searchreindex.php', ['areaid' => $areaid,
+                'sesskey' => sesskey()]), get_string('continue'), 'post', true),
+            new single_button(new moodle_url('/admin/searchareas.php'), get_string('cancel'), 'get'));
+}
+
+echo $OUTPUT->footer();
index 72f7f9c..6292240 100644 (file)
@@ -39,11 +39,15 @@ $statusmsg = '';
 $errormsg  = '';
 
 if ($data = data_submitted() and confirm_sesskey()) {
-    if (admin_write_settings($data)) {
-        redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
-    }
 
+    $count = admin_write_settings($data);
+    // Regardless of whether any setting change was written (a positive count), check validation errors for those that didn't.
     if (empty($adminroot->errors)) {
+        // No errors. Did we change any setting? If so, then redirect with success.
+        if ($count) {
+            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+        }
+        // We didn't change a setting.
         switch ($return) {
             case 'site': redirect("$CFG->wwwroot/");
             case 'admin': redirect("$CFG->wwwroot/$CFG->admin/");
index 8dfceea..8598f54 100644 (file)
@@ -42,6 +42,9 @@ if (has_capability('moodle/grade:manage', $systemcontext)
         // enable publishing in exports/imports
         $temp->add(new admin_setting_configcheckbox('gradepublishing', new lang_string('gradepublishing', 'grades'), new lang_string('gradepublishing_help', 'grades'), 0));
 
+        $temp->add(new admin_setting_configcheckbox('grade_export_exportfeedback', new lang_string('exportfeedback', 'grades'),
+                                                  new lang_string('exportfeedback_desc', 'grades'), 0));
+
         $temp->add(new admin_setting_configselect('grade_export_displaytype', new lang_string('gradeexportdisplaytype', 'grades'),
                                                   new lang_string('gradeexportdisplaytype_desc', 'grades'), GRADE_DISPLAY_TYPE_REAL, $display_types));
 
index 47def2b..f08f71c 100644 (file)
@@ -67,7 +67,7 @@ $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
-$string['modelid'] = 'Model id';
+$string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
 $string['modeltimesplitting'] = 'Time splitting';
index b8fe75b..55fce2e 100644 (file)
@@ -96,7 +96,7 @@ $string['systemaccountconnected'] = 'System account connected';
 $string['systemaccountnotconnected'] = 'System account not connected';
 $string['systemauthstatus'] = 'System account connected';
 $string['usebasicauth'] = 'Authenticate token requests via HTTP headers';
-$string['usebasicauth_help'] = 'Utilize the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
+$string['usebasicauth_help'] = 'Utilise the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
 $string['userfieldexternalfield'] = 'External field name';
 $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.';
 $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.';
index 26a0096..679a0cf 100644 (file)
@@ -9,6 +9,7 @@ Feature: Basic recycle bin functionality
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher@asd.com |
       | student1 | Student | 1 | student@asd.com |
+      | student2 | Student | 2 | student2@asd.com |
     And the following "courses" exist:
       | fullname | shortname |
       | Course 1 | C1 |
@@ -16,6 +17,23 @@ Feature: Basic recycle bin functionality
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | teacher1 | C2 | editingteacher |
+      | student1 | C2 | student |
+      | student2 | C2 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Group A | C2 | G1 |
+      | Group B | C2 | G2 |
+      | Group C | C2 | G3 |
+    And the following "group members" exist:
+      | user | group |
+      | teacher1 | G1 |
+      | teacher1 | G2 |
+      | student1 | G1 |
+      | student2 | G1 |
+      | student2 | G2 |
     And the following config values are set as admin:
       | coursebinenable | 1 | tool_recyclebin |
       | categorybinenable | 1 | tool_recyclebin |
@@ -58,6 +76,12 @@ Feature: Basic recycle bin functionality
     And I wait to be redirected
     And I go to the courses management page
     And I should see "Course 2" in the "#course-listing" "css_element"
+    And I am on "Course 2" course homepage
+    And I navigate to "Groups" node in "Course administration > Users"
+    And I follow "Overview"
+    And "Student 1" "text" should exist in the "Group A" "table_row"
+    And "Student 2" "text" should exist in the "Group A" "table_row"
+    And "Student 2" "text" should exist in the "Group B" "table_row"
 
   @javascript
   Scenario: Deleting a single item from the recycle bin
index 7e94fcf..0de826c 100644 (file)
@@ -71,6 +71,8 @@ if ($options['list']) {
     $shorttime = get_string('strftimedatetimeshort');
 
     $tasks = \core\task\manager::get_all_scheduled_tasks();
+    echo str_pad(get_string('scheduledtasks', 'tool_task'), 50, ' ') . ' ' . str_pad(get_string('runpattern', 'tool_task'), 17, ' ')
+        . ' ' . str_pad(get_string('lastruntime', 'tool_task'), 40, ' ') . get_string('nextruntime', 'tool_task') . "\n";
     foreach ($tasks as $task) {
         $class = '\\' . get_class($task);
         $schedule = $task->get_minute() . ' '
@@ -80,6 +82,7 @@ if ($options['list']) {
             . $task->get_month() . ' '
             . $task->get_day_of_week();
         $nextrun = $task->get_next_run_time();
+        $lastrun = $task->get_last_run_time();
 
         $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
         $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled();
@@ -94,7 +97,14 @@ if ($options['list']) {
             $nextrun = get_string('asap', 'tool_task');
         }
 
-        echo str_pad($class, 50, ' ') . ' ' . str_pad($schedule, 17, ' ') . ' ' . $nextrun . "\n";
+        if ($lastrun) {
+            $lastrun = userdate($lastrun);
+        } else {
+            $lastrun = get_string('never');
+        }
+
+        echo str_pad($class, 50, ' ') . ' ' . str_pad($schedule, 17, ' ') .
+            ' ' . str_pad($lastrun, 40, ' ') . ' ' . $nextrun . "\n";
     }
     exit(0);
 }
index 8663112..2f382b0 100644 (file)
@@ -42,6 +42,7 @@ $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
 $string['runnow'] = 'Run now';
 $string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
+$string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
index abdb88a..2e39463 100644 (file)
@@ -93,11 +93,11 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $mform->addHelpButton('defaults[visible]', 'coursevisibility');
         $mform->setDefault('defaults[visible]', $courseconfig->visible);
 
-        $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
+        $mform->addElement('date_time_selector', 'defaults[startdate]', get_string('startdate'));
         $mform->addHelpButton('defaults[startdate]', 'startdate');
         $mform->setDefault('defaults[startdate]', time() + 3600 * 24);
 
-        $mform->addElement('date_selector', 'defaults[enddate]', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'defaults[enddate]', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('defaults[enddate]', 'enddate');
 
         $courseformats = get_sorted_course_formats(true);
index 84a70c2..78eb5cf 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js and b/admin/tool/usertours/amd/build/tour.min.js differ
index ff3a01c..f77f4d9 100644 (file)
@@ -619,26 +619,43 @@ Tour.prototype.addEventHandler = function (eventName, handler) {
  */
 Tour.prototype.processStepListeners = function (stepConfig) {
     this.listeners.push(
-    // Next/Previous buttons.
-    {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
-    }, {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
-    },
-
-    // Close and end tour buttons.
-    {
-        node: this.currentStepNode,
-        args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
-    },
-
-    // Keypresses.
-    {
-        node: $('body'),
-        args: ['keydown', $.proxy(this.handleKeyDown, this)]
-    });
+        // Next/Previous buttons.
+        {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
+        }, {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
+        },
+
+        // Close and end tour buttons.
+        {
+            node: this.currentStepNode,
+            args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
+        },
+
+        // Click backdrop and hide tour.
+        {
+            node: $('[data-flexitour="backdrop"]'),
+            args: ['click', $.proxy(this.hide, this)]
+        },
+
+        // Click out and hide tour without backdrop.
+        {
+            node: $('body'),
+            args: ['click', $.proxy(function (e) {
+                // Handle click in or click out tour content,
+                // if click out, hide tour.
+                if (!this.currentStepNode.is(e.target) && $(e.target).closest('[data-role="flexitour-step"]').length === 0) {
+                    this.hide();
+                }}, this)]
+        },
+
+        // Keypresses.
+        {
+            node: $('body'),
+            args: ['keydown', $.proxy(this.handleKeyDown, this)]
+        });
 
     if (stepConfig.moveOnClick) {
         var targetNode = this.getStepTarget(stepConfig);
@@ -904,7 +921,7 @@ Tour.prototype.announceStep = function (stepConfig) {
  * @param   {EventFacade} e
  */
 Tour.prototype.handleKeyDown = function (e) {
-    var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button';
+    var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button:enabled';
     switch (e.keyCode) {
         case 27:
             this.endTour();
@@ -923,8 +940,17 @@ Tour.prototype.handleKeyDown = function (e) {
                 var activeElement = $(document.activeElement);
                 var stepTarget = this.getStepTarget(this.currentStepConfig);
                 var tabbableNodes = $(tabbableSelector);
+                var dialogContainer = $('span[data-flexitour="container"]');
                 var currentIndex = void 0;
-                tabbableNodes.filter(function (index, element) {
+                // Filter out element which is not belong to target section or dialogue.
+                if (stepTarget) {
+                    tabbableNodes = tabbableNodes.filter(function (index, element) {
+                        return stepTarget != null && (stepTarget.has(element).length || dialogContainer.has(element).length || stepTarget.is(element) || dialogContainer.is(element));
+                    });
+                }
+
+                // Find index of focusing element.
+                tabbableNodes.each(function (index, element) {
                     if (activeElement.is(element)) {
                         currentIndex = index;
                         return false;
@@ -934,7 +960,7 @@ Tour.prototype.handleKeyDown = function (e) {
                 var nextIndex = void 0;
                 var nextNode = void 0;
                 var focusRelevant = void 0;
-                if (currentIndex) {
+                if (currentIndex != void 0) {
                     var direction = 1;
                     if (e.shiftKey) {
                         direction = -1;
@@ -1090,6 +1116,16 @@ Tour.prototype.hide = function (transition) {
         $(this).remove();
     });
 
+    // Remove aria-describedby and tabindex attributes.
+    if (this.currentStepNode && this.currentStepNode.length) {
+        var stepId = this.currentStepNode.attr('id');
+        if (stepId) {
+            var currentStepElement = '[aria-describedby="' + stepId + '-body"]';
+            $(currentStepElement).removeAttr('tabindex');
+            $(currentStepElement).removeAttr('aria-describedby');
+        }
+    }
+
     // Reset the listeners.
     this.resetStepListeners();
 
index fdcd28d..0bafb34 100644 (file)
@@ -157,3 +157,23 @@ Feature: Apply tour filters to a tour
     When I am on "Course 2" course homepage
     And I wait until the page is ready
     Then I should not see "Welcome to your course tour."
+
+  @javascript
+  Scenario: Aria tags should not exist
+    Given I log in as "admin"
+    And I open the User tour settings page
+    # Turn on default tour for boost theme.
+    And I click on "Enable" "link" in the "Boost - administrator" "table_row"
+    And I am on site homepage
+    When I click on "Next" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should exist
+    And "button[tabindex]" "css_element" should exist
+    When I click on "Next" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should not exist
+    And "button[tabindex]" "css_element" should not exist
+    When I click on "Previous" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should exist
+    And "button[tabindex]" "css_element" should exist
+    When I click on "End tour" "button"
+    Then "button[aria-describedby^='tour-step-tool_usertours']" "css_element" should not exist
+    And "button[tabindex]" "css_element" should not exist
index 7d9c340..f55d8b1 100644 (file)
@@ -4,7 +4,7 @@
     <location>amd/src/tour.js</location>
     <name>Flexitour</name>
     <license>GPLv3</license>
-    <version>0.10.0</version>
+    <version>0.12.0</version>
     <licenseversion>3</licenseversion>
   </library>
   <library>
index e816350..2e4a1a9 100644 (file)
@@ -80,7 +80,7 @@ function transformForm(event) {
             decimalsField.value = '';
             break;
         case '2':  // XMLDB_TYPE_NUMBER
-            lengthTip.innerHTML = ' 1...20'; // Hardcoded xmldb_field::NUMBER_MAX_LENGTH, yes!
+            lengthTip.innerHTML = ' 1...38'; // Hardcoded xmldb_field::NUMBER_MAX_LENGTH, yes!
             lengthField.disabled = false;
             decimalsTip.innerHTML = ' 0...length or empty';
             break;
index aaabf94..6d0cec5 100644 (file)
@@ -48,6 +48,7 @@ class edit_field_save extends XMLDBAction {
             'floatincorrectlength' => 'tool_xmldb',
             'charincorrectlength' => 'tool_xmldb',
             'numberincorrectdecimals' => 'tool_xmldb',
+            'numberincorrectwholepart' => 'tool_xmldb',
             'floatincorrectdecimals' => 'tool_xmldb',
             'defaultincorrect' => 'tool_xmldb',
             'back' => 'tool_xmldb',
@@ -158,6 +159,9 @@ class edit_field_save extends XMLDBAction {
                                        $decimals < $length))) {
                 $errors[] = $this->str['numberincorrectdecimals'];
             }
+            if (!empty($decimals) && ($length - $decimals > xmldb_field::INTEGER_MAX_LENGTH)) {
+                $errors[] = $this->str['numberincorrectwholepart'];
+            }
             if (!(empty($default) || (is_numeric($default) &&
                                        !empty($default)))) {
                 $errors[] = $this->str['defaultincorrect'];
index 80a7dda..5c52efd 100644 (file)
@@ -165,6 +165,7 @@ $string['nowrongintsfound'] = 'No wrong integers have been found, your DB doesn\
 $string['nowrongoraclesemanticsfound'] = 'No Oracle columns using BYTE semantics have been found, your DB doesn\'t need further actions.';
 $string['numberincorrectdecimals'] = 'Incorrect number of decimals for number field';
 $string['numberincorrectlength'] = 'Incorrect length for number field';
+$string['numberincorrectwholepart'] = 'Too big whole number part for number field';
 $string['pendingchanges'] = 'Note: You have performed changes to this file. They can be saved at any moment.';
 $string['pendingchangescannotbesaved'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server.';
 $string['pendingchangescannotbesavedreload'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server. Then reload this page and you should be able to save those changes.';
index d5d0153..21009ba 100644 (file)
@@ -226,13 +226,17 @@ class analytics_model_testcase extends advanced_testcase {
         $this->model->mark_as_trained();
         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 
+        // Wait for the current timestamp to change.
+        $this->waitForSecond();
         $this->model->enable('\core\analytics\time_splitting\deciles');
-        $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
+        $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
+        $uniqueid = $this->model->get_unique_id();
 
-        // Wait 1 sec so the timestamp changes.
-        sleep(1);
+        // Wait for the current timestamp to change.
+        $this->waitForSecond();
         $this->model->enable('\core\analytics\time_splitting\quarters');
         $this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
+        $this->assertNotEquals($uniqueid, $this->model->get_unique_id());
     }
 
     /**
index 2dcd4ae..549e103 100644 (file)
@@ -42,6 +42,7 @@ class auth_plugin_db extends auth_plugin_base {
 
         $this->authtype = 'db';
         $this->config = get_config('auth_db');
+        $this->errorlogtag = '[AUTH DB] ';
         if (empty($this->config->extencoding)) {
             $this->config->extencoding = 'utf-8';
         }
@@ -381,7 +382,7 @@ class auth_plugin_db extends auth_plugin_base {
                     list($in_sql, $params) = $DB->get_in_or_equal($userlistchunk, SQL_PARAMS_NAMED, 'u', true);
                     $params['authtype'] = $this->authtype;
                     $params['mnethostid'] = $CFG->mnet_localhost_id;
-                    $sql = "SELECT u.id, u.username
+                    $sql = "SELECT u.id, u.username, u.suspended
                           FROM {user} u
                          WHERE u.auth = :authtype AND u.deleted = 0 AND u.mnethostid = :mnethostid AND u.username {$in_sql}";
                     $update_users = $update_users + $DB->get_records_sql($sql, $params);
@@ -391,7 +392,7 @@ class auth_plugin_db extends auth_plugin_base {
                     $trace->output("User entries to update: ".count($update_users));
 
                     foreach ($update_users as $user) {
-                        if ($this->update_user_record($user->username, $updatekeys)) {
+                        if ($this->update_user_record($user->username, $updatekeys, false, (bool) $user->suspended)) {
                             $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
                         } else {
                             $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))." - ".get_string('skipped'), 1);
@@ -545,67 +546,6 @@ class auth_plugin_db extends auth_plugin_base {
         return $user;
     }
 
-    /**
-     * will update a local user record from an external source.
-     * is a lighter version of the one in moodlelib -- won't do
-     * expensive ops such as enrolment.
-     *
-     * If you don't pass $updatekeys, there is a performance hit and
-     * values removed from DB won't be removed from moodle.
-     *
-     * @param string $username username
-     * @param bool $updatekeys
-     * @return stdClass
-     */
-    function update_user_record($username, $updatekeys=false) {
-        global $CFG, $DB;
-
-        //just in case check text case
-        $username = trim(core_text::strtolower($username));
-
-        // get the current user record
-        $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id));
-        if (empty($user)) { // trouble
-            error_log("Cannot update non-existent user: $username");
-            print_error('auth_dbusernotexist','auth_db',$username);
-            die;
-        }
-
-        // Ensure userid is not overwritten.
-        $userid = $user->id;
-        $needsupdate = false;
-
-        $updateuser = new stdClass();
-        $updateuser->id = $userid;
-        if ($newinfo = $this->get_userinfo($username)) {
-            $newinfo = truncate_userinfo($newinfo);
-
-            if (empty($updatekeys)) { // All keys? This does not support removing values.
-                $updatekeys = array_keys($newinfo);
-            }
-
-            foreach ($updatekeys as $key) {
-                if (isset($newinfo[$key])) {
-                    $value = $newinfo[$key];
-                } else {
-                    $value = '';
-                }
-
-                if (!empty($this->config->{'field_updatelocal_' . $key})) {
-                    if (isset($user->{$key}) and $user->{$key} != $value) { // Only update if it's changed.
-                        $needsupdate = true;
-                        $updateuser->$key = $value;
-                    }
-                }
-            }
-        }
-        if ($needsupdate) {
-            require_once($CFG->dirroot . '/user/lib.php');
-            user_update_user($updateuser);
-        }
-        return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
-    }
-
     /**
      * Called when the user record is updated.
      * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
index 3b21d60..a9af49f 100644 (file)
@@ -66,7 +66,6 @@ $string['auth_dbupdateusers_description'] = 'As well as inserting new users, upd
 $string['auth_dbupdatinguser'] = 'Updating user {$a->name} id {$a->id}';
 $string['auth_dbuser'] = 'Username with read access to the database';
 $string['auth_dbuser_key'] = 'DB user';
-$string['auth_dbusernotexist'] = 'Cannot update non-existent user: {$a}';
 $string['auth_dbuserstoadd'] = 'User entries to add: {$a}';
 $string['auth_dbuserstoremove'] = 'User entries to remove: {$a}';
 $string['pluginname'] = 'External database';
index 91218ad..b923003 100644 (file)
@@ -547,7 +547,9 @@ class auth_plugin_ldap extends auth_plugin_base {
         // Save any custom profile field information
         profile_save_data($user);
 
-        $this->update_user_record($user->username);
+        $userinfo = $this->get_userinfo($user->username);
+        $this->update_user_record($user->username, false, false, $this->is_user_suspended((object) $userinfo));
+
         // This will also update the stored hash to the latest algorithm
         // if the existing hash is using an out-of-date algorithm (or the
         // legacy md5 algorithm).
@@ -668,6 +670,8 @@ class auth_plugin_ldap extends auth_plugin_base {
     function sync_users($do_updates=true) {
         global $CFG, $DB;
 
+        require_once($CFG->dirroot . '/user/profile/lib.php');
+
         print_string('connectingldap', 'auth_ldap');
         $ldapconnection = $this->ldap_connect();
 
@@ -837,23 +841,7 @@ class auth_plugin_ldap extends auth_plugin_base {
 /// User Updates - time-consuming (optional)
         if ($do_updates) {
             // Narrow down what fields we need to update
-            $all_keys = array_keys(get_object_vars($this->config));
-            $updatekeys = array();
-            foreach ($all_keys as $key) {
-                if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) {
-                    // If we have a field to update it from
-                    // and it must be updated 'onlogin' we
-                    // update it on cron
-                    if (!empty($this->config->{'field_map_'.$match[1]})
-                         and $this->config->{$match[0]} === 'onlogin') {
-                        array_push($updatekeys, $match[1]); // the actual key name
-                    }
-                }
-            }
-            if ($this->config->suspended_attribute && $this->config->sync_suspended) {
-                $updatekeys[] = 'suspended';
-            }
-            unset($all_keys); unset($key);
+            $updatekeys = $this->get_profile_keys();
 
         } else {
             print_string('noupdatestobedone', 'auth_ldap');
@@ -872,7 +860,9 @@ class auth_plugin_ldap extends auth_plugin_base {
 
                 foreach ($users as $user) {
                     echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id));
-                    if (!$this->update_user_record($user->username, $updatekeys, true)) {
+                    $userinfo = $this->get_userinfo($user->username);
+                    if (!$this->update_user_record($user->username, $updatekeys, true,
+                            $this->is_user_suspended((object) $userinfo))) {
                         echo ' - '.get_string('skipped');
                     }
                     echo "\n";
@@ -928,14 +918,17 @@ class auth_plugin_ldap extends auth_plugin_base {
 
                 $id = user_create_user($user, false);
                 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
-                $user = $DB->get_record('user', array('id' => $id));
+                $euser = $DB->get_record('user', array('id' => $id));
 
                 if (!empty($this->config->forcechangepassword)) {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
                 }
 
+                // Save custom profile fields.
+                $this->update_user_record($user->username, $this->get_profile_keys(true), false);
+
                 // Add roles if needed.
-                $this->sync_roles($user);
+                $this->sync_roles($euser);
 
             }
             $transaction->allow_commit();
@@ -950,72 +943,6 @@ class auth_plugin_ldap extends auth_plugin_base {
         return true;
     }
 
-    /**
-     * Update a local user record from an external source.
-     * This is a lighter version of the one in moodlelib -- won't do
-     * expensive ops such as enrolment.
-     *
-     * If you don't pass $updatekeys, there is a performance hit and
-     * values removed from LDAP won't be removed from moodle.
-     *
-     * @param string $username username
-     * @param boolean $updatekeys true to update the local record with the external LDAP values.
-     * @param bool $triggerevent set false if user_updated event should not be triggered.
-     *             This will not affect user_password_updated event triggering.
-     * @return stdClass|bool updated user record or false if there is no new info to update.
-     */
-    function update_user_record($username, $updatekeys = false, $triggerevent = false) {
-        global $CFG, $DB;
-
-        // Just in case check text case
-        $username = trim(core_text::strtolower($username));
-
-        // Get the current user record
-        $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id));
-        if (empty($user)) { // trouble
-            error_log($this->errorlogtag.get_string('auth_dbusernotexist', 'auth_db', '', $username));
-            print_error('auth_dbusernotexist', 'auth_db', '', $username);
-            die;
-        }
-
-        // Protect the userid from being overwritten
-        $userid = $user->id;
-
-        if ($newinfo = $this->get_userinfo($username)) {
-            $newinfo = truncate_userinfo($newinfo);
-
-            if (empty($updatekeys)) { // all keys? this does not support removing values
-                $updatekeys = array_keys($newinfo);
-            }
-
-            if (!empty($updatekeys)) {
-                $newuser = new stdClass();
-                $newuser->id = $userid;
-                // The cast to int is a workaround for MDL-53959.
-                $newuser->suspended = (int)$this->is_user_suspended((object) $newinfo);
-
-                foreach ($updatekeys as $key) {
-                    if (isset($newinfo[$key])) {
-                        $value = $newinfo[$key];
-                    } else {
-                        $value = '';
-                    }
-
-                    if (!empty($this->config->{'field_updatelocal_' . $key})) {
-                        // Only update if it's changed.
-                        if ($user->{$key} != $value) {
-                            $newuser->$key = $value;
-                        }
-                    }
-                }
-                user_update_user($newuser, false, $triggerevent);
-            }
-        } else {
-            return false;
-        }
-        return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
-    }
-
     /**
      * Bulk insert in SQL's temp table
      */
@@ -1201,6 +1128,14 @@ class auth_plugin_ldap extends auth_plugin_base {
             return false;
         }
 
+        // Load old custom fields.
+        $olduserprofilefields = (array) profile_user_record($olduser->id, false);
+
+        $fields = array();
+        foreach (profile_get_custom_fields(false) as $field) {
+            $fields[$field->shortname] = $field;
+        }
+
         $success = true;
         $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
         if ($user_info_result) {
@@ -1219,19 +1154,24 @@ class auth_plugin_ldap extends auth_plugin_base {
             $user_entry = $user_entry[0];
 
             foreach ($attrmap as $key => $ldapkeys) {
-                $profilefield = '';
-                // Only process if the moodle field ($key) has changed and we
-                // are set to update LDAP with it
-                $customprofilefield = 'profile_field_' . $key;
-                if (isset($olduser->$key) and isset($newuser->$key)
-                    and ($olduser->$key !== $newuser->$key)) {
-                    $profilefield = $key;
-                } else if (isset($olduser->$customprofilefield) && isset($newuser->$customprofilefield)
-                    && $olduser->$customprofilefield !== $newuser->$customprofilefield) {
-                    $profilefield = $customprofilefield;
+                if (preg_match('/^profile_field_(.*)$/', $key, $match)) {
+                    // Custom field.
+                    $fieldname = $match[1];
+                    if (isset($fields[$fieldname])) {
+                        $class = 'profile_field_' . $fields[$fieldname]->datatype;
+                        $formfield = new $class($fields[$fieldname]->id, $olduser->id);
+                        $oldvalue = isset($olduserprofilefields[$fieldname]) ? $olduserprofilefields[$fieldname] : null;
+                    } else {
+                        $oldvalue = null;
+                    }
+                    $newvalue = $formfield->edit_save_data_preprocess($newuser->{$formfield->inputname}, new stdClass);
+                } else {
+                    // Standard field.
+                    $oldvalue = isset($olduser->$key) ? $olduser->$key : null;
+                    $newvalue = isset($newuser->$key) ? $newuser->$key : null;
                 }
 
-                if (!empty($profilefield) && !empty($this->config->{'field_updateremote_' . $key})) {
+                if ($newvalue !== null and $newvalue !== $oldvalue and !empty($this->config->{'field_updateremote_' . $key})) {
                     // For ldap values that could be in more than one
                     // ldap key, we will do our best to match
                     // where they came from
@@ -1244,9 +1184,9 @@ class auth_plugin_ldap extends auth_plugin_base {
                         $ambiguous = false;
                     }
 
-                    $nuvalue = core_text::convert($newuser->$profilefield, 'utf-8', $this->config->ldapencoding);
+                    $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding);
                     empty($nuvalue) ? $nuvalue = array() : $nuvalue;
-                    $ouvalue = core_text::convert($olduser->$profilefield, 'utf-8', $this->config->ldapencoding);
+                    $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding);
 
                     foreach ($ldapkeys as $ldapkey) {
                         $ldapkey   = $ldapkey;
@@ -1684,7 +1624,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $sesskey = sesskey();
                     redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey);
                 } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
-                    redirect($CFG-wwwroot.'/login/index.php?authldap_skipntlmsso=1');
+                    redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1');
                 }
             }
             redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php');
@@ -2170,4 +2110,31 @@ class auth_plugin_ldap extends auth_plugin_base {
             echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
         }
     }
-} // End of the class
+
+    /**
+     * Get the list of profile fields.
+     *
+     * @param   bool    $fetchall   Fetch all, not just those for update.
+     * @return  array
+     */
+    protected function get_profile_keys($fetchall = false) {
+        $keys = array_keys(get_object_vars($this->config));
+        $updatekeys = [];
+        foreach ($keys as $key) {
+            if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) {
+                // If we have a field to update it from and it must be updated 'onlogin' we update it on cron.
+                if (!empty($this->config->{'field_map_'.$match[1]})) {
+                    if ($fetchall || $this->config->{$match[0]} === 'onlogin') {
+                        array_push($updatekeys, $match[1]); // the actual key name
+                    }
+                }
+            }
+        }
+
+        if ($this->config->suspended_attribute && $this->config->sync_suspended) {
+            $updatekeys[] = 'suspended';
+        }
+
+        return $updatekeys;
+    }
+}
index 843e1b0..fb61dd1 100644 (file)
@@ -41,12 +41,12 @@ $string['auth_ldapdescription'] = 'This method provides authentication against a
                                   entry in its database. This module can read user attributes from LDAP and prefill
                                   wanted fields in Moodle.  For following logins only the username and
                                   password are checked.';
-$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiration time directly from the LDAP server';
-$string['auth_ldap_expiration_key'] = 'Expiration';
-$string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiration warning is issued.';
-$string['auth_ldap_expiration_warning_key'] = 'Expiration warning';
-$string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiration time.';
-$string['auth_ldap_expireattr_key'] = 'Expiration attribute';
+$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiry time directly from the LDAP server.';
+$string['auth_ldap_expiration_key'] = 'Expiry';
+$string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiry warning is issued.';
+$string['auth_ldap_expiration_warning_key'] = 'Expiry warning';
+$string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.';
+$string['auth_ldap_expireattr_key'] = 'Expiry attribute';
 $string['auth_ldapextrafields'] = 'These fields are optional.  You can choose to pre-fill some Moodle user fields with information from the <b>LDAP fields</b> that you specify here. <p>If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>';
 $string['auth_ldap_graceattr_desc'] = 'Optional: Overrides  gracelogin attribute';
 $string['auth_ldap_gracelogin_key'] = 'Grace login attribute';
@@ -75,7 +75,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 expiry settings';
 $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
 $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
 $string['auth_ldap_rolecontext'] = '{$a->localname} context';
@@ -91,7 +91,7 @@ $string['auth_ldap_suspended_attribute'] = 'Optional: When provided this attribu
 $string['auth_ldap_suspended_attribute_key'] = 'Suspended attribute';
 $string['auth_ldap_user_exists'] = 'LDAP username already exists.';
 $string['auth_ldap_user_settings'] = 'User lookup settings';
-$string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiration, grace logins and user creation will work.';
+$string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiry, grace logins and user creation will work.';
 $string['auth_ldap_user_type_key'] = 'User type';
 $string['auth_ldap_usertypeundefined'] = 'config.user_type not defined or function ldap_expirationtime2unix does not support selected type!';
 $string['auth_ldap_usertypeundefined2'] = 'config.user_type not defined or function ldap_unixi2expirationtime does not support selected type!';
@@ -149,7 +149,7 @@ $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
-$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiration time. Error code: {$a->errno}; Error string: {$a->errstring}';
+$string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}';
 $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}';
index 1f269f7..b3b22f5 100644 (file)
@@ -382,7 +382,7 @@ class auth_plugin_mnet extends auth_plugin_base {
             // with info so that the IDP can maintain mnetservice_enrol_enrolments
             $mnetrequest->add_param($remoteuser->username);
             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
-            $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
+            $courses = enrol_get_users_courses($localuser->id, false, $fields);
             if (is_array($courses) && !empty($courses)) {
                 // Second request to do the JOINs that we'd have done
                 // inside enrol_get_users_courses() if we had been allowed
index fff9fdd..70c49d5 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /auth/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.5 ===
+
+* The auth_db and auth_ldap plugins' implementations of update_user_record() have been removed and both now
+  call the new implementation added in the base class.
+
 === 3.3 ===
 
 * Authentication plugins have been migrated to use the admin settings API.  Plugins should use a settings.php file to
index bc5bb5a..7bb6da7 100644 (file)
@@ -99,7 +99,7 @@ class cc11_resource extends entities11 {
                                 $link = 'http://invalidurldetected/';
                             }
                         } else {
-                            $link = $rawlink;
+                            $link = htmlspecialchars(trim($rawlink), ENT_COMPAT, 'UTF-8', false);
                         }
                     }
                 }
index 2121c02..6bcf826 100644 (file)
@@ -200,7 +200,7 @@ class restore_course_task extends restore_task {
         $startdatedefaultvalue = $this->get_info()->original_course_startdate;
         $startdate = new restore_course_defaultcustom_setting('course_startdate', base_setting::IS_INTEGER, $startdatedefaultvalue);
         $startdate->set_ui(new backup_setting_ui_defaultcustom($startdate, get_string('setting_course_startdate', 'backup'),
-            ['customvalue' => $startdatedefaultvalue, 'defaultvalue' => $course->startdate, 'type' => 'date_selector']));
+            ['customvalue' => $startdatedefaultvalue, 'defaultvalue' => $course->startdate, 'type' => 'date_time_selector']));
         $this->add_setting($startdate);
 
         $keep_enrols = new restore_course_generic_setting('keep_roles_and_enrolments', base_setting::IS_BOOLEAN, false);
index 3b45eea..0589755 100644 (file)
@@ -596,7 +596,7 @@ class core_backup_moodle2_testcase extends advanced_testcase {
             assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext);
         }
 
-        allow_assign($roleidcat, $studentrole->id);
+        core_role_set_assign_allowed($roleidcat, $studentrole->id);
         role_assign($roleidcat, $user->id, $categorycontext);
         accesslib_clear_all_caches_for_unit_testing();
 
index 18e77ef..4890b6c 100644 (file)
@@ -84,10 +84,6 @@ abstract class restore_step extends base_step {
             // Original course has not startdate or setting doesn't exist, offset = 0.
             $cache[$this->get_restoreid()] = 0;
 
-        } else if (abs($setting - $original) < 24 * 60 * 60) {
-            // Less than 24h of difference, offset = 0 (this avoids some problems with timezones).
-            $cache[$this->get_restoreid()] = 0;
-
         } else {
             // Arrived here, let's calculate the real offset.
             $cache[$this->get_restoreid()] = $setting - $original;
index f586d40..8fa0654 100644 (file)
@@ -122,6 +122,8 @@ class backup_confirmation_form extends backup_moodleform {
         if (!array_key_exists('setting_root_filename', $errors)) {
             if (trim($data['setting_root_filename']) == '') {
                 $errors['setting_root_filename'] = get_string('errorfilenamerequired', 'backup');
+            } else if (strlen(trim($data['setting_root_filename'])) > 255) {
+                $errors['setting_root_filename'] = get_string('errorfilenametoolong', 'backup');
             } else if (!preg_match('#\.mbz$#i', $data['setting_root_filename'])) {
                 $errors['setting_root_filename'] = get_string('errorfilenamemustbezip', 'backup');
             }
index e6dd196..2282907 100644 (file)
@@ -27,18 +27,22 @@ function check_site_access() {
     var callback = {
             method: "GET",
             on: {
-                success: function(id, o, args) {
-                            var data = Y.JSON.parse(o.responseText);
-                            if (data.code == 'http-unreachable') {
-                                add.setHTML(data.response);
-                                add.removeClass('hide');
-                            }
-                        },
-                failure: function(o) { }
+                success: function(id, o) {
+                    var data = Y.JSON.parse(o.responseText);
+                    if (data.code == 'http-unreachable') {
+                        add.setHTML(data.response);
+                        add.removeClass('hide');
+                    }
+                    M.util.js_complete('badge/backpack::check_site_access');
+                },
+                failure: function() {
+                    M.util.js_complete('badge/backpack::check_site_access');
+                }
             }
         };
 
     Y.use('io-base', function(Y) {
+        M.util.js_pending('badge/backpack::check_site_access');
         Y.io('ajax.php', callback);
     });
 
index 83fde27..ff1e613 100644 (file)
@@ -65,6 +65,7 @@ class award_criteria_manual extends award_criteria {
         $none = true;
 
         $roles = get_roles_with_capability('moodle/badges:awardbadge', CAP_ALLOW, $PAGE->context);
+        $visibleroles = get_viewable_roles($PAGE->context);
         $roleids = array_map(function($o) {
             return $o->id;
         }, $roles);
@@ -89,6 +90,9 @@ class award_criteria_manual extends award_criteria {
             $mform->addElement('header', 'first_header', $this->get_title());
             $mform->addHelpButton('first_header', 'criteria_' . $this->criteriatype, 'badges');
             foreach ($roleids as $rid) {
+                if (!key_exists($rid, $visibleroles)) {
+                    continue;
+                }
                 $checked = false;
                 if (in_array($rid, $existing)) {
                     $checked = true;
diff --git a/badges/tests/behat/role_visibility.feature b/badges/tests/behat/role_visibility.feature
new file mode 100644 (file)
index 0000000..bb3a49d
--- /dev/null
@@ -0,0 +1,51 @@
+@core @core_badges
+Feature: Test role visibility for the badge administration page
+  In order to control access
+  As an admin
+  I need to control which roles can see each other
+
+  Background: Add a bunch of users
+    Given  the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | manager1 | Manager   | 1        | manager1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | teacher1 | C1     | editingteacher |
+      | manager1 | C1     | manager        |
+
+  @javascript @_file_upload
+  Scenario: Check the default roles are visible
+    Given I log in as "manager1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    Then I should see "Teacher"
+    And I should see "Manager"
+
+  @javascript @_file_upload
+  Scenario: Check hidden roles are not visible
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the following fields to these values:
+      | Add badge criteria | Manual issue by role |
+    Then I should see "Teacher"
+    And I should not see "Manager"
index 6e31b70..00547c6 100644 (file)
@@ -51,7 +51,7 @@ class block_calendar_month extends block_base {
         $courseid = $this->page->course->id;
         $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null;
         $calendar = \calendar_information::create(time(), $courseid, $categoryid);
-        list($data, $template) = calendar_get_view($calendar, 'mini', isloggedin());
+        list($data, $template) = calendar_get_view($calendar, 'mini', isloggedin(), isloggedin());
 
         $renderer = $this->page->get_renderer('core_calendar');
         $this->content->text .= $renderer->render_from_template($template, $data);
index 36003f7..10cbf23 100644 (file)
@@ -57,14 +57,7 @@ class block_course_list extends block_list {
 
         if (empty($CFG->disablemycourses) and isloggedin() and !isguestuser() and
           !(has_capability('moodle/course:update', context_system::instance()) and $adminseesall)) {    // Just print My Courses
-            // As this is producing navigation sort order should default to $CFG->navsortmycoursessort instead
-            // of using the default.
-            if (!empty($CFG->navsortmycoursessort)) {
-                $sortorder = 'visible DESC, ' . $CFG->navsortmycoursessort . ' ASC';
-            } else {
-                $sortorder = 'visible DESC, sortorder ASC';
-            }
-            if ($courses = enrol_get_my_courses(NULL, $sortorder)) {
+            if ($courses = enrol_get_my_courses()) {
                 foreach ($courses as $course) {
                     $coursecontext = context_course::instance($course->id);
                     $linkcss = $course->visible ? "" : " class=\"dimmed\" ";
index 2435f54..2850637 100644 (file)
@@ -63,7 +63,7 @@ class main implements renderable, templatable {
     public function export_for_template(renderer_base $output) {
         global $USER;
 
-        $courses = enrol_get_my_courses('*', 'fullname ASC');
+        $courses = enrol_get_my_courses('*');
         $coursesprogress = [];
 
         foreach ($courses as $course) {
index 1a41395..08521a3 100644 (file)
@@ -74,15 +74,25 @@ class block_online_users extends block_base {
 
         //Calculate minutes
         $minutes  = floor($timetoshowusers/60);
+        $periodminutes = get_string('periodnminutes', 'block_online_users', $minutes);
+
+        // Count users.
+        $usercount = $onlineusers->count_users();
+        if ($usercount === 0) {
+            $usercount = get_string('nouser', 'block_online_users');
+        } else if ($usercount === 1) {
+            $usercount = get_string('numuser', 'block_online_users', $usercount);
+        } else {
+            $usercount = get_string('numusers', 'block_online_users', $usercount);
+        }
+
+        $this->content->text = '<div class="info">'.$usercount.' ('.$periodminutes.')</div>';
 
         // Verify if we can see the list of users, if not just print number of users
         if (!has_capability('block/online_users:viewlist', $this->page->context)) {
-            if (!$usercount = $onlineusers->count_users()) {
-                $usercount = get_string("none");
-            }
-            $this->content->text = "<div class=\"info\">".get_string("periodnminutes","block_online_users",$minutes).": $usercount</div>";
             return $this->content;
         }
+
         $userlimit = 50; // We'll just take the most recent 50 maximum.
         if ($users = $onlineusers->get_users($userlimit)) {
             foreach ($users as $user) {
@@ -92,11 +102,6 @@ class block_online_users extends block_base {
             $users = array();
         }
 
-        $usercount = $onlineusers->count_users();
-        $usercount = ": $usercount";
-
-        $this->content->text = "<div class=\"info\">(".get_string("periodnminutes","block_online_users",$minutes)."$usercount)</div>";
-
         //Now, we have in users, the list of users to show
         //Because they are online
         if (!empty($users)) {
@@ -133,8 +138,6 @@ class block_online_users extends block_base {
                 $this->content->text .= "</li>\n";
             }
             $this->content->text .= '</ul><div class="clearer"><!-- --></div>';
-        } else {
-            $this->content->text .= "<div class=\"info\">".get_string("none")."</div>";
         }
 
         return $this->content;
index 5d641ee..e5a79e3 100644 (file)
@@ -24,6 +24,9 @@
  */
 
 $string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
+$string['nouser'] = 'No online users';
+$string['numuser'] = '{$a} online user';
+$string['numusers'] = '{$a} online users';
 $string['online_users:addinstance'] = 'Add a new online users block';
 $string['online_users:myaddinstance'] = 'Add a new online users block to Dashboard';
 $string['online_users:viewlist'] = 'View list of online users';
index 22591be..e86e4d8 100644 (file)
@@ -24,6 +24,7 @@ Feature: The online users block allow you to see who is currently online
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Online users" block
     Then I should see "Teacher 1" in the "Online users" "block"
+    And I should see "1 online user" in the "Online users" "block"
 
   Scenario: Add the online users on course page and see other logged in users
     Given I log in as "teacher1"
@@ -37,3 +38,4 @@ Feature: The online users block allow you to see who is currently online
     Then I should see "Teacher 1" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
     And I should not see "Student 2" in the "Online users" "block"
+    And I should see "2 online users" in the "Online users" "block"
index 7f88d76..34207c5 100644 (file)
@@ -14,6 +14,7 @@ Feature: The online users block allow you to see who is currently online on dash
   Scenario: View the online users block on the dashboard and see myself
     Given I log in as "teacher1"
     Then I should see "Teacher 1" in the "Online users" "block"
+    And I should see "1 online user" in the "Online users" "block"
 
   Scenario: View the online users block on the dashboard and see other logged in users
     Given I log in as "student2"
@@ -24,3 +25,4 @@ Feature: The online users block allow you to see who is currently online on dash
     Then I should see "Teacher 1" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
     And I should see "Student 2" in the "Online users" "block"
+    And I should see "3 online users" in the "Online users" "block"
index 5909b1c..6237d3d 100644 (file)
@@ -16,6 +16,7 @@ Feature: The online users block allow you to see who is currently online on fron
     And I navigate to "Turn editing on" node in "Front page settings"
     When I add the "Online users" block
     Then I should see "Admin User" in the "Online users" "block"
+    And I should see "1 online user" in the "Online users" "block"
 
   Scenario: View the online users block on the front page as a logged in user
     Given I log in as "admin"
@@ -30,6 +31,7 @@ Feature: The online users block allow you to see who is currently online on fron
     Then I should see "Admin User" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
     And I should see "Student 2" in the "Online users" "block"
+    And I should see "3 online users" in the "Online users" "block"
 
   Scenario: View the online users block on the front page as a guest
     Given I log in as "admin"
@@ -46,3 +48,4 @@ Feature: The online users block allow you to see who is currently online on fron
     Then I should see "Admin User" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
     And I should see "Student 2" in the "Online users" "block"
+    And I should see "3 online users" in the "Online users" "block"
index b21bf11..1d700ba 100644 (file)
@@ -43,7 +43,7 @@
         "datepublished": "12 January 2016, 9:12 pm"
     }
 }}
-<li>
+<li class="p-y-1">
     {{$title}}
         <div class="link">
             <a href="{{{link}}}" onclick='this.target="_blank"'>{{title}}</a>
@@ -52,6 +52,9 @@
 
     {{$content}}
         {{#description}}
+            <div class="date text-muted muted m-b-1">
+                <small>{{{datepublished}}}</small>
+            </div>
             <div class="description">
                 {{{description}}}
             </div>
index e483abe..f263ddf 100644 (file)
@@ -45,7 +45,7 @@ class block_tags extends block_base {
         if (empty($this->config->title)) {
             $this->title = get_string('pluginname', 'block_tags');
         } else {
-            $this->title = $this->config->title;
+            $this->title = format_string($this->config->title, true, ['context' => $this->context]);
         }
     }
 
index ffa3f2e..6a78673 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index ad5f0df..1c3eecf 100644 (file)
Binary files a/calendar/amd/build/calendar_mini.min.js and b/calendar/amd/build/calendar_mini.min.js differ
index 6102199..ecec1a0 100644 (file)
@@ -183,35 +183,38 @@ define([
                 .fail(Notification.exception);
         });
 
-        var eventFormPromise = CalendarCrud.registerEventFormModal(root);
+        var eventFormPromise = CalendarCrud.registerEventFormModal(root),
+            contextId = $(SELECTORS.CALENDAR_MONTH_WRAPPER).data('context-id');
         registerCalendarEventListeners(root, eventFormPromise);
 
-        // Bind click events to calendar days.
-        root.on('click', SELECTORS.DAY, function(e) {
-
-            var target = $(e.target);
-
-            if (!target.is(SELECTORS.VIEW_DAY_LINK)) {
-                var startTime = $(this).attr('data-new-event-timestamp');
-                eventFormPromise.then(function(modal) {
-                    var wrapper = target.closest(CalendarSelectors.wrapper);
-                    modal.setCourseId(wrapper.data('courseid'));
-
-                    var categoryId = wrapper.data('categoryid');
-                    if (typeof categoryId !== 'undefined') {
-                        modal.setCategoryId(categoryId);
-                    }
-
-                    modal.setContextId(wrapper.data('contextId'));
-                    modal.setStartTime(startTime);
-                    modal.show();
-                    return;
-                })
-                .fail(Notification.exception);
-
-                e.preventDefault();
-            }
-        });
+        if (contextId) {
+            // Bind click events to calendar days.
+            root.on('click', SELECTORS.DAY, function (e) {
+
+                var target = $(e.target);
+
+                if (!target.is(SELECTORS.VIEW_DAY_LINK)) {
+                    var startTime = $(this).attr('data-new-event-timestamp');
+                    eventFormPromise.then(function (modal) {
+                        var wrapper = target.closest(CalendarSelectors.wrapper);
+                        modal.setCourseId(wrapper.data('courseid'));
+
+                        var categoryId = wrapper.data('categoryid');
+                        if (typeof categoryId !== 'undefined') {
+                            modal.setCategoryId(categoryId);
+                        }
+
+                        modal.setContextId(wrapper.data('contextId'));
+                        modal.setStartTime(startTime);
+                        modal.show();
+                        return;
+                    })
+                    .fail(Notification.exception);
+
+                    e.preventDefault();
+                }
+            });
+        }
     };
 
     return {
index e8031ee..30f0385 100644 (file)
@@ -99,12 +99,19 @@ function(
     };
 
     return {
-        init: function(root) {
+        init: function(root, loadOnInit) {
             root = $(root);
 
             CalendarViewManager.init(root);
             registerEventListeners(root);
             registerCalendarEventListeners(root);
+
+            if (loadOnInit) {
+                // The calendar hasn't yet loaded it's events so we
+                // should load them as soon as we've initialised.
+                CalendarViewManager.reloadCurrentMonth(root);
+            }
+
         }
     };
 });
index b735d36..08acf56 100644 (file)
@@ -59,6 +59,11 @@ class month_exporter extends exporter {
      */
     protected $includenavigation = true;
 
+    /**
+     * @var bool $initialeventsloaded Whether the events have been loaded for this month.
+     */
+    protected $initialeventsloaded = true;
+
     /**
      * Constructor for month_exporter.
      *
@@ -139,6 +144,12 @@ class month_exporter extends exporter {
                 'type' => PARAM_BOOL,
                 'default' => true,
             ],
+            // Tracks whether the first set of events have been loaded and provided
+            // to the exporter.
+            'initialeventsloaded' => [
+                'type' => PARAM_BOOL,
+                'default' => true,
+            ],
             'previousperiod' => [
                 'type' => date_exporter::read_properties_definition(),
             ],
@@ -210,6 +221,7 @@ class month_exporter extends exporter {
             'larrow' => $output->larrow(),
             'rarrow' => $output->rarrow(),
             'includenavigation' => $this->includenavigation,
+            'initialeventsloaded' => $this->initialeventsloaded,
         ];
 
         if ($context = $this->get_default_add_context()) {
@@ -380,6 +392,19 @@ class month_exporter extends exporter {
         return $this;
     }
 
+    /**
+     * Set whether the initial events have already been loaded and
+     * provided to the exporter.
+     *
+     * @param   bool    $loaded
+     * @return  $this
+     */
+    public function set_initialeventsloaded(bool $loaded) {
+        $this->initialeventsloaded = $loaded;
+
+        return $this;
+    }
+
     /**
      * Get the default context for use when adding a new event.
      *
index 049f13d..7592536 100644 (file)
@@ -503,7 +503,7 @@ class core_calendar_external extends external_api {
             $params['aftereventid'] = null;
         }
 
-        $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, [$courseid]);
+        $courses = enrol_get_my_courses('*', null, 0, [$courseid]);
         $courses = array_values($courses);
 
         if (empty($courses)) {
@@ -588,7 +588,7 @@ class core_calendar_external extends external_api {
         }
 
         $renderer = $PAGE->get_renderer('core_calendar');
-        $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, $params['courseids']);
+        $courses = enrol_get_my_courses('*', null, 0, $params['courseids']);
         $courses = array_values($courses);
 
         if (empty($courses)) {
index 0a8408f..423f605 100644 (file)
@@ -1819,6 +1819,13 @@ function calendar_time_representation($time) {
         $timeformat = get_config(null, 'calendar_site_timeformat');
     }
 
+    // Allow language customization of selected time format.
+    if ($timeformat === CALENDAR_TF_12) {
+        $timeformat = get_string('strftimetime12', 'langconfig');
+    } else if ($timeformat === CALENDAR_TF_24) {
+        $timeformat = get_string('strftimetime24', 'langconfig');
+    }
+
     return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat);
 }
 
@@ -3361,9 +3368,10 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
  * @param   \calendar_information $calendar The calendar being represented
  * @param   string  $view The type of calendar to have displayed
  * @param   bool    $includenavigation Whether to include navigation
+ * @param   bool    $skipevents Whether to load the events or not
  * @return  array[array, string]
  */
-function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true) {
+function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true, bool $skipevents = false) {
     global $PAGE, $CFG;
 
     $renderer = $PAGE->get_renderer('core_calendar');
@@ -3436,36 +3444,40 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
         return $param;
     }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
 
-    $events = \core_calendar\local\api::get_events(
-        $tstart,
-        $tend,
-        null,
-        null,
-        null,
-        null,
-        $eventlimit,
-        null,
-        $userparam,
-        $groupparam,
-        $courseparam,
-        $categoryparam,
-        true,
-        true,
-        function ($event) {
-            if ($proxy = $event->get_course_module()) {
-                $cminfo = $proxy->get_proxied_instance();
-                return $cminfo->uservisible;
-            }
+    if ($skipevents) {
+        $events = [];
+    } else {
+        $events = \core_calendar\local\api::get_events(
+            $tstart,
+            $tend,
+            null,
+            null,
+            null,
+            null,
+            $eventlimit,
+            null,
+            $userparam,
+            $groupparam,
+            $courseparam,
+            $categoryparam,
+            true,
+            true,
+            function ($event) {
+                if ($proxy = $event->get_course_module()) {
+                    $cminfo = $proxy->get_proxied_instance();
+                    return $cminfo->uservisible;
+                }
 
-            if ($proxy = $event->get_category()) {
-                $category = $proxy->get_proxied_instance();
+                if ($proxy = $event->get_category()) {
+                    $category = $proxy->get_proxied_instance();
 
-                return $category->is_uservisible();
-            }
+                    return $category->is_uservisible();
+                }
 
-            return true;
-        }
-    );
+                return true;
+            }
+        );
+    }
 
     $related = [
         'events' => $events,
@@ -3477,6 +3489,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
     if ($view == "month" || $view == "mini" || $view == "minithree") {
         $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
         $month->set_includenavigation($includenavigation);
+        $month->set_initialeventsloaded(!$skipevents);
         $data = $month->export($renderer);
     } else if ($view == "day") {
         $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
index e8dadbe..610ba2d 100644 (file)
@@ -81,15 +81,15 @@ class core_calendar_renderer extends plugin_renderer_base {
 
         // Previous.
         $calendar->set_time($prev);
-        list($previousmonth, ) = calendar_get_view($calendar, 'minithree', false);
+        list($previousmonth, ) = calendar_get_view($calendar, 'minithree', false, true);
 
         // Current month.
         $calendar->set_time($current);
-        list($currentmonth, ) = calendar_get_view($calendar, 'minithree', false);
+        list($currentmonth, ) = calendar_get_view($calendar, 'minithree', false, true);
 
         // Next month.
         $calendar->set_time($next);
-        list($nextmonth, ) = calendar_get_view($calendar, 'minithree', false);
+        list($nextmonth, ) = calendar_get_view($calendar, 'minithree', false, true);
 
         // Reset the time back.
         $calendar->set_time($current);
index 5b91ac6..e7929f8 100644 (file)
@@ -40,6 +40,6 @@
 </div>
 {{#js}}
 require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) {
-    CalendarMini.init($("#calendar-month-{{date.year}}-{{date.month}}-{{uniqid}}"));
+    CalendarMini.init($("#calendar-month-{{date.year}}-{{date.month}}-{{uniqid}}"), !{{initialeventsloaded}});
 });
 {{/js}}
index 9abff8e..f3cdfeb 100644 (file)
                     <td class="dayblank">&nbsp;</td>
                 {{/prepadding}}
                 {{#days}}
-                    <td class="clickable day text-sm-center text-md-left{{!
+                    <td class="day text-sm-center text-md-left{{!
                             }}{{#istoday}} today{{/istoday}}{{!
                             }}{{#isweekend}} weekend{{/isweekend}}{{!
                             }}{{#durationevents.0}} duration{{/durationevents.0}}{{!
                             }}{{#durationevents}} duration_{{.}}{{/durationevents}}{{!
+                            }}{{#defaulteventcontext}} clickable{{/defaulteventcontext}}{{!
                         }}"
                         data-day-timestamp="{{timestamp}}"
                         data-drop-zone="month-view-day"
index 081738e..6c6e753 100644 (file)
@@ -728,4 +728,33 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertCount(1, $courses);
 
     }
+
+    /**
+     * Confirm that the skip events flag causes the calendar_get_view function
+     * to avoid querying for the calendar events.
+     */
+    public function test_calendar_get_view_skip_events() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $skipnavigation = true;
+        $skipevents = true;
+        $event = create_event([
+            'eventtype' => 'user',
+            'userid' => $user->id
+        ]);
+
+        $this->setUser($user);
+        $calendar = \calendar_information::create(time() - 10, SITEID, null);
+
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
+        $this->assertEmpty($data->events);
+
+        $skipevents = false;
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
+
+        $this->assertEquals($event->id, $data->events[0]->id);
+    }
 }
index 1024e57..0c35f41 100644 (file)
@@ -841,10 +841,10 @@ class external extends external_api {
         $validcolumns = array('id', 'shortname', 'description', 'sortorder', 'idnumber',
             'parentid', 'competencyframeworkid');
         foreach ($params['filters'] as $filter) {
-            if (!in_array($filter->column, $validcolumns)) {
+            if (!in_array($filter['column'], $validcolumns)) {
                 throw new invalid_parameter_exception('Filter column was invalid');
             }
-            $safefilters[$filter->column] = $filter->value;
+            $safefilters[$filter['column']] = $filter['value'];
         }
 
         $context = null;
index f0b50c8..1a26d38 100644 (file)
@@ -2801,4 +2801,43 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => true));
     }
 
+    /**
+     * Test that we can list competencies with a filter.
+     *
+     * @return void
+     */
+    public function test_list_competencies_with_filter() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $dg = $this->getDataGenerator();
+        $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
+
+        $framework = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+        $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+        $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+        $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+        $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+
+        // Test if removing competency from plan don't create sortorder holes.
+        $filters = [];
+        $sort = 'id';
+        $order = 'ASC';
+        $skip = 0;
+        $limit = 0;
+        $result = external::list_competencies($filters, $sort, $order, $skip, $limit);
+        $this->assertCount(5, $result);
+
+        $result = external::list_competencies($filters, $sort, $order, 2, $limit);
+        $this->assertCount(3, $result);
+        $result = external::list_competencies($filters, $sort, $order, 2, 2);
+        $this->assertCount(2, $result);
+
+        $filter = $result[0]->shortname;
+        $filters[0] = ['column' => 'shortname', 'value' => $filter];
+        $result = external::list_competencies($filters, $sort, $order, $skip, $limit);
+        $this->assertCount(1, $result);
+        $this->assertEquals($filter, $result[0]->shortname);
+    }
+
 }
index 676d712..505c789 100644 (file)
@@ -220,7 +220,7 @@ abstract class core_completion_edit_base_form extends moodleform {
         }
 
         // Completion expected at particular date? (For progress tracking).
-        $mform->addElement('date_selector', 'completionexpected',
+        $mform->addElement('date_time_selector', 'completionexpected',
             get_string('completionexpected', 'completion'), ['optional' => true]);
         $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
         $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
index 7c60ee0..3238f05 100644 (file)
@@ -410,12 +410,12 @@ M.course_dndupload = {
      * @return false to prevent the event from continuing to be processed
      */
     drop: function(e) {
+        this.hide_preview_element();
+
         if (!(type = this.check_drag(e))) {
             return false;
         }
 
-        this.hide_preview_element();
-
         // Work out the number of the section we are on (from its id)
         var section = this.get_section(e.currentTarget);
         var sectionnumber = this.get_section_number(section);
@@ -792,16 +792,42 @@ M.course_dndupload = {
 
         // Prepare the data to send
         var formData = new FormData();
-        formData.append('repo_upload_file', file);
+        try {
+            formData.append('repo_upload_file', file);
+        } catch (e) {
+            // Edge throws an error at this point if we try to upload a folder.
+            resel.parent.removeChild(resel.li);
+            new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
+            return;
+        }
         formData.append('sesskey', M.cfg.sesskey);
         formData.append('course', this.courseid);
         formData.append('section', sectionnumber);
         formData.append('module', module);
         formData.append('type', 'Files');
 
-        // Send the AJAX call
-        xhr.open("POST", this.url, true);
-        xhr.send(formData);
+        // Try reading the file to check it is not a folder, before sending it to the server.
+        var reader = new FileReader();
+        reader.onload = function() {
+            // File was read OK - send it to the server.
+            xhr.open("POST", self.url, true);
+            xhr.send(formData);
+        };
+        reader.onerror = function() {
+            // Unable to read the file (it is probably a folder) - display an error message.
+            resel.parent.removeChild(resel.li);
+            new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
+        };
+        if (file.size > 0) {
+            // If this is a non-empty file, try reading the first few bytes.
+            // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
+            reader.readAsText(file.slice(0, 5));
+        } else {
+            // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
+            // instead of reader.onerror().
+            // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
+            reader.readAsText(file);
+        }
     },
 
     /**
index 73ecfc8..1e12105 100644 (file)
@@ -64,6 +64,7 @@ function dndupload_add_to_course($course, $modnames) {
             array('namedfiletoolarge', 'moodle'),
             array('actionchoice', 'moodle'),
             array('servererror', 'moodle'),
+            array('filereaderror', 'moodle'),
             array('upload', 'moodle'),
             array('cancel', 'moodle')
         ),
index 4c03e2d..4ffe172 100644 (file)
@@ -88,7 +88,7 @@ class course_edit_form extends moodleform {
             }
         } else {
             if (has_capability('moodle/course:changecategory', $coursecontext)) {
-                $displaylist = coursecat::make_categories_list('moodle/course:create');
+                $displaylist = coursecat::make_categories_list('moodle/course:changecategory');
                 if (!isset($displaylist[$course->category])) {
                     //always keep current
                     $displaylist[$course->category] = coursecat::get($course->category, MUST_EXIST, true)->get_formatted_name();
@@ -120,12 +120,13 @@ class course_edit_form extends moodleform {
                 $mform->setConstant('visible', $courseconfig->visible);
             }
         }
-
-        $mform->addElement('date_selector', 'startdate', get_string('startdate'));
+        $mform->addElement('date_time_selector', 'startdate', get_string('startdate'));
         $mform->addHelpButton('startdate', 'startdate');
-        $mform->setDefault('startdate', time() + 3600 * 24);
+        $date = (new DateTime())->setTimestamp(usergetmidnight(time()));
+        $date->modify('+1 day');
+        $mform->setDefault('startdate', $date->getTimestamp());
 
-        $mform->addElement('date_selector', 'enddate', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'enddate', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('enddate', 'enddate');
 
         $mform->addElement('text','idnumber', get_string('idnumbercourse'),'maxlength="100"  size="10"');
index 06cc2b1..024f159 100644 (file)
@@ -710,7 +710,8 @@ abstract class moodleform_mod extends moodleform {
             }
 
             // Completion expected at particular date? (For progress tracking)
-            $mform->addElement('date_selector', 'completionexpected', get_string('completionexpected', 'completion'), array('optional'=>true));
+            $mform->addElement('date_time_selector', 'completionexpected', get_string('completionexpected', 'completion'),
+                    array('optional' => true));
             $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
             $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
         }
index 64b2575..a086ba6 100644 (file)
@@ -647,7 +647,7 @@ class core_course_renderer extends plugin_renderer_base {
             }
         } else {
             $linkclasses .= ' dimmed';
-            $textclasses .= ' dimmed_text';
+            $textclasses .= ' dimmed dimmed_text';
         }
         return array($linkclasses, $textclasses);
     }
@@ -1988,13 +1988,7 @@ class core_course_renderer extends plugin_renderer_base {
         }
 
         $output = '';
-        if (!empty($CFG->navsortmycoursessort)) {
-            // sort courses the same as in navigation menu
-            $sortorder = 'visible DESC,'. $CFG->navsortmycoursessort.' ASC';
-        } else {
-            $sortorder = 'visible DESC,sortorder ASC';
-        }
-        $courses  = enrol_get_my_courses('summary, summaryformat', $sortorder);
+        $courses  = enrol_get_my_courses('summary, summaryformat');
         $rhosts   = array();
         $rcourses = array();
         if (!empty($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode==='strict') {
index 6314a16..1accadd 100644 (file)
@@ -41,9 +41,9 @@ class course_reset_form extends moodleform {
 
         $mform->addElement('header', 'generalheader', get_string('general'));
 
-        $mform->addElement('date_selector', 'reset_start_date', get_string('startdate'), array('optional'=>true));
+        $mform->addElement('date_time_selector', 'reset_start_date', get_string('startdate'), array('optional' => true));
         $mform->addHelpButton('reset_start_date', 'startdate');
-        $mform->addElement('date_selector', 'reset_end_date', get_string('enddate'), array('optional' => true));
+        $mform->addElement('date_time_selector', 'reset_end_date', get_string('enddate'), array('optional' => true));
         $mform->addHelpButton('reset_end_date', 'enddate');
         $mform->addElement('checkbox', 'reset_events', get_string('deleteevents', 'calendar'));
         $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes'));
index badd2a5..5857ccc 100644 (file)
@@ -25,4 +25,4 @@
 $string['category:config'] = 'Configure category enrol instances';
 $string['category:synchronised'] = 'Role assignments synchronised to course enrolment';
 $string['pluginname'] = 'Category enrolments';
-$string['pluginname_desc'] = 'Category enrolment plugin is a legacy solution for enrolments at the course category level via role assignments. It is recommended to use cohort synchronisation instead.';
+$string['pluginname_desc'] = 'The category enrolments plugin synchronises any role assignments in the category context for roles with the capability enrol/category:synchronised allowed.';
index 7556419..0447951 100644 (file)
@@ -117,6 +117,7 @@ class course_enrolment_manager {
     private $_plugins = null;
     private $_allplugins = null;
     private $_roles = null;
+    private $_visibleroles = null;
     private $_assignableroles = null;
     private $_assignablerolesothers = null;
     private $_groups = null;
@@ -593,6 +594,18 @@ class course_enrolment_manager {
         return $this->_roles;
     }
 
+    /**
+     * Gets all of the roles this course can contain.
+     *
+     * @return array
+     */
+    public function get_viewable_roles() {
+        if ($this->_visibleroles === null) {
+            $this->_visibleroles = get_viewable_roles($this->context);
+        }
+        return $this->_visibleroles;
+    }
+
     /**
      * Gets all of the assignable roles for this course.
      *
@@ -1032,7 +1045,7 @@ class course_enrolment_manager {
         $strunenrol = get_string('unenrol', 'enrol');
         $stredit = get_string('edit');
 
-        $allroles   = $this->get_all_roles();
+        $visibleroles   = $this->get_viewable_roles();
         $assignable = $this->get_assignable_roles();
         $allgroups  = $this->get_all_groups();
         $context    = $this->get_context();
@@ -1054,7 +1067,15 @@ class course_enrolment_manager {
                 if (!is_siteadmin() and !isset($assignable[$rid])) {
                     $unchangeable = true;
                 }
-                $details['roles'][$rid] = array('text'=>$allroles[$rid]->localname, 'unchangeable'=>$unchangeable);
+
+                if (isset($visibleroles[$rid])) {
+                    $label = $visibleroles[$rid];
+                } else {
+                    $label = get_string('novisibleroles', 'role');
+                    $unchangeable = true;
+                }
+
+                $details['roles'][$rid] = array('text' => $label, 'unchangeable' => $unchangeable);
             }
 
             // Users
index 427c130..0e86534 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js and b/enrol/manual/amd/build/quickenrolment.min.js differ
index 6f359b3..980581a 100644 (file)
@@ -137,7 +137,7 @@ define(['core/templates',
         // This hidden fields are added automatically by mforms and when it reaches the AJAX we get an error.
         var hidden = form.find(SELECTORS.UNWANTEDHIDDENFIELDS);
         hidden.each(function() {
-            this.remove();
+            $(this).remove();
         });
 
         var formData = form.serialize();
index 2eefd4a..6b224dd 100644 (file)
@@ -62,7 +62,7 @@ class enrol_manual_externallib_testcase extends externallib_advanced_testcase {
         $this->assignUserCapability('moodle/course:view', $context2->id, $roleid);
         $this->assignUserCapability('moodle/role:assign', $context2->id, $roleid);
 
-        allow_assign($roleid, 3);
+        core_role_set_assign_allowed($roleid, 3);
 
         // Call the external function.
         enrol_manual_external::enrol_users(array(
diff --git a/enrol/tests/behat/role_visibility.feature b/enrol/tests/behat/role_visibility.feature
new file mode 100644 (file)
index 0000000..7c8d911
--- /dev/null
@@ -0,0 +1,38 @@
+@core @core_enrol
+Feature: Test role visibility for the participants page
+  In order to control access
+  As an admin
+  I need to control which roles can see each other
+
+  Background: Add a bunch of users
+    Given  the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | learner1 | Learner   | 1        | learner1@example.com |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | manager1 | Manager   | 1        | manager1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | learner1 | C1     | student        |
+      | teacher1 | C1     | editingteacher |
+      | manager1 | C1     | manager        |
+
+  Scenario: Check the default roles are visible
+    Given I log in as "manager1"
+    And I follow "Course 1"
+    When I navigate to "Enrolled users" node in "Course administration > Users"
+    Then "Learner 1" row "Roles" column of "participants" table should contain "Student"
+    And "Teacher 1" row "Roles" column of "participants" table should contain "Teacher"
+    And "Manager 1" row "Roles" column of "participants" table should contain "Manager"
+    And I should not see "No Roles" in the "table#participants" "css_element"
+
+  Scenario: Do not allow managers to view any roles but manager and check they are hidden
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    When I navigate to "Enrolled users" node in "Course administration > Users"
+    Then "Learner 1" row "Roles" column of "participants" table should contain "Student"
+    And "Teacher 1" row "Roles" column of "participants" table should contain "Teacher"
+    And "Manager 1" row "Roles" column of "participants" table should not contain "Manager"
+    And "Manager 1" row "Roles" column of "participants" table should contain "No roles"
index d52dc6f..1678462 100644 (file)
@@ -55,10 +55,24 @@ class core_enrollib_testcase extends advanced_testcase {
 
         $category1 = $this->getDataGenerator()->create_category(array('visible'=>0));
         $category2 = $this->getDataGenerator()->create_category();
-        $course1 = $this->getDataGenerator()->create_course(array('category'=>$category1->id));
-        $course2 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
-        $course3 = $this->getDataGenerator()->create_course(array('category'=>$category2->id, 'visible'=>0));
-        $course4 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
+
+        $course1 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'Z',
+            'category' => $category1->id,
+        ));
+        $course2 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'X',
+            'category' => $category2->id,
+        ));
+        $course3 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'Y',
+            'category' => $category2->id,
+            'visible' => 0,
+        ));
+        $course4 = $this->getDataGenerator()->create_course(array(
+            'shortname' => 'W',
+            'category' => $category2->id,
+        ));
 
         $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
         $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$maninstance1->id));
@@ -150,6 +164,18 @@ class core_enrollib_testcase extends advanced_testcase {
 
         $courses = enrol_get_all_users_courses($user2->id, false, null, 'id DESC');
         $this->assertEquals(array($course3->id, $course2->id, $course1->id), array_keys($courses));
+
+        // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+
+        $CFG->navsortmycoursessort = 'shortname';
+
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
+
+        // But still the explicit sorting takes precedence over the implicit one.
+
+        $courses = enrol_get_all_users_courses($user1->id, false, null, 'shortname DESC');
+        $this->assertEquals(array($course1->id, $course3->id, $course2->id), array_keys($courses));
     }
 
     public function test_enrol_user_sees_own_courses() {
@@ -590,15 +616,15 @@ class core_enrollib_testcase extends advanced_testcase {
         // Create test user and 4 courses, two of which have guest access enabled.
         $user = $this->getDataGenerator()->create_user();
         $course1 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'Z',
+                (object)array('shortname' => 'X',
                 'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
                 'enrol_guest_password_0' => ''));
         $course2 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'Y',
+                (object)array('shortname' => 'Z',
                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
                 'enrol_guest_password_0' => ''));
         $course3 = $this->getDataGenerator()->create_course(
-                (object)array('shortname' => 'X',
+                (object)array('shortname' => 'Y',
                 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
                 'enrol_guest_password_0' => 'frog'));
         $course4 = $this->getDataGenerator()->create_course(
@@ -645,10 +671,19 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertObjectHasAttribute('summary', $courses[$course3->id]);
         $this->assertObjectHasAttribute('summaryformat', $courses[$course3->id]);
 
-        // Check sort parameter still works.
-        $courses = enrol_get_my_courses(null, 'shortname', 0, [], true);
+        // By default, courses are ordered by sortorder - which by default is most recent first.
+        $courses = enrol_get_my_courses(null, null, 0, [], true);
         $this->assertEquals([$course3->id, $course2->id, $course1->id], array_keys($courses));
 
+        // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+        $CFG->navsortmycoursessort = 'shortname';
+        $courses = enrol_get_my_courses(null, null, 0, [], true);
+        $this->assertEquals([$course1->id, $course3->id, $course2->id], array_keys($courses));
+
+        // But still the explicit sorting takes precedence over the implicit one.
+        $courses = enrol_get_my_courses(null, 'shortname DESC', 0, [], true);
+        $this->assertEquals([$course2->id, $course3->id, $course1->id], array_keys($courses));
+
         // Check filter parameter still works.
         $courses = enrol_get_my_courses(null, 'id', 0, [$course2->id, $course3->id, $course4->id], true);
         $this->assertEquals([$course2->id, $course3->id], array_keys($courses));
index 528fe37..09ff3ba 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.5 ===
+
+* Default sorting in enrol_get_my_courses(), enrol_get_all_users_courses() and enrol_get_users_courses() now respects
+  the site setting "navsortmycoursessort" and should be consistently used when displaying the courses in the UI.
+
 === 3.4 ===
 
 * render_course_enrolment_users_table method has been removed from the renderer. The enrolled users page is now
index 97b62b7..aa3ada7 100644 (file)
@@ -156,10 +156,10 @@ class enrol_users_filter_form extends moodleform {
         // names if applied. The reason for not restricting to roles that can
         // be assigned at course level is that upper-level roles display in the
         // enrolments table so it makes sense to let users filter by them.
-        $allroles = $manager->get_all_roles();
+        $visibleroles = $manager->get_viewable_roles();
         $rolenames = array();
-        foreach ($allroles as $id => $role) {
-            $rolenames[$id] = $role->localname;
+        foreach ($visibleroles as $id => $role) {
+            $rolenames[$id] = $role;
         }
         $mform->addElement('select', 'role', get_string('role'),
                 array(0 => get_string('all')) + $rolenames);
index 65212dd..889f6c4 100644 (file)
@@ -82,7 +82,8 @@ class grade_export_form extends moodleform {
         }
 
         $mform->addElement('advcheckbox', 'export_feedback', get_string('exportfeedback', 'grades'));
-        $mform->setDefault('export_feedback', 0);
+        $exportfeedback = isset($CFG->grade_export_exportfeedback) ? $CFG->grade_export_exportfeedback : 0;
+        $mform->setDefault('export_feedback', $exportfeedback);
         $coursecontext = context_course::instance($COURSE->id);
         if (has_capability('moodle/course:viewsuspendedusers', $coursecontext)) {
             $mform->addElement('advcheckbox', 'export_onlyactive', get_string('exportonlyactive', 'grades'));
index 57bd616..ca774c1 100644 (file)
@@ -978,6 +978,7 @@ function groups_calculate_role_people($rs, $context) {
     }
 
     $allroles = role_fix_names(get_all_roles($context), $context);
+    $visibleroles = get_viewable_roles($context);
 
     // Array of all involved roles
     $roles = array();
@@ -1036,14 +1037,15 @@ function groups_calculate_role_people($rs, $context) {
 
     // Now we rearrange the data to store users by role
     foreach ($users as $userid=>$userdata) {
-        $rolecount = count($userdata->roles);
+        $visibleuserroles = array_intersect_key($userdata->roles, $visibleroles);
+        $rolecount = count($visibleuserroles);
         if ($rolecount == 0) {
             // does not have any roles
             $roleid = 0;
         } else if($rolecount > 1) {
             $roleid = '*';
         } else {
-            $userrole = reset($userdata->roles);
+            $userrole = reset($visibleuserroles);
             $roleid = $userrole->id;
         }
         $roles[$roleid]->users[$userid] = $userdata;
diff --git a/group/tests/behat/role_visibility.feature b/group/tests/behat/role_visibility.feature
new file mode 100644 (file)
index 0000000..37b283b
--- /dev/null
@@ -0,0 +1,52 @@
+@core @core_group
+Feature: Test role visibility for the groups management page
+  In order to control access
+  As an admin
+  I need to control which roles can see each other
+
+  Background: Set up some groups
+    Given  the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | learner1 | Learner   | 1        | learner1@example.com |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | manager1 | Manager   | 1        | manager1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | learner1 | C1     | student        |
+      | teacher1 | C1     | editingteacher |
+      | manager1 | C1     | manager        |
+    And the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+    And the following "group members" exist:
+      | user     | group |
+      | learner1 | G1    |
+      | teacher1 | G1    |
+      | manager1 | G1    |
+
+  Scenario: Check the default roles are visible
+    Given I log in as "manager1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Groups" node in "Course administration > Users"
+    When I set the field "groups" to "Group 1 (3)"
+    And I press "Show members for group"
+    Then "optgroup[label='No roles']" "css_element" should not exist in the "#members" "css_element"
+    And "optgroup[label='Student']" "css_element" should exist in the "#members" "css_element"
+    And "optgroup[label='Teacher']" "css_element" should exist in the "#members" "css_element"
+    And "optgroup[label='Manager']" "css_element" should exist in the "#members" "css_element"
+    And I log out
+
+  Scenario: Do not allow managers to view any roles and check they are hidden
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Groups" node in "Course administration > Users"
+    When I set the field "groups" to "Group 1 (3)"
+    And I press "Show members for group"
+    Then "optgroup[label='No roles']" "css_element" should exist in the "#members" "css_element"
+    And "optgroup[label='Student']" "css_element" should exist in the "#members" "css_element"
+    And "optgroup[label='Teacher']" "css_element" should exist in the "#members" "css_element"
+    And "optgroup[label='Manager']" "css_element" should not exist in the "#members" "css_element"
+    And I log out
index 7ce183a..0d4b9a1 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'שפת ממשק';
+$string['moodlelogo'] = 'לוגו של Moodle';
 $string['next'] = 'הבא';
 $string['previous'] = 'קודם';
 $string['reload'] = 'טעינה מחדש';
index d72a54e..9e3eca5 100644 (file)
@@ -35,4 +35,8 @@ $string['cliansweryes'] = 'वाई';
 $string['cliincorrectvalueerror'] = 'त्रुटि, "{$a->option}" के लिए गलत मान  जो की  "{$a->value}" है';
 $string['cliincorrectvalueretry'] = 'ग़लत मान, कृपया पुनः प्रयास करें';
 $string['clitypevalue'] = 'टाइप मूल्य';
+$string['clitypevaluedefault'] = 'वैल्यू टाइप करें, डिफ़ॉल्ट वैल्यू ({$ a}) का उपयोग करने के लिए Enter दबाएं';
+$string['cliunknowoption'] = 'अपरिचित विकल्प:
+   {$a}
+कृपया --help विकल्प का उपयोग करें।';
 $string['cliyesnoprompt'] = 'टाइप Y (का मतलब है हाँ) या N (का मतलब है नहीं )';
index f6970c4..6f164a7 100644 (file)
@@ -81,17 +81,16 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
     }
 
     public function test_ipv4() {
-
-        $result = iplookup_find_location('131.111.150.25');
+        $result = iplookup_find_location('192.30.255.112');
 
         $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Cambridge', $result['city']);
-        $this->assertEquals(0.1167, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(52.2, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals('San Francisco', $result['city']);
+        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
         $this->assertNull($result['error']);
         $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Cambridge', $result['title'][0]);
-        $this->assertEquals('United Kingdom', $result['title'][1]);
+        $this->assertEquals('San Francisco', $result['title'][0]);
+        $this->assertEquals('United States', $result['title'][1]);
     }
 
     public function test_ipv6() {
@@ -110,4 +109,3 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
         $this->assertEquals('United States', $result['title'][1]);
     }
 }
-
index 8c94273..864d944 100644 (file)
@@ -46,17 +46,17 @@ class core_iplookup_geoplugin_testcase extends advanced_testcase {
         $CFG->geoipfile = '';
     }
 
-    public function test_geoip_ipv4() {
-        $result = iplookup_find_location('131.111.150.25');
+    public function test_ipv4() {
+        $result = iplookup_find_location('192.30.255.112');
 
         $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Cambridge', $result['city']);
-        $this->assertEquals(0.1167, $result['longitude'], '', 0.001);
-        $this->assertEquals(52.200000000000003, $result['latitude'], '', 0.001);
+        $this->assertEquals('San Francisco', $result['city']);
+        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
+        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
         $this->assertNull($result['error']);
         $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Cambridge', $result['title'][0]);
-        $this->assertEquals('United Kingdom', $result['title'][1]);
+        $this->assertEquals('San Francisco', $result['title'][0]);
+        $this->assertEquals('United States', $result['title'][1]);
     }
 
     public function test_geoip_ipv6() {
index 69291ba..7a48f3b 100644 (file)
@@ -154,6 +154,7 @@ $string['configallowusermailcharset'] = 'If enabled, users can choose an email c
 $string['configallowuserswitchrolestheycantassign'] = 'By default, moodle/role:assign is required for users to switch roles. Enabling this setting removes this requirement, and results in the roles available in the "Switch role to" dropdown menu being determined by settings in the "Allow role assignments" table only.
 It is recommended that the settings in the "Allow role assignments" table do not allow users to switch to a role with more capabilities than their existing role.';
 $string['configallowuserthemes'] = 'If you enable this, then users will be allowed to set their own themes.  User themes override site themes (but not course themes)';
+$string['configallowview'] = 'Select which roles a user will see, be able to filter by etc. based on which roles they already have.';
 $string['configallusersaresitestudents'] = 'For activities on the front page of the site, should ALL users be considered as students?  If you answer "Yes", then any confirmed user account will be allowed to participate as a student in those activities.  If you answer "No", then only users who are already a participant in at least one course will be able to take part in those front page activities. Only admins and specially assigned teachers can act as teachers for these front page activities.';
 $string['configauthenticationplugins'] = 'Please choose the authentication plugins you wish to use and arrange them in order of failthrough.';
 $string['configautolang'] = 'Detect default language from browser setting, if disabled site default is used.';
@@ -196,8 +197,8 @@ $string['configdefaulthomepage'] = 'This determines the home page for logged in
 $string['configdefaultrequestcategory'] = 'Courses requested by users will be automatically placed in this category.';
 $string['configdefaultrequestedcategory'] = 'Default category to put courses that were requested into, if they\'re approved.';
 $string['configdefaultuserroleid'] = 'All logged in users will be given the capabilities of the role you specify here, at the site level, in ADDITION to any other roles they may have been given.  The default is the Authenticated user role.  Note that this will not conflict with other roles they have unless you prohibit capabilities, it just ensures that all users have capabilities that are not assignable at the course level (eg post blog entries, manage own calendar, etc).';
-$string['configdeleteincompleteusers'] = 'After this period, old not fully setup accounts are deleted.';
-$string['configdeleteunconfirmed'] = 'If you are using email authentication, this is the period within which a response will be accepted from users.  After this period, old unconfirmed accounts are deleted.';
+$string['configdeleteincompleteusers'] = 'After this period, any account without the first name, last name or email field filled in is deleted.';
+$string['configdeleteunconfirmed'] = 'For certain authentication methods, such as email-based self-registration, users must confirm their account within a certain time. After this period, any old unconfirmed accounts are deleted.';
 $string['configdenyemailaddresses'] = 'To deny email addresses from particular domains list them here in the same way.  All other domains will be accepted. To deny subdomains add the domain with a preceding \'.\'. eg <strong>hotmail.com yahoo.co.uk .live.com</strong>';
 $string['configenableblogs'] = 'This switch provides all site users with their own blog.';
 $string['configenabledevicedetection'] = 'Enables detection of mobiles, smartphones, tablets or default devices (desktop PCs, laptops, etc) for the application of themes and other features.';
@@ -293,7 +294,7 @@ $string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If
 $string['configperfdebug'] = 'If you turn this on, performance info will be printed in the footer of the standard theme';
 $string['configprofileroles'] = 'List of roles that are visible on user profiles and participation page.';
 $string['configprofilesforenrolledusersonly'] = 'To prevent misuse by spammers, profile descriptions of users who are not yet enrolled in any course are hidden. New users must enrol in at least one course before they can add a profile description.';
-$string['configprotectusernames'] = 'By default forget_password.php does not display any hints that would allow guessing of usernames or email addresses.';
+$string['configprotectusernames'] = 'If enabled, the forgotten password form will not display any hints allowing account usernames or email addresses to be guessed.';
 $string['configproxybypass'] = 'Comma separated list of (partial) hostnames or IPs that should bypass proxy (e.g., 192.168., .mydomain.com)';
 $string['configproxyhost'] = 'If this <b>server</b> needs to use a proxy computer (eg a firewall) to access the Internet, then provide the proxy hostname here.  Otherwise leave it blank.';
 $string['configproxypassword'] = 'Password needed to access internet through proxy if required, empty if none (PHP cURL extension required).';
@@ -835,7 +836,7 @@ $string['pathtopsql'] = 'Path to psql';
 $string['pathtopsqldesc'] = 'This is only necessary to enter if you have more than one psql on your system (for example if you have more than one version of postgresql installed)';
 $string['pathtopsqlinvalid'] = 'Invalid path to psql - either wrong path or not executable';
 $string['pathtopython'] = 'Path to Python';
-$string['pathtopythondesc'] = 'Path to your executable Python binary.';
+$string['pathtopythondesc'] = 'Path to your executable Python binary (both Python 2 and Python 3 are acceptable).';
 $string['pcreunicodewarning'] = 'It is strongly recommended to use PCRE PHP extension that is compatible with Unicode characters.';
 $string['perfdebug'] = 'Performance info';
 $string['performance'] = 'Performance';
@@ -940,7 +941,7 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
 $string['recaptchapublickey'] = 'ReCAPTCHA site key';
 $string['register'] = 'Register your site';
-$string['registermoodlenet'] = '<p>We\'d love to stay in touch for important things for your Moodle site!</p><p>By registering,</p><ul><li>You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services.</li><li>You’ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>Optionally, your site can be included as a proud member and supporter of the Moodle community on the <a href="https://moodle.net/stats">list of registered sites</a>.</li></ul>';
+$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You\'ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>You are contributing to our <a href="https://moodle.net/stats/">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://moodle.net/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
 $string['registermoodleorg'] = 'When you register your site';
 $string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
 $string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
@@ -1161,7 +1162,7 @@ $string['uninstallplugin'] = 'Uninstall';
 $string['unlockaccount'] = 'Unlock account';
 $string['unsettheme'] = 'Unset theme';
 $string['unsupported'] = 'Unsupported';
-$string['unsupporteddbfileformat'] = 'Your database has tables using Antelope as the file format. Full UTF-8 support in MySQL and MariaDB requires the Barracuda file format. Please convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
+$string['unsupporteddbfileformat'] = 'Your database uses Antelope as the file format. Full UTF-8 support in MySQL and MariaDB requires the Barracuda file format. Please switch to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/admin/environment/custom check/mysql full unicode support">MySQL full unicode support</a> for details.';
 $string['unsupporteddbfilepertable'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_file_per_table\' to \'ON\'. See the documentation for further details.';
 $string['unsupporteddblargeprefix'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_large_prefix\' to \'ON\'. See the documentation for further details.';
 $string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
@@ -1256,7 +1257,7 @@ $string['warningcurrentsetting'] = 'Invalid current value: {$a}';
 $string['warningiconvbuggy'] = 'Your version of the iconv library does not support the //IGNORE modifier. You should install the mbstring extension which can be used instead for cleaning strings containing invalid UTF-8 characters.';
 $string['webproxy'] = 'Web proxy';
 $string['webproxyinfo'] = 'Fill in following options if your Moodle server can not access internet directly. Internet access is required for download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>PHP cURL extension is highly recommended.</em>';
-$string['xmlrpcrecommended'] = 'The xmlrpc extension is useful for web services and Moodle networking';
+$string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.';
 $string['yuicomboloading'] = 'YUI combo loading';
 $string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.';
 
index d2493b1..d5f70cb 100644 (file)
@@ -64,8 +64,9 @@ $string['auth_updateremote_ldap'] = '<p><b>Note:</b> Updating external LDAP data
 $string['auth_user_create'] = 'Enable user creation';
 $string['auth_user_creation'] = 'New (anonymous) users can create user accounts on the external authentication source and confirmed via email. If you enable this , remember to also configure module-specific options for user creation.';
 $string['auth_usernameexists'] = 'Selected username already exists. Please choose a new one.';
+$string['auth_usernotexist'] = 'Cannot update non-existent user: {$a}';
 $string['auto_add_remote_users'] = 'Auto add remote users';
-$string['cannotmapfield'] = 'Field "{$a->fieldname}" can not be mapped because its short name "{$a->shortname}" is too long. To enable mapping reduce the profile field short name down to {$a->charlimit} characters. <a href="{$a->link}">Edit user profile fields</a>';
+$string['cannotmapfield'] = 'The field "{$a->fieldname}" can\'t be mapped because its short name "{$a->shortname}" is too long. To allow it to be mapped, you need to reduce the short name to {$a->charlimit} characters. <a href="{$a->link}">Edit user profile fields</a>';
 $string['createpassword'] = 'Generate password and notify user';
 $string['createpasswordifneeded'] = 'Create password if needed and send via email';
 $string['emailchangecancel'] = 'Cancel email change';
index 5e2e3d4..46ed93e 100644 (file)
@@ -140,6 +140,7 @@ $string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->b
 $string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
 $string['errorcopyingbackupfile'] = "Failed to copy the backup file to the temporary folder before restoring.";
 $string['errorfilenamerequired'] = 'You must enter a valid filename for this backup';
+$string['errorfilenametoolong'] = 'The filename length you enter must be less than 255 characters';
 $string['errorfilenamemustbezip'] = 'The filename you enter must be a ZIP file and have the .mbz extension';
 $string['errorminbackup20version'] = 'This backup file has been created with one development version of Moodle backup ({$a->backup}). Minimum required is {$a->min}. Cannot be restored.';
 $string['errorinvalidformat'] = 'Unknown backup format';
index 2364312..309f29c 100644 (file)
@@ -27,7 +27,7 @@
 $string['actions'] = 'Actions';
 $string['activate'] = 'Enable access';
 $string['activatesuccess'] = 'Access to the badges was successfully enabled.';
-$string['addbadge'] = 'Add a badge as criteria';
+$string['addbadge'] = 'Add badges';
 $string['addbadge_help'] = 'Select all badges that should be added to this badge requirement. Hold CTRL key to select multiple items.';
 $string['addbadgecriteria'] = 'Add badge criteria';
 $string['addcriteria'] = 'Add criteria';
@@ -260,7 +260,7 @@ $string['error:invalidexpiredate'] = 'Expiry date has to be in the future.';
 $string['error:invalidexpireperiod'] = 'Expiry period cannot be negative or equal 0.';
 $string['error:invalidparambadge'] = 'Badge does not exist. ';
 $string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.';
-$string['error:nobadges'] = 'There are no course or site badges available to be added as criteria.  Make sure that your other badges are enabled ';
+$string['error:nobadges'] = 'There are no course or site badges with access enabled to be added as criteria.';
 $string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.';
 $string['error:nogroups'] = '<p>There are no public collections of badges available in your backpack. </p>
 <p>Only public collections are shown, <a href="http://backpack.openbadges.org">visit your backpack</a> to create some public collections.</p>';
index 81caef6..b166512 100644 (file)
@@ -202,6 +202,7 @@ $string['expand'] = 'Expand category';
 $string['export'] = 'Export';
 $string['exportalloutcomes'] = 'Export all outcomes';
 $string['exportfeedback'] = 'Include feedback in export';
+$string['exportfeedback_desc'] = 'This can be overridden during export.';
 $string['exportformatoptions'] = 'Export format options';
 $string['exportplugins'] = 'Export plugins';
 $string['exportsettings'] = 'Export settings';
index 016b1fa..fcd85b0 100644 (file)
@@ -25,7 +25,7 @@
 
 $string['addscreenshots'] = 'Add screenshots';
 $string['advertise'] = 'Share this course for people to join';
-$string['advertised'] = 'Advertised';
+$string['advertised'] = 'For people to join';
 $string['advertiseon'] = 'Share this course on {$a}';
 $string['readvertiseon'] = 'Update advertising information on {$a}';
 $string['advertisepublication_help'] = 'This course will be listed on Moodle.net as a course that people can enrol in and participate. Email-based self-registration should be enabled on the site and you need to enable self enrolment in this course.';
@@ -119,7 +119,7 @@ $string['publicationinfo'] = 'Course publication information';
 $string['publish'] = 'Share';
 $string['publishcourse'] = 'Share {$a}';
 $string['publishcourseon'] = 'Share on {$a}';
-$string['publishedon'] = 'Publications';
+$string['publishedon'] = 'Course sharing';
 $string['publisheremail'] = 'Publisher email';
 $string['publisheremail_help'] = 'The publisher email address allows the hub administrator to alert the publisher about any changes to the status of the published course.';
 $string['publishername'] = 'Publisher';
@@ -147,7 +147,7 @@ $string['sendfollowinginfo'] = 'More information';
 $string['sendfollowinginfo_help'] = 'The following information will be sent to contribute to overall statistics only.  It will not be made public on any site listing.';
 $string['sent'] = '...finished';
 $string['share'] = 'Share this course for people to download';
-$string['shared'] = 'Shared';
+$string['shared'] = 'For people to download';
 $string['shareon'] = 'Upload this course to {$a}';
 $string['sharepublication_help'] = 'A backup of this course will be available on Moodle.net for people to restore and use on their own site.';
 $string['siteadmin'] = 'Administrator';
@@ -195,10 +195,10 @@ $string['statuspublished'] = 'Listed';
 $string['statusunpublished'] = 'Not listed';
 $string['tags'] = 'Tags';
 $string['tags_help'] = 'Tags help to further categorise your course and help it to be found. Please use simple, meaningful words and separate them with a comma. Example: math, algebra, geometry';
-$string['type'] = 'Advertised / Shared';
+$string['type'] = 'Shared';
 $string['unpublish'] = 'Stop sharing';
-$string['unpublishalladvertisedcourses'] = 'Remove all courses currently being advertised on Moodle.net';
-$string['unpublishalluploadedcourses'] = 'Removed all courses that were uploaded to Moodle.net';
+$string['unpublishalladvertisedcourses'] = 'Remove all courses that were shared on Moodle.net for people to join';
+$string['unpublishalluploadedcourses'] = 'Remove all courses that were shared on Moodle.net for people to download';
 $string['unpublishconfirmation'] = 'Do you really want to remove the course "{$a->courseshortname}" from "{$a->hubname}"';
 $string['unpublishcourse'] = 'Stop sharing {$a}';
 $string['unregister'] = 'Unregister';
index 48eaf09..331cf1f 100644 (file)
@@ -48,6 +48,8 @@ $string['strftimemonthyear'] = '%B %Y';
 $string['strftimerecent'] = '%d %b, %H:%M';
 $string['strftimerecentfull'] = '%a, %d %b %Y, %I:%M %p';
 $string['strftimetime'] = '%I:%M %p';
+$string['strftimetime12'] = '%I:%M %p';
+$string['strftimetime24'] = '%H:%M';
 $string['thisdirection'] = 'ltr';
 $string['thisdirectionvertical'] = 'btt';
 $string['thislanguage'] = 'English';
index f1eb252..7df89eb 100644 (file)
@@ -799,6 +799,7 @@ $string['feedback'] = 'Feedback';
 $string['file'] = 'File';
 $string['fileexists'] = 'There is already a file called {$a}';
 $string['filemissing'] = '{$a} is missing';
+$string['filereaderror'] = 'Unable to read the file \'{$a}\' - please check this really is a file and not a folder';
 $string['files'] = 'Files';
 $string['filesanduploads'] = 'Files and uploads';
 $string['filesfolders'] = 'Files/folders';
@@ -1747,7 +1748,7 @@ $string['selectcoursesortby'] = 'Select how you would like to sort courses';
 $string['senddetails'] = 'Send my details via email';
 $string['separate'] = 'Separate';
 $string['separateandconnected'] = 'Separate and Connected ways of knowing';
-$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tends to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
+$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
 $string['servererror'] = 'An error occurred whilst communicating with the server';
 $string['serverlocaltime'] = 'Server\'s local time';
 $string['setcategorytheme'] = 'Set category theme';
index a978f52..3b67454 100644 (file)
@@ -166,6 +166,7 @@ $string['manage'] = 'Manage repositories';
 $string['manageinstances'] = 'Manage instances';
 $string['manageurl'] = 'Manage';
 $string['manageuserrepository'] = 'Manage individual repository';
+$string['missingsourcekey'] = 'The source key is missing. This key must also be provided to retrieve the file.';
 $string['moving'] = 'Moving';
 $string['name'] = 'Name';
 $string['newfolder'] = 'New folder';
@@ -222,6 +223,7 @@ $string['setmainfile'] = 'Set main file';
 $string['setmainfile_help'] = 'If there are multiple files in the folder, the main file is the one that appears on the view page. Other files such as images or videos may be embedded in it. In filemanager the main file is indicated with a title in bold.';
 $string['siteinstances'] = 'Repositories instances of the site';
 $string['size'] = 'Size';
+$string['sourcekeymismatch'] = 'The source url does not match the sourcekey.';
 $string['submit'] = 'Submit';
 $string['sync'] = 'Sync';
 $string['syncfiletimeout'] = 'Sync file timeout';
index f105225..8c8072e 100644 (file)
@@ -32,7 +32,9 @@ $string['allowoverride'] = 'Allow role overrides';
 $string['allowroletoassign'] = 'Allow users with role {$a->fromrole} to assign the role {$a->targetrole}';
 $string['allowroletooverride'] = 'Allow users with role {$a->fromrole} to override the role {$a->targetrole}';
 $string['allowroletoswitch'] = 'Allow users with role {$a->fromrole} to switch roles to the role {$a->targetrole}';
+$string['allowroletoview'] = 'Allow users with role {$a->fromrole} to view the role {$a->targetrole}';
 $string['allowswitch'] = 'Allow role switches';
+$string['allowview'] = 'Allow role to view';
 $string['allsiteusers'] = 'All site users';
 $string['analytics:listinsights'] = 'List insights';
 $string['analytics:managemodels'] = 'Manage models';
@@ -219,6 +221,7 @@ $string['errorexistsroleshortname'] = 'Role name already exists';
 $string['eventroleallowassignupdated'] = 'Allow role assignment';
 $string['eventroleallowoverrideupdated'] = 'Allow role override';
 $string['eventroleallowswitchupdated'] = 'Allow role switch';
+$string['eventroleallowviewupdated'] = 'Allow role view';
 $string['eventroleassigned'] = 'Role assigned';
 $string['eventrolecapabilitiesupdated'] = 'Role capabilities updated';
 $string['eventroledeleted'] = 'Role deleted';
@@ -292,6 +295,7 @@ $string['noneinthisxmatching'] = 'No users matching \'{$a->search}\' in this {$a
 $string['norole'] = 'No role';
 $string['noroles'] = 'No roles';
 $string['noroleassignments'] = 'This user does not have any role assignments anywhere in this site.';
+$string['novisibleroles'] = 'No roles';
 $string['notabletoassignroleshere'] = 'Assigning of roles in this context has not been enabled by an administrator.';
 $string['notabletooverrideroleshere'] = 'You are not able to override the permissions on any roles here';
 $string['notes:manage'] = 'Manage notes';
index 552d9b6..8241c69 100644 (file)
@@ -36,6 +36,7 @@ $string['createdon'] = 'Created on';
 $string['database'] = 'Database';
 $string['databasestate'] = 'Indexing database state';
 $string['datadirectory'] = 'Data directory';
+$string['deleteindex'] = 'Delete index {$a}';
 $string['deletionsinindex'] = 'Deletions in index';
 $string['docmodifiedon'] = 'Last modified on {$a}';
 $string['doctype'] = 'Doctype';
@@ -51,6 +52,7 @@ $string['enginenotinstalled'] = 'Engine {$a} is not installed.';
 $string['enginenotselected'] = 'You have not selected any search engine.';
 $string['engineserverstatus'] = 'The search engine is not available. Please contact your administrator.';
 $string['enteryoursearchquery'] = 'Enter your search query';
+$string['error_indexing'] = 'An error occurred while indexing';
 $string['errors'] = 'Errors';
 $string['errorareanotavailable'] = '{$a} search area is not available.';
 $string['everywhere'] = 'Everywhere you can access';
@@ -59,6 +61,9 @@ $string['filterheader'] = 'Filter';
 $string['fromtime'] = 'Modified after';
 $string['globalsearch'] = 'Global search';
 $string['globalsearchdisabled'] = 'Global searching is not enabled.';
+$string['gradualreindex'] = 'Gradual reindex {$a}';
+$string['gradualreindex_confirm'] = 'Are you sure you want to reindex {$a}? This may take some time, although existing data will remain available during the reindex.';
+$string['gradualreindex_queued'] = 'Reindexing has been requested for {$a->name} ({$a->count} contexts). This indexing will be carried out by the &lsquo;Global search indexing&rsquo; scheduled task.';
 $string['checkdb'] = 'Check database';
 $string['checkdbadvice'] = 'Check your database for any problems.';
 $string['checkdir'] = 'Check dir';
@@ -76,7 +81,12 @@ $string['notitle'] = 'No title';
 $string['normalsearch'] = 'Normal search';
 $string['openedon'] = 'opened on';
 $string['optimize'] = 'Optimize';
+$string['priority'] = 'Priority';
+$string['priority_reindexing'] = 'Reindexing';
+$string['priority_normal'] = 'Normal';
+$string['progress'] = 'Progress';
 $string['queryerror'] = 'The query you provided could not be parsed by the search engine: {$a}';
+$string['queueheading'] = 'Additional indexing queue ({$a} items)';
 $string['resultsreturnedfor'] = 'results returned for';
 $string['runindexer'] = 'Run indexer (real)';
 $string['runindexertest'] = 'Run indexer test';
index 856a809..98aaa06 100644 (file)
@@ -1984,7 +1984,7 @@ function get_default_capabilities($archetype) {
  * Return default roles that can be assigned, overridden or switched
  * by give role archetype.
  *
- * @param string $type  assign|override|switch
+ * @param string $type  assign|override|switch|view
  * @param string $archetype
  * @return array of role ids
  */
@@ -2034,6 +2034,16 @@ function get_default_role_archetype_allows($type, $archetype) {
             'user'           => array(),
             'frontpage'      => array(),
         ),
+        'view' => array(
+            'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
+            'coursecreator'  => array('coursecreator', 'editingteacher', 'teacher', 'student'),
+            'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
+            'teacher'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
+            'student'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
+            'guest'          => array(),
+            'user'           => array(),
+            'frontpage'      => array(),
+        ),
     );
 
     if (!isset($defaults[$type][$archetype])) {
@@ -2602,10 +2612,14 @@ function get_user_roles_in_course($userid, $courseid) {
     $rolestring = '';
 
     if ($roles = $DB->get_records_sql($sql, $params)) {
-        $rolenames = role_fix_names($roles, $context, ROLENAME_ALIAS, true);   // Substitute aliases
+        $viewableroles = get_viewable_roles($context, $userid);
 
-        foreach ($rolenames as $roleid => $rolename) {
-            $rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
+        $rolenames = array();
+        foreach ($roles as $roleid => $unused) {
+            if (isset($viewableroles[$roleid])) {
+                $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
+                $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
+            }
         }
         $rolestring = implode(',', $rolenames);
     }
@@ -2838,16 +2852,16 @@ function get_user_roles_with_special(context $context, $userid = 0) {
 /**
  * Creates a record in the role_allow_override table
  *
- * @param int $sroleid source roleid
- * @param int $troleid target roleid
+ * @param int $fromroleid source roleid
+ * @param int $targetroleid target roleid
  * @return void
  */
-function allow_override($sroleid, $troleid) {
+function core_role_set_override_allowed($fromroleid, $targetroleid) {
     global $DB;
 
     $record = new stdClass();
-    $record->roleid        = $sroleid;
-    $record->allowoverride = $troleid;
+    $record->roleid        = $fromroleid;
+    $record->allowoverride = $targetroleid;
     $DB->insert_record('role_allow_override', $record);
 }
 
@@ -2858,7 +2872,7 @@ function allow_override($sroleid, $troleid) {
  * @param int $targetroleid target roleid
  * @return void
  */
-function allow_assign($fromroleid, $targetroleid) {
+function core_role_set_assign_allowed($fromroleid, $targetroleid) {
     global $DB;
 
     $record = new stdClass();
@@ -2874,7 +2888,7 @@ function allow_assign($fromroleid, $targetroleid) {
  * @param int $targetroleid target roleid
  * @return void
  */
-function allow_switch($fromroleid, $targetroleid) {
+function core_role_set_switch_allowed($fromroleid, $targetroleid) {
     global $DB;
 
     $record = new stdClass();
@@ -2883,6 +2897,22 @@ function allow_switch($fromroleid, $targetroleid) {
     $DB->insert_record('role_allow_switch', $record);
 }
 
+/**
+ * Creates a record in the role_allow_view table
+ *
+ * @param int $fromroleid source roleid
+ * @param int $targetroleid target roleid
+ * @return void
+ */
+function core_role_set_view_allowed($fromroleid, $targetroleid) {
+    global $DB;
+
+    $record = new stdClass();
+    $record->roleid      = $fromroleid;
+    $record->allowview = $targetroleid;
+    $DB->insert_record('role_allow_view', $record);
+}
+
 /**
  * Gets a list of roles that this user can assign in this context
  *
@@ -3023,6 +3053,58 @@ function get_switchable_roles(context $context) {
     return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
 }
 
+/**
+ * Gets a list of roles that this user can view in a context
+ *
+ * @param context $context a context.
+ * @param int $userid id of user.
+ * @return array an array $roleid => $rolename.
+ */
+function get_viewable_roles(context $context, $userid = null) {
+    global $USER, $DB;
+
+    if ($userid == null) {
+        $userid = $USER->id;
+    }
+
+    $params = array();
+    $extrajoins = '';
+    $extrawhere = '';
+    if (!is_siteadmin()) {
+        // Admins are allowed to view any role.
+        // Others are subject to the additional constraint that the view role must be allowed by
+        // 'role_allow_view' for some role they have assigned in this context or any parent.
+        $contexts = $context->get_parent_context_ids(true);
+        list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
+
+        $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
+                       JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
+        $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
+
+        $params += $inparams;
+        $params['userid'] = $userid;
+    }
+
+    if ($coursecontext = $context->get_course_context(false)) {
+        $params['coursecontext'] = $coursecontext->id;
+    } else {
+        $params['coursecontext'] = 0; // No course aliases.
+        $coursecontext = null;
+    }
+
+    $query = "
+        SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
+          FROM {role} r
+          $extrajoins
+     LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
+          $extrawhere
+      GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
+      ORDER BY r.sortorder";
+    $roles = $DB->get_records_sql($query, $params);
+
+    return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
+}
+
 /**
  * Gets a list of roles that this user can override in this context.
  *
index e1735fd..6752ae4 100644 (file)
@@ -110,6 +110,13 @@ class auth_plugin_base {
      */
     var $customfields = null;
 
+    /**
+     * The tag we want to prepend to any error log messages.
+     *
+     * @var string
+     */
+    protected $errorlogtag = '';
+
     /**
      * This is the primary method that is used by the authenticate_user_login()
      * function in moodlelib.php.
@@ -618,6 +625,92 @@ class auth_plugin_base {
     public function postlogout_hook($user) {
     }
 
+    /**
+     * Update a local user record from an external source.
+     * This is a lighter version of the one in moodlelib -- won't do
+     * expensive ops such as enrolment.
+     *
+     * @param string $username username
+     * @param array $updatekeys fields to update, false updates all fields.
+     * @param bool $triggerevent set false if user_updated event should not be triggered.
+     *             This will not affect user_password_updated event triggering.
+     * @param bool $suspenduser Should the user be suspended?
+     * @return stdClass|bool updated user record or false if there is no new info to update.
+     */
+    protected function update_user_record($username, $updatekeys = false, $triggerevent = false, $suspenduser = false) {
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+
+        // Just in case check text case.
+        $username = trim(core_text::strtolower($username));
+
+        // Get the current user record.
+        $user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id));
+        if (empty($user)) { // Trouble.
+            error_log($this->errorlogtag . get_string('auth_usernotexist', 'auth', $username));
+            print_error('auth_usernotexist', 'auth', '', $username);
+            die;
+        }
+
+        // Protect the userid from being overwritten.
+        $userid = $user->id;
+
+        $needsupdate = false;
+
+        if ($newinfo = $this->get_userinfo($username)) {
+            $newinfo = truncate_userinfo($newinfo);
+
+            if (empty($updatekeys)) { // All keys? this does not support removing values.
+                $updatekeys = array_keys($newinfo);
+            }
+
+            if (!empty($updatekeys)) {
+                $newuser = new stdClass();
+                $newuser->id = $userid;
+                // The cast to int is a workaround for MDL-53959.
+                $newuser->suspended = (int) $suspenduser;
+                // Load all custom fields.
+                $profilefields = (array) profile_user_record($user->id, false);
+                $newprofilefields = [];
+
+                foreach ($updatekeys as $key) {
+                    if (isset($newinfo[$key])) {
+                        $value = $newinfo[$key];
+                    } else {
+                        $value = '';
+                    }
+
+                    if (!empty($this->config->{'field_updatelocal_' . $key})) {
+                        if (preg_match('/^profile_field_(.*)$/', $key, $match)) {
+                            // Custom field.
+                            $field = $match[1];
+                            $currentvalue = isset($profilefields[$field]) ? $profilefields[$field] : null;
+                            $newprofilefields[$field] = $value;
+                        } else {
+                            // Standard field.
+                            $currentvalue = isset($user->$key) ? $user->$key : null;
+                            $newuser->$key = $value;
+                        }
+
+                        // Only update if it's changed.
+                        if ($currentvalue !== $value) {
+                            $needsupdate = true;
+                        }
+                    }
+                }
+            }
+
+            if ($needsupdate) {
+                user_update_user($newuser, false, $triggerevent);
+                profile_save_custom_fields($newuser->id, $newprofilefields);
+                return $DB->get_record('user', array('id' => $userid, 'deleted' => 0));
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Return the list of enabled identity providers.
      *
diff --git a/lib/classes/event/role_allow_view_updated.php b/lib/classes/event/role_allow_view_updated.php
new file mode 100644 (file)
index 0000000..1d08df9
--- /dev/null
@@ -0,0 +1,82 @@
+<?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/>.
+
+/**
+ * Role allow view updated event.
+ *
+ * @package    core
+ * @since      Moodle 3.4
+ * @copyright  2017 Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Role allow view updated event class.
+ *
+ * @package    core
+ * @since      Moodle 2.6
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class role_allow_view_updated extends base {
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventroleallowviewupdated', 'role');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated Allow role views.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/roles/allow.php', array('mode' => 'view'));
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'role', 'edit allow view', 'admin/roles/allow.php?mode=view');
+    }
+}
index 9a26281..38c195a 100644 (file)
@@ -157,7 +157,7 @@ abstract class exporter {
                 $formatparams = $this->get_format_parameters($property);
                 $format = $record->$propertyformat;
 
-                list($text, $format) = external_format_text($data->$property, $format, $formatparams['context']->id,
+                list($text, $format) = external_format_text($data->$property, $format, $formatparams['context'],
                     $formatparams['component'], $formatparams['filearea'], $formatparams['itemid'], $formatparams['options']);
 
                 $data->$property = $text;
@@ -168,11 +168,11 @@ abstract class exporter {
 
                 if (!empty($definition['multiple'])) {
                     foreach ($data->$property as $key => $value) {
-                        $data->{$property}[$key] = external_format_string($value, $formatparams['context']->id,
+                        $data->{$property}[$key] = external_format_string($value, $formatparams['context'],
                             $formatparams['striplinks'], $formatparams['options']);
                     }
                 } else {
-                    $data->$property = external_format_string($data->$property, $formatparams['context']->id,
+                    $data->$property = external_format_string($data->$property, $formatparams['context'],
                             $formatparams['striplinks'], $formatparams['options']);
                 }
             }
index 757c2a6..8c71720 100644 (file)
@@ -113,6 +113,9 @@ class curl_security_helper extends curl_security_helper_base {
      * 2. Check the host component against the list of domain names and wildcard domain names.
      *  - This will perform a DNS reverse lookup if required.
      *
+     * The behaviour of this function can be classified as strict, as it returns true for hosts which are invalid or
+     * could not be parsed, as well as those valid URLs which were found in the blacklist.
+     *
      * @param string $host the host component of the URL to check against the blacklist.
      * @return bool true if the host is both valid and blocked, false otherwise.
      */
@@ -132,7 +135,8 @@ class curl_security_helper extends curl_security_helper_base {
 
             // Only perform a reverse lookup if there is a point to it (i.e. we have rules to check against).
             if ($blacklistedhosts['domain'] || $blacklistedhosts['domainwildcard']) {
-                $hostname = gethostbyaddr($host); // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
+                // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
+                $hostname = gethostbyaddr($host);
                 if ($hostname !== $host && $this->host_explicitly_blocked($hostname)) {
                     return true;
                 }
@@ -144,15 +148,39 @@ class curl_security_helper extends curl_security_helper_base {
 
             // Only perform a forward lookup if there are IP rules to check against.
             if ($blacklistedhosts['ipv4'] || $blacklistedhosts['ipv6']) {
-                $hostip = gethostbyname($host); // DNS forward lookup - only returns IPv4 addresses!
-                if ($hostip !== $host && $this->address_explicitly_blocked($hostip)) {
+                // DNS forward lookup - returns a list of only IPv4 addresses!
+                $hostips = $this->get_host_list_by_name($host);
+
+                // If we don't get a valid record, bail (so cURL is never called).
+                if (!$hostips) {
                     return true;
                 }
+
+                // If any of the returned IPs are in the blacklist, block the request.
+                foreach ($hostips as $hostip) {
+                    if ($this->address_explicitly_blocked($hostip)) {
+                        return true;
+                    }
+                }
             }
+        } else {
+            // Was not something we consider to be a valid IP or domain name, block it.
+            return true;
         }
+
         return false;
     }
 
+    /**
+     * Retrieve all hosts for a domain name.
+     *
+     * @param string $param
+     * @return array An array of IPs associated with the host name.
+     */
+    protected function get_host_list_by_name($host) {
+        return ($hostips = gethostbynamel($host)) ? $hostips : [];
+    }
+
     /**
      * Checks whether the given port is blocked, as determined by its absence on the ports whitelist.
      * Ports are assumed to be blocked unless found in the whitelist.
index 22816a1..ddf0aa2 100644 (file)
@@ -266,8 +266,8 @@ function xmldb_main_install() {
 
     // Default allow role matrices.
     foreach ($DB->get_records('role') as $role) {
-        foreach (array('assign', 'override', 'switch') as $type) {
-            $function = 'allow_'.$type;
+        foreach (array('assign', 'override', 'switch', 'view') as $type) {
+            $function = "core_role_set_{$type}_allowed";
             $allows = get_default_role_archetype_allows($type, $role->archetype);
             foreach ($allows as $allowid) {
                 $function($role->id, $allowid);
index 821cf37..593ff37 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20171205" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20171222" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <INDEX NAME="roleid-allowoverride" UNIQUE="true" FIELDS="roleid, allowswitch" COMMENT="Each pair (roleid, allowswitch) must appear at most once."/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="role_allow_view" COMMENT="This table stores which which other roles a user is allowed to view to if they have one role.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The role the user has."/>
+        <FIELD NAME="allowview" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of a role that the user is allowed to view to as a result of having this role."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="roleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/>
+        <KEY NAME="allowview" TYPE="foreign" FIELDS="allowview" REFTABLE="role" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="roleid-allowview" UNIQUE="true" FIELDS="roleid, allowview" COMMENT="Each pair (roleid, allowview) must appear at most once."/>
+      </INDEXES>
+    </TABLE>
     <TABLE NAME="role_assignments" COMMENT="assigning roles in different context">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="timerequested" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time at which this index update was requested."/>
         <FIELD NAME="partialarea" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="If processing of this context partially completed, set to the area that needs processing next. Blank indicates not processed yet."/>
         <FIELD NAME="partialtime" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="If processing partially completed, set to the timestamp within the next area where processing should start. 0 indicates not processed yet."/>
+        <FIELD NAME="indexpriority" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Priority value so that important requests can be dealt with first; higher numbers are processed first"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="indexprioritytimerequested" UNIQUE="false" FIELDS="indexpriority, timerequested"/>
+      </INDEXES>
     </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index 31019f5..504fbfd 100644 (file)
@@ -1859,5 +1859,81 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017121200.00);
     }
 
+    if ($oldversion < 2017121900.00) {
+
+        // Define table role_allow_view to be created.
+        $table = new xmldb_table('role_allow_view');
+
+        // Adding fields to table role_allow_view.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('allowview', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table role_allow_view.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('roleid', XMLDB_KEY_FOREIGN, array('roleid'), 'role', array('id'));
+        $table->add_key('allowview', XMLDB_KEY_FOREIGN, array('allowview'), 'role', array('id'));
+
+        // Conditionally launch create table for role_allow_view.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        $index = new xmldb_index('roleid-allowview', XMLDB_INDEX_UNIQUE, array('roleid', 'allowview'));
+
+        // Conditionally launch add index roleid.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        $roles = $DB->get_records('role', array(), 'sortorder ASC');
+
+        $DB->delete_records('role_allow_view');
+        foreach ($roles as $role) {
+            foreach ($roles as $allowedrole) {
+                $record = new stdClass();
+                $record->roleid      = $role->id;
+                $record->allowview = $allowedrole->id;
+                $DB->insert_record('role_allow_view', $record);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017121900.00);
+    }
+
+    if ($oldversion < 2017122200.01) {
+
+        // Define field indexpriority to be added to search_index_requests. Allow null initially.
+        $table = new xmldb_table('search_index_requests');
+        $field = new xmldb_field('indexpriority', XMLDB_TYPE_INTEGER, '10',
+                null, null, null, null, 'partialtime');
+
+        // Conditionally add field.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+
+            // Set existing values to 'normal' value (100).
+            $DB->set_field('search_index_requests', 'indexpriority', 100);
+
+            // Now make the field 'NOT NULL'.
+            $field = new xmldb_field('indexpriority', XMLDB_TYPE_INTEGER, '10',
+                    null, XMLDB_NOTNULL, null, null, 'partialtime');
+            $dbman->change_field_notnull($table, $field);
+        }
+
+        // Define index indexprioritytimerequested (not unique) to be added to search_index_requests.
+        $index = new xmldb_index('indexprioritytimerequested', XMLDB_INDEX_NOTUNIQUE,
+                array('indexpriority', 'timerequested'));
+
+        // Conditionally launch add index indexprioritytimerequested.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017122200.01);
+    }
+
     return true;
 }
index a71b225..c03a69a 100644 (file)
@@ -194,10 +194,6 @@ class mssql_sql_generator extends sql_generator {
             case XMLDB_TYPE_NUMBER:
                 $dbtype = $this->number_type;
                 if (!empty($xmldb_length)) {
-                    // 38 is the max allowed
-                    if ($xmldb_length > 38) {
-                        $xmldb_length = 38;
-                    }
                     $dbtype .= '(' . $xmldb_length;
                     if (!empty($xmldb_decimals)) {
                         $dbtype .= ',' . $xmldb_decimals;
@@ -548,9 +544,9 @@ class mssql_sql_generator extends sql_generator {
         $fieldname = $xmldb_field->getName();
 
         // Look for any default constraint in this field and drop it
-        if ($default = $this->mdb->get_record_sql("SELECT id, object_name(cdefault) AS defaultconstraint
-                                                     FROM syscolumns
-                                                    WHERE id = object_id(?)
+        if ($default = $this->mdb->get_record_sql("SELECT object_id, object_name(default_object_id) AS defaultconstraint
+                                                     FROM sys.columns
+                                                    WHERE object_id = object_id(?)
                                                           AND name = ?", array($tablename, $fieldname))) {
             return $default->defaultconstraint;
         } else {
@@ -607,7 +603,7 @@ class mssql_sql_generator extends sql_generator {
             case 'fk':
             case 'ck':
                 if ($check = $this->mdb->get_records_sql("SELECT name
-                                                            FROM sysobjects
+                                                            FROM sys.objects
                                                            WHERE lower(name) = ?", array(strtolower($object_name)))) {
                     return true;
                 }
@@ -615,7 +611,7 @@ class mssql_sql_generator extends sql_generator {
             case 'ix':
             case 'uix':
                 if ($check = $this->mdb->get_records_sql("SELECT name
-                                                            FROM sysindexes
+                                                            FROM sys.indexes
                                                            WHERE lower(name) = ?", array(strtolower($object_name)))) {
                     return true;
                 }
index efc610c..1933f69 100644 (file)
@@ -568,9 +568,9 @@ class mysql_sql_generator extends sql_generator {
      * @return array An array of database specific reserved words
      */
     public static function getReservedWords() {
-        // This file contains the reserved words for MySQL databases
-        // from http://dev.mysql.com/doc/refman/6.0/en/reserved-words.html
+        // This file contains the reserved words for MySQL databases.
         $reserved_words = array (
+            // From http://dev.mysql.com/doc/refman/6.0/en/reserved-words.html.
             'accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc',
             'asensitive', 'before', 'between', 'bigint', 'binary',
             'blob', 'both', 'by', 'call', 'cascade', 'case', 'change',
@@ -610,7 +610,13 @@ class mysql_sql_generator extends sql_generator {
             'upgrade', 'usage', 'use', 'using', 'utc_date', 'utc_time',
             'utc_timestamp', 'values', 'varbinary', 'varchar', 'varcharacter',
             'varying', 'when', 'where', 'while', 'with', 'write', 'x509',
-            'xor', 'year_month', 'zerofill'
+            'xor', 'year_month', 'zerofill',
+            // Added in MySQL 8.0, compared to MySQL 5.7:
+            // https://dev.mysql.com/doc/refman/8.0/en/keywords.html#keywords-new-in-current-series.
+            '_filename', 'admin', 'cume_dist', 'dense_rank', 'empty', 'except', 'first_value', 'grouping', 'groups',
+            'json_table', 'lag', 'last_value', 'lead', 'nth_value', 'ntile',
+            'of', 'over', 'percent_rank', 'persist', 'persist_only', 'rank', 'recursive', 'row_number',
+            'system', 'window'
         );
         return $reserved_words;
     }
index 8deccf9..063eddb 100644 (file)
@@ -181,10 +181,6 @@ class oracle_sql_generator extends sql_generator {
             case XMLDB_TYPE_FLOAT:
             case XMLDB_TYPE_NUMBER:
                 $dbtype = $this->number_type;
-                // 38 is the max allowed
-                if ($xmldb_length > 38) {
-                    $xmldb_length = 38;
-                }
                 if (!empty($xmldb_length)) {
                     $dbtype .= '(' . $xmldb_length;
                     if (!empty($xmldb_decimals)) {
index 1fbec98..2f42a62 100644 (file)
@@ -52,6 +52,7 @@ class core_ddl_testcase extends database_driver_testcase {
         $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
         $table->add_field('grade', XMLDB_TYPE_NUMBER, '20,0', null, null, null, null);
         $table->add_field('percent', XMLDB_TYPE_NUMBER, '5,2', null, null, null, 66.6);
+        $table->add_field('bignum', XMLDB_TYPE_NUMBER, '38,18', null, null, null, 1234567890.1234);
         $table->add_field('warnafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
         $table->add_field('blockafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
         $table->add_field('blockperiod', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
@@ -414,10 +415,10 @@ class core_ddl_testcase extends database_driver_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
 
-        // Invalid decimal length.
+        // Invalid decimal length - max precision is 38 digits.
         $table = new xmldb_table('test_table4');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('num', XMLDB_TYPE_NUMBER, '21,10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('num', XMLDB_TYPE_NUMBER, '39,19', null, XMLDB_NOTNULL, null, null);
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $table->setComment("This is a test'n drop table. You can drop it safely");
 
@@ -430,7 +431,7 @@ class core_ddl_testcase extends database_driver_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
 
-        // Invalid decimal decimals.
+        // Invalid decimal decimals - number of decimals can't be higher than total number of digits.
         $table = new xmldb_table('test_table4');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('num', XMLDB_TYPE_NUMBER, '10,11', null, XMLDB_NOTNULL, null, null);
@@ -446,6 +447,38 @@ class core_ddl_testcase extends database_driver_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
 
+        // Invalid decimal whole number - the whole number part can't have more digits than integer fields.
+        $table = new xmldb_table('test_table4');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('num', XMLDB_TYPE_NUMBER, '38,17', null, XMLDB_NOTNULL, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->setComment("This is a test'n drop table. You can drop it safely");
+
+        $this->tables[$table->getName()] = $table;
+
+        try {
+            $dbman->create_table($table);
+            $this->fail('Exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        // Invalid decimal decimals - negative scale not supported.
+        $table = new xmldb_table('test_table4');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('num', XMLDB_TYPE_NUMBER, '30,-5', null, XMLDB_NOTNULL, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->setComment("This is a test'n drop table. You can drop it safely");
+
+        $this->tables[$table->getName()] = $table;
+
+        try {
+            $dbman->create_table($table);
+            $this->fail('Exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
         // Invalid decimal default.
         $table = new xmldb_table('test_table4');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
index e368daf..ade1600 100644 (file)
@@ -10,6 +10,7 @@
         <FIELD NAME="intro" TYPE="text" LENGTH="medium" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="avatar" TYPE="binary" LENGTH="medium" NOTNULL="false" UNSIGNED="false" SEQUENCE="false"/>
         <FIELD NAME="grade" TYPE="number" LENGTH="20" DECIMALS="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="bignum" TYPE="number" LENGTH="38" DECIMALS="18" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="path" TYPE="char" LENGTH="255" NOTNULL="true"/>
       </FIELDS>
       <KEYS>
index 9ef0019..a206596 100644 (file)
@@ -6511,3 +6511,48 @@ function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxeve
 
     return $output;
 }
+
+/**
+ * Creates a record in the role_allow_override table
+ *
+ * @param int $sroleid source roleid
+ * @param int $troleid target roleid
+ * @return void
+ * @deprecated since Moodle 3.4. MDL-50666
+ */
+function allow_override($sroleid, $troleid) {
+    debugging('allow_override() has been deprecated. Please update your code to use core_role_set_override_allowed.',
+            DEBUG_DEVELOPER);
+
+    core_role_set_override_allowed($sroleid, $troleid);
+}
+
+/**
+ * Creates a record in the role_allow_assign table
+ *
+ * @param int $fromroleid source roleid
+ * @param int $targetroleid target roleid
+ * @return void
+ * @deprecated since Moodle 3.4. MDL-50666
+ */
+function allow_assign($fromroleid, $targetroleid) {
+    debugging('allow_assign() has been deprecated. Please update your code to use core_role_set_assign_allowed.',
+            DEBUG_DEVELOPER);
+
+    core_role_set_assign_allowed($fromroleid, $targetroleid);
+}
+
+/**
+ * Creates a record in the role_allow_switch table
+ *
+ * @param int $fromroleid source roleid
+ * @param int $targetroleid target roleid
+ * @return void
+ * @deprecated since Moodle 3.4. MDL-50666
+ */
+function allow_switch($fromroleid, $targetroleid) {
+    debugging('allow_switch() has been deprecated. Please update your code to use core_role_set_switch_allowed.',
+            DEBUG_DEVELOPER);
+
+    core_role_set_switch_allowed($fromroleid, $targetroleid);
+}
index cf55544..2299472 100644 (file)
@@ -481,8 +481,8 @@ class mssql_native_moodle_database extends moodle_database {
                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
                            column_default AS default_value
                       FROM tempdb.INFORMATION_SCHEMA.COLUMNS
-                      JOIN tempdb..sysobjects ON name = table_name
-                     WHERE id = object_id('tempdb..{" . $table . "}')
+                      JOIN tempdb.sys.objects ON name = table_name
+                     WHERE object_id = object_id('tempdb..{" . $table . "}')
                   ORDER BY ordinal_position";
         }
 
index d69094b..1a73b2d 100644 (file)
@@ -544,26 +544,32 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
 /**
  * Returns list of courses current $USER is enrolled in and can access
  *
- * - $fields is an array of field names to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
  *
  * If $allaccessible is true, this will additionally return courses that the current user is not
  * enrolled in, but can access because they are open to the user for other reasons (course view
  * permission, currently viewing course as a guest, or course allows guest access without
  * password).
  *
- * @param string|array $fields
- * @param string $sort
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @param int $limit max number of courses
  * @param array $courseids the list of course ids to filter by
  * @param bool $allaccessible Include courses user is not enrolled in, but can access
  * @return array
  */
-function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
-          $limit = 0, $courseids = [], $allaccessible = false) {
+function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false) {
     global $DB, $USER, $CFG;
 
+    if ($sort === null) {
+        if (empty($CFG->navsortmycoursessort)) {
+            $sort = 'visible DESC, sortorder ASC';
+        } else {
+            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+        }
+    }
+
     // Guest account does not have any enrolled courses.
     if (!$allaccessible && (isguestuser() or !isloggedin())) {
         return array();
@@ -584,7 +590,7 @@ function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder AS
     } else if (is_array($fields)) {
         $fields = array_unique(array_merge($basefields, $fields));
     } else {
-        throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+        throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
     }
     if (in_array('*', $fields)) {
         $fields = array('*');
@@ -788,19 +794,19 @@ function enrol_get_course_description_texts($course) {
 
 /**
  * Returns list of courses user is enrolled into.
- * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
  *
- * - $fields is an array of fieldnames to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
  *
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @return array
  */
-function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
+function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
     global $DB;
 
     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
@@ -877,19 +883,27 @@ function enrol_user_sees_own_courses($user = null) {
 }
 
 /**
- * Returns list of courses user is enrolled into without any capability checks
- * - $fields is an array of fieldnames to ADD
- *   so name the fields you really need, which will
- *   be added and uniq'd
+ * Returns list of courses user is enrolled into without performing any capability checks.
  *
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
  * @return array
  */
-function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
-    global $DB;
+function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
+    global $CFG, $DB;
+
+    if ($sort === null) {
+        if (empty($CFG->navsortmycoursessort)) {
+            $sort = 'visible DESC, sortorder ASC';
+        } else {
+            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+        }
+    }
 
     // Guest account does not have any courses
     if (isguestuser($userid) or empty($userid)) {
@@ -912,7 +926,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NUL
     } else if (is_array($fields)) {
         $fields = array_unique(array_merge($basefields, $fields));
     } else {
-        throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+        throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
     }
     if (in_array('*', $fields)) {
         $fields = array('*');
index e49aaf2..ec42975 100644 (file)
@@ -896,21 +896,25 @@ function external_validate_format($format) {
  * @param string $str The string to be filtered. Should be plain text, expect
  * possibly for multilang tags.
  * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
- * @param int $contextid The id of the context for the string (affects filters).
+ * @param context|int $contextorid The id of the context for the string or the context (affects filters).
  * @param array $options options array/object or courseid
  * @return string text
  * @since Moodle 3.0
  */
-function external_format_string($str, $contextid, $striplinks = true, $options = array()) {
+function external_format_string($str, $contextorid, $striplinks = true, $options = array()) {
 
     // Get settings (singleton).
     $settings = external_settings::get_instance();
-    if (empty($contextid)) {
+    if (empty($contextorid)) {
         throw new coding_exception('contextid is required');
     }
 
     if (!$settings->get_raw()) {
-        $context = context::instance_by_id($contextid);
+        if (is_object($contextorid) && is_a($contextorid, 'context')) {
+            $context = $contextorid;
+        } else {
+            $context = context::instance_by_id($contextorid);
+        }
         $options['context'] = $context;
         $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
         $str = format_string($str, $striplinks, $options);
@@ -944,7 +948,7 @@ function external_format_string($str, $contextid, $striplinks = true, $options =
  *
  * @param string $text The content that may contain ULRs in need of rewriting.
  * @param int $textformat The text format.
- * @param int $contextid This parameter and the next two identify the file area to use.
+ * @param context|int $contextorid This parameter and the next two identify the file area to use.
  * @param string $component
  * @param string $filearea helps identify the file area.
  * @param int $itemid helps identify the file area.
@@ -953,13 +957,21 @@ function external_format_string($str, $contextid, $striplinks = true, $options =
  * @since Moodle 2.3
  * @since Moodle 3.2 component, filearea and itemid are optional parameters
  */
-function external_format_text($text, $textformat, $contextid, $component = null, $filearea = null, $itemid = null,
+function external_format_text($text, $textformat, $contextorid, $component = null, $filearea = null, $itemid = null,
                                 $options = null) {
     global $CFG;
 
     // Get settings (singleton).
     $settings = external_settings::get_instance();
 
+    if (is_object($contextorid) && is_a($contextorid, 'context')) {
+        $context = $contextorid;
+        $contextid = $context->id;
+    } else {
+        $context = null;
+        $contextid = $contextorid;
+    }
+
     if ($component and $filearea and $settings->get_fileurl()) {
         require_once($CFG->libdir . "/filelib.php");
         $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $contextid, $component, $filearea, $itemid);
@@ -979,7 +991,7 @@ function external_format_text($text, $textformat, $contextid, $component = null,
 
         $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
         $options['para'] = isset($options['para']) ? $options['para'] : false;
-        $options['context'] = context::instance_by_id($contextid);
+        $options['context'] = !is_null($context) ? $context : context::instance_by_id($contextid);
         $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
 
         $text = format_text($text, $textformat, $options);
index 6b4f1dc..53f07db 100644 (file)
@@ -904,6 +904,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
 
         $newhashes = array();
         $filecount = 0;
+        $context = context::instance_by_id($contextid, MUST_EXIST);
         foreach ($draftfiles as $file) {
             if (!$options['subdirs'] && $file->get_filepath() !== '/') {
                 continue;
@@ -912,8 +913,11 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 continue;
             }
             if (!$file->is_directory()) {
-                if ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize()) {
-                    // oversized file - should not get here at all
+                // Check to see if this file was uploaded by someone who can ignore the file size limits.
+                $fileusermaxbytes = get_user_max_upload_file_size($context, $options['maxbytes'], 0, 0, $file->get_userid());
+                if ($fileusermaxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS
+                        && ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize())) {
+                    // Oversized file.
                     continue;
                 }
                 if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {
@@ -3027,7 +3031,6 @@ class curl {
      * Set HTTP Request Header
      *
      * @param array $header
-     * @param bool $replace If true, will remove any existing headers before appending the new one.
      */
     public function setHeader($header) {
         if (is_array($header)) {
@@ -3036,7 +3039,10 @@ class curl {
             }
         } else {
             // Remove newlines, they are not allowed in headers.
-            $this->header[] = preg_replace('/[\r\n]/', '', $header);
+            $newvalue = preg_replace('/[\r\n]/', '', $header);
+            if (!in_array($newvalue, $this->header)) {
+                $this->header[] = $newvalue;
+            }
         }
     }
 
index 36f186f..c1a4254 100644 (file)
@@ -304,7 +304,9 @@ class stored_file {
         $filerecord->filesize = $newfile->get_filesize();
         $filerecord->referencefileid = $newfile->get_referencefileid();
         $filerecord->userid = $newfile->get_userid();
+        $oldcontenthash = $this->get_contenthash();
         $this->update($filerecord);
+        $this->filesystem->remove_file($oldcontenthash);
     }
 
     /**
index e1d7944..aeda1d5 100644 (file)
Binary files a/lib/form/amd/build/defaultcustom.min.js and b/lib/form/amd/build/defaultcustom.min.js differ
index 0f280d5..e2e4d11 100644 (file)
@@ -39,6 +39,12 @@ define(['jquery'], function($) {
             form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
             form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
             form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
+        } else if (type === 'date_time_selector') {
+            form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
+            form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
+            form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
+            form.find('[name="' + elementName + '[hour]"]').val(newvalue.hour);
+            form.find('[name="' + elementName + '[minute]"]').val(newvalue.minute);
         }
     };
 
index ab8929a..16a263d 100644 (file)
@@ -84,8 +84,8 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         if (is_array($options)) {
             foreach ($options as $name => $value) {
                 if (array_key_exists($name, $this->_options)) {
-                    if ($name === 'type' && !in_array($value, ['text', 'date_selector'])) {
-                        throw new coding_exception('Only text and date_selector elements are supported in ' . $this->_type);
+                    if ($name === 'type' && !in_array($value, ['text', 'date_selector', 'date_time_selector'])) {
+                        throw new coding_exception('Only text, date_selector, and date_time_selector elements are supported in ' . $this->_type);
                     }
                     if ($name === 'optional' && $value) {
                         throw new coding_exception('Date selector can not be optional in ' . $this->_type);
@@ -105,6 +105,8 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         $calendartype = \core_calendar\type_factory::get_calendar_instance();
         $currentdate = $calendartype->timestamp_to_date_array($value, $this->_options['timezone']);
         return array(
+            'minute' => $currentdate['minutes'],
+            'hour' => $currentdate['hours'],
             'day' => $currentdate['mday'],
             'month' => $currentdate['mon'],
             'year' => $currentdate['year']);
@@ -137,6 +139,9 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         } else if ($this->_options['type'] === 'date_selector') {
             $element = $this->createFormElement($this->_options['type'], 'value', '', $this->_options,
                 $this->getAttributes());
+        } else if ($this->_options['type'] === 'date_time_selector') {
+            $element = $this->createFormElement($this->_options['type'], 'value', '', $this->_options,
+                $this->getAttributes());
         }
         $this->_elements[] = $element;
     }
@@ -184,10 +189,17 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
                 if ($this->has_customize_switch()) {
                     if ($this->_options['type'] === 'text') {
                         $caller->disabledIf($arg[0] . '[value]', $arg[0] . '[customize]', 'notchecked');
+                    } else if ($this->_options['type'] === 'date_selector') {
+                        $caller->disabledIf($arg[0] . '[value][day]', $arg[0]