Merge branch 'MDL-62950-master' of git://github.com/andrewnicols/moodle
authorJun Pataleta <jun@moodle.com>
Fri, 27 Jul 2018 08:19:22 +0000 (16:19 +0800)
committerJun Pataleta <jun@moodle.com>
Fri, 27 Jul 2018 08:19:22 +0000 (16:19 +0800)
105 files changed:
admin/tool/behat/tests/manager_test.php [deleted file]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/my_data_requests_page.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/db/access.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/dataexport.feature [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/policy/classes/output/page_viewalldoc.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/templates/page_viewalldoc.mustache
admin/tool/policy/tests/behat/consent.feature
auth/shibboleth/index_form.html
auth/shibboleth/login.php
backup/util/dbops/restore_dbops.class.php
blocks/myprofile/lang/en/block_myprofile.php
blocks/myprofile/lang/en/deprecated.txt
calendar/amd/build/event_form.min.js
calendar/amd/build/repository.min.js
calendar/amd/src/event_form.js
calendar/amd/src/repository.js
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/forms/eventtype.php
calendar/classes/local/event/forms/managesubscriptions.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/tests/behat/calendar.feature
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
comment/lib.php
course/ajax/management.php
course/classes/management_renderer.php
enrol/manual/amd/build/form-potential-user-selector.min.js
enrol/manual/amd/src/form-potential-user-selector.js
enrol/manual/tests/behat/quickenrolment.feature [new file with mode: 0644]
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/settings.php
lang/en/admin.php
lang/en/calendar.php
lang/en/competency.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/media.php
lang/en/message.php
lang/en/moodle.php
lang/en/webservice.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/behat/classes/behat_config_manager.php
lib/db/caches.php
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/tests/fixtures/disable_control_example.php [new file with mode: 0644]
lib/editor/tests/fixtures/editor_form.php [new file with mode: 0644]
lib/editor/textarea/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/editor/tinymce/module.js
lib/editor/tinymce/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/externallib.php
lib/form/form.js
lib/grouplib.php
lib/password_compat/lib/password.php [deleted file]
lib/questionlib.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_general.php
lib/tests/grouplib_test.php
lib/tests/questionlib_test.php
lib/tests/string_manager_standard_test.php
lib/upgrade.txt
lib/upgradelib.php
mod/assign/lang/en/assign.php
mod/assign/lang/en/deprecated.txt
mod/data/lang/en/data.php
mod/data/lang/en/deprecated.txt
mod/feedback/lang/en/deprecated.txt
mod/feedback/lang/en/feedback.php
mod/feedback/show_nonrespondents.php
mod/forum/post.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/wiki/lib.php
pix/i/empty.png [moved from pix/i/emtpy.png with 100% similarity]
pix/i/empty.svg [moved from pix/i/emtpy.svg with 100% similarity]
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
user/editlib.php
user/lib.php
version.php

diff --git a/admin/tool/behat/tests/manager_test.php b/admin/tool/behat/tests/manager_test.php
deleted file mode 100644 (file)
index 7b88c80..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-<?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/>.
-
-/**
- * Unit tests for behat manager.
- *
- * @package   tool_behat
- * @copyright  2012 David Monllaó
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/' . $CFG->admin .'/tool/behat/locallib.php');
-require_once($CFG->libdir . '/behat/classes/util.php');
-require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
-
-/**
- * Behat manager tests.
- *
- * @package    tool_behat
- * @copyright  2012 David Monllaó
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tool_behat_manager_testcase extends advanced_testcase {
-
-    /**
-     * behat_config_manager tests.
-     */
-    public function test_merge_configs() {
-
-        // Simple default config.
-        $array1 = array(
-            'the' => 'same',
-            'simple' => 'value',
-            'array' => array(
-                'one' => 'arrayvalue1',
-                'two' => 'arrayvalue2'
-            )
-        );
-
-        // Simple override.
-        $array2 = array(
-            'simple' => 'OVERRIDDEN1',
-            'array' => array(
-                'one' => 'OVERRIDDEN2'
-            ),
-            'newprofile' => array(
-                'anotherlevel' => array(
-                    'andanotherone' => array(
-                        'list1',
-                        'list2'
-                    )
-                )
-            )
-        );
-
-        $array = testable_behat_config_manager::merge_config($array1, $array2);
-        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
-
-        // Overrides are applied.
-        $this->assertEquals('OVERRIDDEN1', $array['simple']);
-        $this->assertEquals('OVERRIDDEN2', $array['array']['one']);
-
-        // Other values are respected.
-        $this->assertNotEmpty($array['array']['two']);
-
-        // Completely new nodes are added.
-        $this->assertNotEmpty($array['newprofile']);
-        $this->assertNotEmpty($array['newprofile']['anotherlevel']['andanotherone']);
-        $this->assertEquals('list1', $array['newprofile']['anotherlevel']['andanotherone'][0]);
-        $this->assertEquals('list2', $array['newprofile']['anotherlevel']['andanotherone'][1]);
-
-        // Complex override changing vectors to scalars and scalars to vectors.
-        $array2 = array(
-            'simple' => array(
-                'simple' => 'should',
-                'be' => 'overridden',
-                'by' => 'this-array'
-            ),
-            'array' => 'one'
-        );
-
-        $array = testable_behat_config_manager::merge_config($array1, $array2);
-        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
-
-        // Overrides applied.
-        $this->assertNotEmpty($array['simple']);
-        $this->assertNotEmpty($array['array']);
-        $this->assertTrue(is_array($array['simple']));
-        $this->assertFalse(is_array($array['array']));
-
-        // Other values are maintained.
-        $this->assertEquals('same', $array['the']);
-    }
-
-    /**
-     * behat_config_manager tests.
-     */
-    public function test_config_file_contents() {
-        global $CFG;
-
-        $this->resetAfterTest(true);
-
-        // Skip tests if behat is not installed.
-        $vendorpath = $CFG->dirroot . '/vendor';
-        if (!file_exists($vendorpath . '/autoload.php') || !is_dir($vendorpath . '/behat')) {
-            $this->markTestSkipped('Behat not installed.');
-        }
-
-        // Add some fake test url.
-        $CFG->behat_wwwroot = 'http://example.com/behat';
-
-        // To avoid user value at config.php level.
-        unset($CFG->behat_config);
-
-        // List.
-        $features = array(
-            'feature1',
-            'feature2',
-            'feature3'
-        );
-
-        // Associative array.
-        $stepsdefinitions = array(
-            'micarro' => '/me/lo/robaron',
-            'anoche' => '/cuando/yo/dormia'
-        );
-
-        $contents = testable_behat_config_manager::get_config_file_contents($features, $stepsdefinitions);
-        $this->assertDebuggingCalled("Use of get_config_file_contents is deprecated, please see behat_config_util");
-
-        // YAML decides when is is necessary to wrap strings between single quotes, so not controlled
-        // values like paths should not be asserted including the key name as they would depend on the
-        // directories values.
-        $this->assertContains($CFG->dirroot,
-            $contents['default']['extensions']['Moodle\BehatExtension']['moodledirroot']);
-
-        // Not quoted strings.
-        $this->assertEquals('/me/lo/robaron',
-            $contents['default']['extensions']['Moodle\BehatExtension']['steps_definitions']['micarro']);
-
-        // YAML uses single quotes to wrap URL strings.
-        $this->assertEquals($CFG->behat_wwwroot, $contents['default']['extensions']['Behat\MinkExtension']['base_url']);
-
-        // Lists.
-        $this->assertEquals('feature1', $contents['default']['suites']['default']['paths'][0]);
-        $this->assertEquals('feature3', $contents['default']['suites']['default']['paths'][2]);
-
-        unset($CFG->behat_wwwroot);
-    }
-
-}
-
-/**
- * Allows access to internal methods without exposing them.
- *
- * @package    tool_behat
- * @copyright  2012 David Monllaó
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class testable_behat_config_manager extends behat_config_manager {
-
-    /**
-     * Allow access to protected method
-     * @see parent::merge_config()
-     * @param mixed $config
-     * @param mixed $localconfig
-     * @return mixed
-     */
-    public static function merge_config($config, $localconfig) {
-        return parent::merge_config($config, $localconfig);
-    }
-
-    /**
-     * Allow access to protected method
-     * @see parent::get_config_file_contents()
-     * @param array $features
-     * @param array $stepsdefinitions
-     * @return string
-     */
-    public static function get_config_file_contents($features, $stepsdefinitions) {
-        return parent::get_config_file_contents($features, $stepsdefinitions);
-    }
-}
index 9aaea28..8ec0bb5 100644 (file)
@@ -608,6 +608,54 @@ class api {
         return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
     }
 
+    /**
+     * Checks whether a user can download a data request.
+     *
+     * @param int $userid Target user id (subject of data request)
+     * @param int $requesterid Requester user id (person who requsted it)
+     * @param int|null $downloaderid Person who wants to download user id (default current)
+     * @return bool
+     * @throws coding_exception
+     */
+    public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
+        global $USER;
+
+        if (!$downloaderid) {
+            $downloaderid = $USER->id;
+        }
+
+        $usercontext = \context_user::instance($userid);
+        // If it's your own and you have the right capability, you can download it.
+        if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
+            return true;
+        }
+        // If you can download anyone's in that context, you can download it.
+        if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
+            return true;
+        }
+        // If you can have the 'child access' ability to request in that context, and you are the one
+        // who requested it, then you can download it.
+        if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets an action menu link to download a data request.
+     *
+     * @param \context_user $usercontext User context (of user who the data is for)
+     * @param int $requestid Request id
+     * @return \action_menu_link_secondary Action menu link
+     * @throws coding_exception
+     */
+    public static function get_download_link(\context_user $usercontext, $requestid) {
+        $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
+                'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
+        $downloadtext = get_string('download', 'tool_dataprivacy');
+        return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
+    }
+
     /**
      * Creates a new data purpose.
      *
index d8b0644..51bb133 100644 (file)
@@ -208,6 +208,14 @@ class data_requests_table extends table_sql {
                 break;
         }
 
+        if ($status == api::DATAREQUEST_STATUS_COMPLETE) {
+            $userid = $data->foruser->id;
+            $usercontext = \context_user::instance($userid, IGNORE_MISSING);
+            if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
+                $actions[] = api::get_download_link($usercontext, $requestid);
+            }
+        }
+
         $actionsmenu = new action_menu($actions);
         $actionsmenu->set_menu_trigger(get_string('actions'));
         $actionsmenu->set_owner_selector('request-actions-' . $requestid);
index c5e18a1..d82968c 100644 (file)
@@ -95,7 +95,8 @@ class my_data_requests_page implements renderable, templatable {
             $requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
             $item = $requestexporter->export($output);
 
-            if ($request->get('userid') != $USER->id) {
+            $self = $request->get('userid') == $USER->id;
+            if (!$self) {
                 // Append user name if it differs from $USER.
                 $a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
                 $item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
@@ -110,6 +111,10 @@ class my_data_requests_page implements renderable, templatable {
                     $cancancel = false;
                     // Show download links only for export-type data requests.
                     $candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
+                    if ($usercontext) {
+                        $candownload = api::can_download_data_request_for_user(
+                                $request->get('userid'), $request->get('requestedby'));
+                    }
                     break;
                 case api::DATAREQUEST_STATUS_CANCELLED:
                 case api::DATAREQUEST_STATUS_REJECTED:
@@ -126,10 +131,7 @@ class my_data_requests_page implements renderable, templatable {
                 $actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata);
             }
             if ($candownload && $usercontext) {
-                $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/',
-                        'export.zip', true);
-                $downloadtext = get_string('download', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext);
+                $actions[] = api::get_download_link($usercontext, $requestid);
             }
             if (!empty($actions)) {
                 $actionsmenu = new action_menu($actions);
index c58f574..6db9252 100644 (file)
@@ -139,8 +139,17 @@ class process_data_request_task extends adhoc_task {
 
         $output = $PAGE->get_renderer('tool_dataprivacy');
         $emailonly = false;
+        $notifyuser = true;
         switch ($request->type) {
             case api::DATAREQUEST_TYPE_EXPORT:
+                // Check if the user is allowed to download their own export. (This is for
+                // institutions which centrally co-ordinate subject access request across many
+                // systems, not just one Moodle instance, so we don't want every instance emailing
+                // the user.)
+                if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
+                    $notifyuser = false;
+                }
+
                 $typetext = get_string('requesttypeexport', 'tool_dataprivacy');
                 // We want to notify the user in Moodle about the processing results.
                 $message->notification = 1;
@@ -179,18 +188,40 @@ class process_data_request_task extends adhoc_task {
         $message->fullmessagehtml = $messagehtml;
 
         // Send message to the user involved.
-        if ($emailonly) {
-            email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
-        } else {
-            message_send($message);
+        if ($notifyuser) {
+            if ($emailonly) {
+                email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
+            } else {
+                message_send($message);
+            }
+            mtrace('Message sent to user: ' . $messagetextdata['username']);
         }
-        mtrace('Message sent to user: ' . $messagetextdata['username']);
 
-        // Send to requester as well if this request was made on behalf of another user who's not a DPO,
-        // and has the capability to make data requests for the user (e.g. Parent).
-        if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
+        // Send to requester as well in some circumstances.
+        if ($foruser->id != $request->requestedby) {
+            $sendtorequester = false;
+            switch ($request->type) {
+                case api::DATAREQUEST_TYPE_EXPORT:
+                    // Send to the requester as well if they can download it, unless they are the
+                    // DPO. If we didn't notify the user themselves (because they can't download)
+                    // then send to requester even if it is the DPO, as in that case the requester
+                    // needs to take some action.
+                    if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
+                        $sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
+                    }
+                    break;
+                case api::DATAREQUEST_TYPE_DELETE:
+                    // Send to the requester if they are not the DPO and if they are allowed to
+                    // create data requests for the user (e.g. Parent).
+                    $sendtorequester = !api::is_site_dpo($request->requestedby) &&
+                            api::can_create_data_request_for_user($request->userid, $request->requestedby);
+                    break;
+                default:
+                    throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+            }
+
             // Ensure the requester has the capability to make data requests for this user.
-            if (api::can_create_data_request_for_user($request->userid, $request->requestedby)) {
+            if ($sendtorequester) {
                 $requestedby = core_user::get_user($request->requestedby);
                 $message->userto = $requestedby;
                 $messagetextdata['username'] = fullname($requestedby);
index ecc2ec3..ad31867 100644 (file)
@@ -49,4 +49,22 @@ $capabilities = [
         'contextlevel' => CONTEXT_USER,
         'archetypes' => []
     ],
+
+    // Capability for users to download the results of their own data request.
+    'tool/dataprivacy:downloadownrequest' => [
+        'riskbitmask' => 0,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => [
+            'user' => CAP_ALLOW
+        ]
+    ],
+
+    // Capability for administrators to download other people's data requests.
+    'tool/dataprivacy:downloadallrequests' => [
+        'riskbitmask' => RISK_PERSONAL,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => []
+    ],
 ];
index b01a0b6..4af32a8 100644 (file)
@@ -66,6 +66,8 @@ $string['datadeletionpagehelp'] = 'Data for which the retention period has expir
 $string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
 $string['dataprivacy:managedatarequests'] = 'Manage data requests';
 $string['dataprivacy:managedataregistry'] = 'Manage data registry';
+$string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
+$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
 $string['dataregistry'] = 'Data registry';
 $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
index 3c66485..73ffc14 100644 (file)
@@ -185,26 +185,18 @@ function tool_dataprivacy_output_fragment_contextlevel_form($args) {
  * @return bool Returns false if we don't find a file.
  */
 function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
-    global $USER;
-
     if ($context->contextlevel == CONTEXT_USER) {
         // Make sure the user is logged in.
         require_login(null, false);
 
-        // Validate the user downloading this archive.
-        $usercontext = context_user::instance($USER->id);
-        // The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent).
-        if ($usercontext->instanceid !== $context->instanceid) {
-            // Get the data request ID. This should be the first element of the $args array.
-            $itemid = $args[0];
-            // Fetch the data request object. An invalid ID will throw an exception.
-            $datarequest = new \tool_dataprivacy\data_request($itemid);
-
-            // Check if the user is the requester and has the capability to make data requests for the target user.
-            $candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context);
-            if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) {
-                return false;
-            }
+        // Get the data request ID. This should be the first element of the $args array.
+        $itemid = $args[0];
+        // Fetch the data request object. An invalid ID will throw an exception.
+        $datarequest = new \tool_dataprivacy\data_request($itemid);
+
+        // Check if user is allowed to download it.
+        if (!\tool_dataprivacy\api::can_download_data_request_for_user($context->instanceid, $datarequest->get('requestedby'))) {
+            return false;
         }
 
         // All good. Serve the exported data.
index f4a7a66..c341a63 100644 (file)
@@ -276,6 +276,57 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
     }
 
+    /**
+     * Test for api::can_download_data_request_for_user()
+     */
+    public function test_can_download_data_request_for_user() {
+        $generator = $this->getDataGenerator();
+
+        // Three victims.
+        $victim1 = $generator->create_user();
+        $victim2 = $generator->create_user();
+        $victim3 = $generator->create_user();
+
+        // Assign a user as victim 1's parent.
+        $systemcontext = \context_system::instance();
+        $parentrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
+        $parent = $generator->create_user();
+        role_assign($parentrole, $parent->id, \context_user::instance($victim1->id));
+
+        // Assign another user as data access wonder woman.
+        $wonderrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext);
+        $staff = $generator->create_user();
+        role_assign($wonderrole, $staff->id, $systemcontext);
+
+        // Finally, victim 3 has been naughty; stop them accessing their own data.
+        $naughtyrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext);
+        role_assign($naughtyrole, $victim3->id, $systemcontext);
+
+        // Victims 1 and 2 can access their own data, regardless of who requested it.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id));
+
+        // Victim 3 cannot access his own data.
+        $this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id));
+
+        // Victims 1 and 2 cannot access another victim's data.
+        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id));
+
+        // Staff can access everyone's data.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id));
+
+        // Parent can access victim 1's data only if they requested it.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id));
+    }
+
     /**
      * Test for api::create_data_request()
      */
@@ -417,27 +468,20 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      * @return array
      */
     public function get_data_requests_provider() {
-        $generator = new testing_data_generator();
-        $user1 = $generator->create_user();
-        $user2 = $generator->create_user();
-        $user3 = $generator->create_user();
-        $user4 = $generator->create_user();
-        $user5 = $generator->create_user();
-        $users = [$user1, $user2, $user3, $user4, $user5];
         $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
         $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
 
         return [
             // Own data requests.
-            [$users, $user1, false, $completeonly],
+            ['user', false, $completeonly],
             // Non-DPO fetching all requets.
-            [$users, $user2, true, $completeonly],
+            ['user', true, $completeonly],
             // Admin fetching all completed and cancelled requests.
-            [$users, get_admin(), true, $completeandcancelled],
+            ['dpo', true, $completeandcancelled],
             // Admin fetching all completed requests.
-            [$users, get_admin(), true, $completeonly],
+            ['dpo', true, $completeonly],
             // Guest fetching all requests.
-            [$users, guest_user(), true, $completeonly],
+            ['guest', true, $completeonly],
         ];
     }
 
@@ -445,12 +489,31 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      * Test for api::get_data_requests()
      *
      * @dataProvider get_data_requests_provider
-     * @param stdClass[] $users Array of users to create data requests for.
-     * @param stdClass $loggeduser The user logging in.
+     * @param string $usertype The type of the user logging in.
      * @param boolean $fetchall Whether to fetch all records.
      * @param int[] $statuses Status filters.
      */
-    public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
+    public function test_get_data_requests($usertype, $fetchall, $statuses) {
+        $generator = new testing_data_generator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $user3 = $generator->create_user();
+        $user4 = $generator->create_user();
+        $user5 = $generator->create_user();
+        $users = [$user1, $user2, $user3, $user4, $user5];
+
+        switch ($usertype) {
+            case 'user':
+                $loggeduser = $user1;
+                break;
+            case 'dpo':
+                $loggeduser = get_admin();
+                break;
+            case 'guest':
+                $loggeduser = guest_user();
+                break;
+        }
+
         $comment = 'Data %s request comment by user %d';
         $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
         $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
diff --git a/admin/tool/dataprivacy/tests/behat/dataexport.feature b/admin/tool/dataprivacy/tests/behat/dataexport.feature
new file mode 100644 (file)
index 0000000..3ab0467
--- /dev/null
@@ -0,0 +1,107 @@
+@tool @tool_dataprivacy
+Feature: Data export from the privacy API
+  In order to export data for users and meet legal requirements
+  As an admin, user, or parent
+  I need to be able to export data for a user
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname      | lastname |
+      | victim   | Victim User    | 1        |
+      | parent   | Long-suffering | Parent   |
+    And the following "roles" exist:
+      | shortname | name  | archetype |
+      | tired     | Tired |           |
+    And the following "permission overrides" exist:
+      | capability                                   | permission | role  | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+    And the following "role assigns" exist:
+      | user   | role  | contextlevel | reference |
+      | parent | tired | User         | victim    |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+
+  @javascript
+  Scenario: As admin, export data for a user and download it
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
+
+  @javascript
+  Scenario: As a student, request data export and then download it when approved
+    Given I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I press "Save changes"
+    Then I should see "Export all of my personal data"
+    And I should see "Pending" in the "Export all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Export all of my personal data" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Export all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Export all of my personal data" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
+
+  @javascript
+  Scenario: As a parent, request data export for my child because I don't trust the little blighter
+    Given I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
index 01b8b2b..f2cf6d1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018051402;
+$plugin->version   = 2018051403;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 5ca5d99..66ea3aa 100644 (file)
@@ -102,6 +102,7 @@ class page_viewalldoc implements renderable, templatable {
 
         array_walk($data->policies, function($item, $key) {
             $item->policytypestr = get_string('policydoctype'.$item->type, 'tool_policy');
+            $item->policyaudiencestr = get_string('policydocaudience'.$item->audience, 'tool_policy');
         });
 
         return $data;
index fd29175..3a75285 100644 (file)
@@ -84,6 +84,7 @@ $string['inactivatingconfirm'] = '<p>You are about to inactivate policy <em>\'{$
 $string['inactivatingconfirmyes'] = 'Inactivate';
 $string['invalidversionid'] = 'There is no policy with this identifier!';
 $string['irevokethepolicy'] = 'Withdraw user consent';
+$string['listactivepolicies'] = 'List of active policies';
 $string['minorchange'] = 'Minor change';
 $string['minorchangeinfo'] = 'A minor change does not alter the meaning of the policy. Users are not required to agree to the policy again if the edit is marked as a minor change.';
 $string['managepolicies'] = 'Manage policies';
index 89812ff..2f9df36 100644 (file)
                 "name": "Terms &amp; conditions",
                 "summary": "Policy <u>summary</u>",
                 "content": "Policy <em>content</em>",
-                "policytypestr": "Site policy"
+                "policytypestr": "Site policy",
+                "policyaudiencestr": "All users"
             },
             {
                 "id": "5",
                 "name": "Privacy",
                 "summary": "We keep your information private",
                 "content": "Very private",
-                "policytypestr": "Privacy policy"
+                "policytypestr": "Privacy policy",
+                "policyaudiencestr": "Authenticated users"
             }
         ]
     }
 }}
 
 <a id="top"></a>
-<div id="policies_index" class="m-b-3">
-<ul>
+<div id="policies_index">
+<h1>{{# str }} listactivepolicies, tool_policy {{/ str }}</h1>
+<table class="table">
+    <thead>
+    <tr>
+        <th scope="col">{{# str }}policydocname, tool_policy {{/ str }}</th>
+        <th scope="col">{{# str }}policydoctype, tool_policy {{/ str }}</th>
+        <th scope="col">{{# str }}policydocaudience, tool_policy {{/ str }}</th>
+    </tr>
+    </thead>
+    <tbody>
     {{#policies }}
-        <li><a href="#policy-{{id}}">{{{name}}} ({{{policytypestr}}})</a></li>
+        <tr>
+            <td><a href="#policy-{{id}}">{{{name}}}</a></td>
+            <td>{{{ policytypestr }}}</td>
+            <td>{{{ policyaudiencestr }}}</td>
+        </tr>
     {{/policies }}
-</ul>
+    </tbody>
+</table>
 </div>
 
 {{^policies }}
         <hr>
     <div class="policy_version m-b-3">
         <div class="clearfix m-t-2">
-            <h1><a id="policy-{{id}}">{{{name}}}</a></h1>
+            <h2><a id="policy-{{id}}">{{{name}}}</a></h2>
         </div>
         <div class="policy_document_summary clearfix m-b-1">
-            <h2>{{# str }} policydocsummary, tool_policy {{/ str }}</h2>
+            <h3>{{# str }} policydocsummary, tool_policy {{/ str }}</h3>
             {{{summary}}}
         </div>
         <div class="policy_document_content m-t-2">
-            <h2>{{# str }} policydoccontent, tool_policy {{/ str }}</h2>
+            <h3>{{# str }} policydoccontent, tool_policy {{/ str }}</h3>
             {{{content}}}
         </div>
         <div class="pull-right">
index 6a5f4c0..2b9b026 100644 (file)
@@ -460,6 +460,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
       | This privacy policy | 1    |          | full text3 | short text3 | active   | loggedin |
       | This guests policy  | 0    |          | full text4 | short text4 | active   | guest    |
     And I am on site homepage
+    And I change window size to "large"
     And I follow "Log in"
     When I press "Log in as a guest"
     Then I should see "If you continue browsing this website, you agree to our policies"
index 4d7e7df..144c1fd 100644 (file)
@@ -71,8 +71,8 @@ if ($show_instructions) {
 <?php     if (is_enabled_auth('none')) { // instructions override the rest for security reasons
               print_string("loginstepsnone");
           } else if ($CFG->registerauth == 'email') {
-              if (!empty($CFG->auth_instructions)) {
-                  echo format_text($CFG->auth_instructions);
+              if (!empty($config->auth_instructions)) {
+                  echo format_text($config->auth_instructions);
               } else {
                   print_string("loginsteps", "", "signup.php");
               } ?>
@@ -82,14 +82,14 @@ if ($show_instructions) {
                    </form>
                  </div>
 <?php     } else if (!empty($CFG->registerauth)) {
-              echo format_text($CFG->auth_instructions); ?>
+              echo format_text($config->auth_instructions); ?>
               <div class="signupform">
                 <form action="../../login/signup.php" method="get" id="signup">
                 <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
                 </form>
               </div>
 <?php     } else {
-              echo format_text($CFG->auth_instructions);
+              echo format_text($config->auth_instructions);
           } ?>
       </div>
     </div>
index 721af80..2ed931a 100644 (file)
 
     $loginurl = (!empty($CFG->alternateloginurl)) ? $CFG->alternateloginurl : '';
 
-
-    if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($CFG->auth_instructions)) {
+    $config = get_config('auth_shibboleth');
+    if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) {
         $show_instructions = true;
     } else {
         $show_instructions = false;
     }
 
-    // Set SAML domain cookie
-    $config = get_config('auth_shibboleth');
-
-
     $IdPs = get_idp_list($config->organization_selection);
     if (isset($_POST['idp']) && isset($IdPs[$_POST['idp']])){
         $selectedIdP = $_POST['idp'];
index 9992713..7ef9117 100644 (file)
@@ -723,7 +723,7 @@ abstract class restore_dbops {
 
             // 8) Check if backup is made on Moodle >= 3.5 and there are more than one top-level category in the context.
             if ($after35 && $topcats > 1) {
-                $errors[] = get_string('restoremultipletopcats', 'questions', $contextid);
+                $errors[] = get_string('restoremultipletopcats', 'question', $contextid);
             }
 
         }
index b7de027..5d886c2 100644 (file)
@@ -46,6 +46,3 @@ $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboar
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
 $string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
-
-// Deprecated since Moodle 3.2.
-$string['display_un'] = 'Display name';
index 0abfe87..0d3bc5d 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index 6b1db42..894dcf9 100644 (file)
Binary files a/calendar/amd/build/repository.min.js and b/calendar/amd/build/repository.min.js differ
index 2b6707d..b64639a 100644 (file)
  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery'], function($) {
+define(['jquery', 'core_calendar/repository'], function($, CalendarRepository) {
 
     var SELECTORS = {
         EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
         EVENT_GROUP_ID: '[name="groupid"]',
-        SELECT_OPTION: 'option',
-    };
-
-    /**
-     * Parse the group id select element in the event form and pull out
-     * the course id from the value to allow us to toggle other select
-     * elements based on the course id for the group a user selects.
-     *
-     * This is a little hacky but I couldn't find a better way to pass
-     * the course id for each group id with the limitations of mforms.
-     *
-     * The group id options are rendered with a value like:
-     * "<courseid>-<groupid>"
-     * E.g.
-     * For a group with id 10 in a course with id 3 the value of the
-     * option will be 3-10.
-     *
-     * @method parseGroupSelect
-     * @param {object} formElement The root form element
-     */
-    var parseGroupSelect = function(formElement) {
-        formElement.find(SELECTORS.EVENT_GROUP_ID)
-            .find(SELECTORS.SELECT_OPTION)
-            .each(function(index, element) {
-                element = $(element);
-                var value = element.attr('value');
-                var splits = value.split('-');
-                var courseId = splits[0];
-
-                element.attr('data-course-id', courseId);
-            });
+        SELECT_OPTION: 'option'
     };
 
     /**
@@ -69,39 +39,29 @@ define(['jquery'], function($) {
      */
     var addCourseGroupSelectListeners = function(formElement) {
         var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);
-        var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID);
-        var groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION);
-        var filterGroupSelectOptions = function() {
-            var selectedCourseId = courseGroupSelect.val();
-            var selectedIndex = null;
-            var hasGroups = false;
-            groupSelectOptions.each(function(index, element) {
-                element = $(element);
 
-                if (element.attr('data-course-id') == selectedCourseId) {
-                    element.removeClass('hidden');
-                    element.prop('disabled', false);
-                    hasGroups = true;
-                    if (selectedIndex === null || element.attr('selected')) {
-                        selectedIndex = index;
-                    }
-                } else {
-                    element.addClass('hidden');
-                    element.prop('disabled', true);
-                }
-            });
+        var loadGroupSelectOptions = function(groups) {
+            var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID),
+                groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION),
+                courseGroups = $(groups);
 
-            if (hasGroups) {
-                groupSelect.prop('disabled', false);
-            } else {
-                groupSelect.prop('disabled', true);
-            }
-
-            groupSelect.prop('selectedIndex', selectedIndex);
+            // Let's clear all options first.
+            groupSelectOptions.remove();
+            groupSelect.prop("disabled", false);
+            courseGroups.each(function(id, group) {
+                $(groupSelect).append($("<option></option>").attr("value", group.id).text(group.name));
+            });
         };
 
-        courseGroupSelect.on('change', filterGroupSelectOptions);
-        filterGroupSelectOptions();
+        // If the user choose a course in the selector do a WS request to get groups.
+        courseGroupSelect.on('change', function() {
+            var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();
+            CalendarRepository.getCourseGroupsData(courseId)
+                .then(function(groups) {
+                    return loadGroupSelectOptions(groups);
+                })
+                .catch(Notification.exception);
+        });
     };
 
     /**
@@ -112,8 +72,6 @@ define(['jquery'], function($) {
      */
     var init = function(formId) {
         var formElement = $('#' + formId);
-
-        parseGroupSelect(formElement);
         addCourseGroupSelectListeners(formElement);
     };
 
index 049f819..d8166fa 100644 (file)
@@ -181,6 +181,23 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Get the groups by course id.
+     *
+     * @param {Number} courseid The course id to fetch the groups from.
+     * @return {promise} Resolved with the course groups.
+     */
+    var getCourseGroupsData = function(courseid) {
+        var request = {
+            methodname: 'core_group_get_course_groups',
+            args: {
+                courseid: courseid
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     return {
         getEventById: getEventById,
         deleteEvent: deleteEvent,
@@ -188,6 +205,7 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         submitCreateUpdateForm: submitCreateUpdateForm,
         getCalendarMonthData: getCalendarMonthData,
         getCalendarDayData: getCalendarDayData,
-        getCalendarUpcomingData: getCalendarUpcomingData
+        getCalendarUpcomingData: getCalendarUpcomingData,
+        getCourseGroupsData: getCourseGroupsData
     };
 });
index 924a6fa..ff9407b 100644 (file)
@@ -66,9 +66,11 @@ class create extends \moodleform {
         $mform = $this->_form;
         $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0;
         $editoroptions = !(empty($this->_customdata['editoroptions'])) ? $this->_customdata['editoroptions'] : null;
-        $eventtypes = calendar_get_all_allowed_types();
+        $courseid = !(empty($this->_customdata['courseid'])) ? $this->_customdata['courseid'] : null;
 
-        if (empty($eventtypes)) {
+        $eventtypes = calendar_get_allowed_event_types($courseid);
+
+        if (in_array(true, $eventtypes, true) === false) {
             print_error('nopermissiontoupdatecalendar');
         }
 
@@ -120,18 +122,20 @@ class create extends \moodleform {
      * @return array
      */
     public function validation($data, $files) {
-        global $DB, $CFG;
+        global $DB;
 
         $errors = parent::validation($data, $files);
-        $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
         $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
-        if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $eventtypes = calendar_get_allowed_event_types($courseid);
+
+        if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
         }
 
-        if (isset($data[$coursekey]) && $data[$coursekey] > 0) {
-            if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
+        if ($courseid && $courseid > 0) {
+            if ($course = $DB->get_record('course', ['id' => $courseid])) {
                 if ($data['timestart'] < $course->startdate) {
                     $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar');
                 }
@@ -140,11 +144,15 @@ class create extends \moodleform {
             }
         }
 
-        if ($eventtype == 'course' && empty($data['courseid'])) {
+        if ($eventtype == 'course' && empty($courseid)) {
             $errors['courseid'] = get_string('selectacourse');
         }
 
-        if ($eventtype == 'group' && empty($data['groupcourseid'])) {
+        if ($eventtype == 'group' && (!empty($courseid) && empty($data['groupid']))) {
+            $errors['groupcourseid'] = get_string('nogroups', 'core_group');
+        }
+
+        if ($eventtype == 'group' && empty($courseid)) {
             $errors['groupcourseid'] = get_string('selectacourse');
         }
 
index 59a9cd7..f848687 100644 (file)
@@ -53,28 +53,29 @@ trait eventtype {
      * @param array $eventtypes The available event types for the user
      */
     protected function add_event_type_elements($mform, $eventtypes) {
+        global $CFG, $DB;
         $options = [];
 
-        if (isset($eventtypes['user'])) {
+        if (!empty($eventtypes['user'])) {
             $options['user'] = get_string('user');
         }
-        if (isset($eventtypes['group'])) {
+        if (!empty($eventtypes['group'])) {
             $options['group'] = get_string('group');
         }
-        if (isset($eventtypes['course'])) {
+        if (!empty($eventtypes['course'])) {
             $options['course'] = get_string('course');
         }
-        if (isset($eventtypes['category'])) {
+        if (!empty($eventtypes['category'])) {
             $options['category'] = get_string('category');
         }
-        if (isset($eventtypes['site'])) {
+        if (!empty($eventtypes['site'])) {
             $options['site'] = get_string('site');
         }
 
         // If we only have one event type and it's 'user' event then don't bother
         // rendering the select boxes because there is no choice for the user to
         // make.
-        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+        if (!empty($eventtypes['user']) && count($options) == 1) {
             $mform->addElement('hidden', 'eventtype');
             $mform->setType('eventtype', PARAM_TEXT);
             $mform->setDefault('eventtype', 'user');
@@ -87,9 +88,9 @@ trait eventtype {
             $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
         }
 
-        if (isset($eventtypes['category'])) {
+        if (!empty($eventtypes['category'])) {
             $categoryoptions = [];
-            foreach ($eventtypes['category'] as $id => $category) {
+            foreach (\coursecat::make_categories_list('moodle/category:manage') as $id => $category) {
                 $categoryoptions[$id] = $category;
             }
 
@@ -97,33 +98,26 @@ trait eventtype {
             $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
         }
 
-        if (isset($eventtypes['course'])) {
-            $limit = !has_capability('moodle/calendar:manageentries', \context_system::instance());
-            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
+        $showall = $CFG->calendar_adminseesall && !has_capability('moodle/calendar:manageentries', \context_system::instance());
+        if (!empty($eventtypes['course'])) {
+            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => !$showall]);
             $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
         }
 
-        if (isset($eventtypes['group'])) {
-            $options = ['limittoenrolled' => true];
-            // Exclude courses without group.
-            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
-                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
-                    array_keys($eventtypes['groupcourses']));
-            }
-
-            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
+        if (!empty($eventtypes['group'])) {
+            $groups = !(empty($this->_customdata['groups'])) ? $this->_customdata['groups'] : null;
+            // Get the list of courses without groups to filter on the course selector.
+            $sql = "SELECT c.id
+                      FROM {course} c
+                     WHERE c.id NOT IN (
+                            SELECT DISTINCT courseid FROM {groups}
+                           )";
+            $coursesnogroup = $DB->get_records_sql($sql);
+            $mform->addElement('course', 'groupcourseid', get_string('course'),  ['limittoenrolled' => !$showall,
+                    'exclude' => array_keys($coursesnogroup)]);
             $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
 
-            $groupoptions = [];
-            foreach ($eventtypes['group'] as $group) {
-                // We are formatting it this way in order to provide the javascript both
-                // the course and group ids so that it can enhance the form for the user.
-                $index = "{$group->courseid}-{$group->id}";
-                $groupoptions[$index] = format_string($group->name, true,
-                    ['context' => \context_course::instance($group->courseid)]);
-            }
-
-            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->addElement('select', 'groupid', get_string('group'), $groups);
             $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
             // We handle the group select hide/show actions on the event_form module.
         }
index 0e5a0b6..102761f 100644 (file)
@@ -41,8 +41,8 @@ class managesubscriptions extends \moodleform {
      */
     public function definition() {
         $mform = $this->_form;
-        $eventtypes = calendar_get_all_allowed_types();
-        if (empty($eventtypes)) {
+        $eventtypes = calendar_get_allowed_event_types();
+        if (in_array(true, $eventtypes, true) === false) {
             print_error('nopermissiontoupdatecalendar');
         }
 
@@ -100,9 +100,10 @@ class managesubscriptions extends \moodleform {
 
         $errors = parent::validation($data, $files);
 
-        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
-        $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $eventtypes = calendar_get_allowed_event_types($courseid);
 
         if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
index 91c1435..032d839 100644 (file)
@@ -58,7 +58,7 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
 
         if ($legacyevent->eventtype == 'group') {
             // Set up the correct value for the to display on the form.
-            $data->groupid = "{$legacyevent->courseid}-{$legacyevent->groupid}";
+            $data->groupid = $legacyevent->groupid;
             $data->groupcourseid = $legacyevent->courseid;
         }
         if ($legacyevent->eventtype == 'course') {
@@ -93,12 +93,8 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
                 $properties->courseid = $data->groupcourseid;
                 unset($properties->groupcourseid);
             }
-
-            // Pull the group id back out of the value. The form saves the value
-            // as "<courseid>-<groupid>" to allow the javascript to work correctly.
             if (isset($data->groupid)) {
-                list($courseid, $groupid) = explode('-', $data->groupid);
-                $properties->groupid = $groupid;
+                $properties->groupid = $data->groupid;
             }
         } else {
             // Default course id if none is set.
index d4775ee..5b863b2 100644 (file)
@@ -170,7 +170,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
         $subqueryconditions = [];
 
         // Get the user's courses. Otherwise, get the default courses being shown by the calendar.
-        $usercourses = calendar_get_default_courses();
+        $usercourses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
 
         // Set calendar filters.
         list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true);
index 10fa760..80e60cd 100644 (file)
@@ -865,15 +865,31 @@ class core_calendar_external extends external_api {
         self::validate_context($context);
         parse_str($params['formdata'], $data);
 
+        $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
+        $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
+        if ($courseid) {
+            require_once($CFG->libdir . '/grouplib.php');
+            $groupcoursedata = groups_get_course_data($courseid);
+            if (!empty($groupcoursedata->groups)) {
+                $formoptions['groups'] = [];
+                foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                    $formoptions['groups'][$groupid] = $groupdata->name;
+                }
+            }
+        }
+
         if (!empty($data['id'])) {
             $eventid = clean_param($data['id'], PARAM_INT);
             $legacyevent = calendar_event::load($eventid);
             $legacyevent->count_repeats();
-            $formoptions = ['event' => $legacyevent];
+            $formoptions['event'] = $legacyevent;
             $mform = new update_event_form(null, $formoptions, 'post', '', null, true, $data);
         } else {
             $legacyevent = null;
-            $mform = new create_event_form(null, null, 'post', '', null, true, $data);
+            $mform = new create_event_form(null, $formoptions, 'post', '', null, true, $data);
         }
 
         if ($validateddata = $mform->get_data()) {
index 60d3d0f..15dee2a 100644 (file)
@@ -1070,7 +1070,7 @@ class calendar_information {
             $category = (\coursecat::get($course->category, MUST_EXIST, true))->get_db_record();
         } else if (!empty($categoryid)) {
             $course = get_site();
-            $courses = calendar_get_default_courses();
+            $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
 
             // Filter available courses to those within this category or it's children.
             $ids = [$categoryid];
@@ -1084,7 +1084,7 @@ class calendar_information {
             $calendar->context = context_coursecat::instance($categoryid);
         } else {
             $course = get_site();
-            $courses = calendar_get_default_courses();
+            $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
             $category = null;
 
             $calendar->context = context_system::instance();
@@ -2570,76 +2570,6 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $
     }
 }
 
-/**
- * Get all of the allowed types for all of the courses and groups
- * the logged in user belongs to.
- *
- * The returned array will optionally have 5 keys:
- *      'user' : true if the logged in user can create user events
- *      'site' : true if the logged in user can create site events
- *      'category' : array of course categories that the user can create events for
- *      'course' : array of courses that the user can create events for
- *      'group': array of groups that the user can create events for
- *      'groupcourses' : array of courses that the groups belong to (can
- *                       be different from the list in 'course'.
- *
- * @return array The array of allowed types.
- */
-function calendar_get_all_allowed_types() {
-    global $CFG, $USER, $DB;
-
-    require_once($CFG->libdir . '/enrollib.php');
-
-    $types = [];
-
-    $allowed = new stdClass();
-
-    calendar_get_allowed_types($allowed);
-
-    if ($allowed->user) {
-        $types['user'] = true;
-    }
-
-    if ($allowed->site) {
-        $types['site'] = true;
-    }
-
-    if (coursecat::has_manage_capability_on_any()) {
-        $types['category'] = coursecat::make_categories_list('moodle/category:manage');
-    }
-
-    // This function warms the context cache for the course so the calls
-    // to load the course context in calendar_get_allowed_types don't result
-    // in additional DB queries.
-    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
-
-    // We want to pre-fetch all of the groups for each course in a single
-    // query to avoid calendar_get_allowed_types from hitting the DB for
-    // each separate course.
-    $groups = groups_get_all_groups_for_courses($courses);
-
-    foreach ($courses as $course) {
-        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
-        calendar_get_allowed_types($allowed, $course, $coursegroups);
-
-        if (!empty($allowed->courses)) {
-            $types['course'][$course->id] = $course;
-        }
-
-        if (!empty($allowed->groups)) {
-            $types['groupcourses'][$course->id] = $course;
-
-            if (!isset($types['group'])) {
-                $types['group'] = array_values($allowed->groups);
-            } else {
-                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
-            }
-        }
-    }
-
-    return $types;
-}
-
 /**
  * See if user can add calendar entries at all used to print the "New Event" button.
  *
@@ -3508,18 +3438,18 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
  */
 function calendar_output_fragment_event_form($args) {
     global $CFG, $OUTPUT, $USER;
-
+    require_once($CFG->libdir . '/grouplib.php');
     $html = '';
     $data = [];
     $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
     $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
-    $courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
+    $courseid = (isset($args['courseid']) && $args['courseid'] != SITEID) ? clean_param($args['courseid'], PARAM_INT) : null;
     $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
     $event = null;
     $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
     $context = \context_user::instance($USER->id);
     $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
-    $formoptions = ['editoroptions' => $editoroptions];
+    $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
     $draftitemid = 0;
 
     if ($hasformdata) {
@@ -3534,6 +3464,13 @@ function calendar_output_fragment_event_form($args) {
     }
 
     if (is_null($eventid)) {
+        if (!empty($courseid)) {
+            $groupcoursedata = groups_get_course_data($courseid);
+            $formoptions['groups'] = [];
+            foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                $formoptions['groups'][$groupid] = $groupdata->name;
+            }
+        }
         $mform = new \core_calendar\local\event\forms\create(
             null,
             $formoptions,
@@ -3545,16 +3482,19 @@ function calendar_output_fragment_event_form($args) {
         );
 
         // Let's check first which event types user can add.
-        calendar_get_allowed_types($allowed, $courseid);
+        $eventtypes = calendar_get_allowed_event_types($courseid);
 
         // If the user is on course context and is allowed to add course events set the event type default to course.
-        if ($courseid != SITEID && !empty($allowed->courses)) {
+        if ($courseid != SITEID && !empty($eventtypes['course'])) {
             $data['eventtype'] = 'course';
             $data['courseid'] = $courseid;
             $data['groupcourseid'] = $courseid;
-        } else if (!empty($categoryid) && !empty($allowed->category)) {
+        } else if (!empty($categoryid) && !empty($eventtypes['category'])) {
             $data['eventtype'] = 'category';
             $data['categoryid'] = $categoryid;
+        } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) {
+            $data['groupcourseid'] = $courseid;
+            $data['groups'] = $groupcoursedata->groups;
         }
         $mform->set_data($data);
     } else {
@@ -3564,6 +3504,15 @@ function calendar_output_fragment_event_form($args) {
         $data = array_merge((array) $eventdata, $data);
         $event->count_repeats();
         $formoptions['event'] = $event;
+
+        if (!empty($event->courseid)) {
+            $groupcoursedata = groups_get_course_data($event->courseid);
+            $formoptions['groups'] = [];
+            foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                $formoptions['groups'][$groupid] = $groupdata->name;
+            }
+        }
+
         $data['description']['text'] = file_prepare_draft_area(
             $draftitemid,
             $event->context->id,
@@ -3692,3 +3641,175 @@ function calendar_is_valid_eventtype($type) {
     ];
     return in_array($type, $validtypes);
 }
+
+/**
+ * Get event types the user can create event based on categories, courses and groups
+ * the logged in user belongs to.
+ *
+ * @param int|null $courseid The course id.
+ * @return array The array of allowed types.
+ */
+function calendar_get_allowed_event_types(int $courseid = null) {
+    global $DB, $CFG, $USER;
+
+    $types = [
+        'user' => false,
+        'site' => false,
+        'course' => false,
+        'group' => false,
+        'category' => false
+    ];
+
+    if (!empty($courseid) && $courseid != SITEID) {
+        $context = \context_course::instance($courseid);
+        $groups = groups_get_all_groups($courseid);
+
+        $types['user'] = has_capability('moodle/calendar:manageownentries', $context);
+
+        if (has_capability('moodle/calendar:manageentries', $context) || !empty($CFG->calendar_adminseesall)) {
+            $types['course'] = true;
+
+            $types['group'] = (!empty($groups) && has_capability('moodle/site:accessallgroups', $context))
+                || array_filter($groups, function($group) use ($USER) {
+                    return groups_is_member($group->id);
+                });
+        } else if (has_capability('moodle/calendar:managegroupentries', $context)) {
+            $types['group'] = (!empty($groups) && has_capability('moodle/site:accessallgroups', $context))
+                || array_filter($groups, function($group) use ($USER) {
+                    return groups_is_member($group->id);
+                });
+        }
+    }
+
+    if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) {
+        $types['site'] = true;
+    }
+
+    if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) {
+        $types['user'] = true;
+    }
+    if (coursecat::has_manage_capability_on_any()) {
+        $types['category'] = true;
+    }
+
+    // We still don't know if the user can create group and course events, so iterate over the courses to find out
+    // if the user has capabilities in one of the courses.
+    if ($types['course'] == false || $types['group'] == false) {
+        if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) {
+            $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                      FROM {course} c
+                      JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id
+                     WHERE c.id IN (
+                            SELECT DISTINCT courseid FROM {groups}
+                        )";
+            $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]);
+            foreach ($courseswithgroups as $course) {
+                context_helper::preload_from_record($course);
+                $context = context_course::instance($course->id);
+
+                if (has_capability('moodle/calendar:manageentries', $context)) {
+                    if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) {
+                        // The user can manage group entries or access any group.
+                        $types['group'] = true;
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+            }
+            $courseswithgroups->close();
+
+            if (false === $types['course']) {
+                // Course is still not confirmed. There may have been no courses with a group in them.
+                $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
+                $sql = "SELECT
+                            c.id, c.visible, {$ctxfields}
+                        FROM {course}
+                        JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
+                $params = [
+                    'contextlevel' => CONTEXT_COURSE,
+                ];
+                $courses = $DB->get_recordset_sql($sql, $params);
+                foreach ($courses as $course) {
+                    context_helper::preload_from_record($course);
+                    $context = context_course::instance($course->id);
+                    if (has_capability('moodle/calendar:manageentries', $context)) {
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+                $courses->close();
+            }
+
+        } else {
+            $courses = calendar_get_default_courses(null, 'id');
+            if (empty($courses)) {
+                return $types;
+            }
+
+            $courseids = array_map(function($c) {
+                return $c->id;
+            }, $courses);
+
+            // Check whether the user has access to create events within courses which have groups.
+            list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+            $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                      FROM {course} c
+                      JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
+                     WHERE c.id $insql
+                       AND c.id IN (SELECT DISTINCT courseid FROM {groups})";
+            $params['contextlevel'] = CONTEXT_COURSE;
+            $courseswithgroups = $DB->get_recordset_sql($sql, $params);
+            foreach ($courseswithgroups as $coursewithgroup) {
+                context_helper::preload_from_record($coursewithgroup);
+                $context = context_course::instance($coursewithgroup->id);
+
+                if (has_capability('moodle/calendar:manageentries', $context)) {
+                    // The user has access to manage calendar entries for the whole course.
+                    // This includes groups if they have the accessallgroups capability.
+                    $types['course'] = true;
+                    if (has_capability('moodle/site:accessallgroups', $context)) {
+                        // The user also has access to all groups so they can add calendar entries to any group.
+                        // The manageentries capability overrides the managegroupentries capability.
+                        $types['group'] = true;
+                        break;
+                    }
+
+                    if (empty($types['group']) && has_capability('moodle/calendar:managegroupentries', $context)) {
+                        // The user has the managegroupentries capability.
+                        // If they have access to _any_ group, then they can create calendar entries within that group.
+                        $types['group'] = !empty(groups_get_all_groups($coursewithgroup->id, $USER->id));
+                    }
+                }
+
+                // Okay, course and group event types are allowed, no need to keep the loop iteration.
+                if ($types['course'] == true && $types['group'] == true) {
+                    break;
+                }
+            }
+            $courseswithgroups->close();
+
+            if (false === $types['course']) {
+                list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+                $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                                FROM {course} c
+                                JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
+                                WHERE c.id $insql";
+                $params['contextlevel'] = CONTEXT_COURSE;
+                $contextrecords = $DB->get_recordset_sql($contextsql, $params);
+                foreach ($contextrecords as $course) {
+                    context_helper::preload_from_record($course);
+                    $coursecontext = context_course::instance($course->id);
+                    if (has_capability('moodle/calendar:manageentries', $coursecontext)
+                            && ($courseid == $course->id || empty($courseid))) {
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+                $contextrecords->close();
+            }
+
+        }
+    }
+
+    return $types;
+}
index 75ddc09..87289a6 100644 (file)
@@ -61,7 +61,7 @@ if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
 
-$form = new \core_calendar\local\event\forms\managesubscriptions();
+$form = new \core_calendar\local\event\forms\managesubscriptions(null, ['courseid' => $course->id]);
 $form->set_data(array(
     'course' => $course->id
 ));
@@ -105,26 +105,26 @@ if (!empty($formdata)) {
     }
 }
 
-$types = calendar_get_all_allowed_types();
+$types = calendar_get_allowed_event_types($courseid);
 
 $searches = [];
 $params = [];
 
 $usedefaultfilters = true;
-if (!empty($courseid) && $courseid == SITEID && isset($types['site'])) {
+if (!empty($courseid) && $courseid == SITEID && !empty($types['site'])) {
     $searches[] = "(eventtype = 'site')";
     $searches[] = "(eventtype = 'user' AND userid = :userid)";
     $params['userid'] = $USER->id;
     $usedefaultfilters = false;
 }
 
-if (!empty($courseid) && isset($types['course']) && array_key_exists($courseid, $types['course'])) {
+if (!empty($courseid) && !empty($types['course'])) {
     $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)";
     $params += ['courseid' => $courseid];
     $usedefaultfilters = false;
 }
 
-if (!empty($categoryid) && isset($types['category']) && array_key_exists($categoryid, $types['category'])) {
+if (!empty($categoryid) && !empty($types['category'])) {
     $searches[] = "(eventtype = 'category' AND categoryid = :categoryid)";
     $params += ['categoryid' => $categoryid];
     $usedefaultfilters = false;
@@ -134,19 +134,27 @@ if ($usedefaultfilters) {
     $searches[] = "(eventtype = 'user' AND userid = :userid)";
     $params['userid'] = $USER->id;
 
-    if (isset($types['site'])) {
+    if (!empty($types['site'])) {
         $searches[] = "(eventtype = 'site' AND courseid  = :siteid)";
         $params += ['siteid' => SITEID];
     }
 
-    if (isset($types['course'])) {
-        list($courseinsql, $courseparams) = $DB->get_in_or_equal(array_keys($types['course']), SQL_PARAMS_NAMED, 'course');
-        $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
-        $params += $courseparams;
+    if (!empty($types['course'])) {
+        $courses = calendar_get_default_courses(null, 'id', true);
+        if (!empty($courses)) {
+            $courseids = array_map(function ($c) {
+                return $c->id;
+            }, $courses);
+
+            list($courseinsql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'course');
+            $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
+            $params += $courseparams;
+        }
     }
 
-    if (isset($types['category'])) {
-        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(array_keys($types['category']), SQL_PARAMS_NAMED, 'category');
+    if (!empty($types['category'])) {
+        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(
+                array_keys(\coursecat::make_categories_list('moodle/category:manage')), SQL_PARAMS_NAMED, 'category');
         $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})";
         $params += $categoryparams;
     }
index 5603daa..5483edb 100644 (file)
@@ -170,8 +170,8 @@ Feature: Perform basic calendar functionality
     And I click on "New event" "button"
     When I click on "Save" "button"
     Then I should see "Required"
-    And I am on site homepage
-    And I follow "Calendar"
+    And I am on homepage
+    And I follow "This month"
     And I click on "New event" "button"
     And I set the field "Type of event" to "Course"
     When I click on "Save" "button"
index 28b42d3..ed623d0 100644 (file)
@@ -1989,12 +1989,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
         $this->setUser($user);
 
-        $this->expectException('moodle_exception');
-
-        external_api::clean_returnvalue(
+        $result = external_api::clean_returnvalue(
             core_calendar_external::submit_create_update_form_returns(),
             core_calendar_external::submit_create_update_form($querystring)
         );
+
+        $this->assertTrue($result['validationerror']);
     }
 
     /**
@@ -2027,7 +2027,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2100,7 +2100,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2174,7 +2174,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2248,7 +2248,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
index bf8b6ca..e50e58c 100644 (file)
@@ -418,65 +418,92 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertCount(3, $events);
     }
 
-    public function test_calendar_get_all_allowed_types_no_types() {
+    public function test_calendar_get_default_courses() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
-        $systemcontext = context_system::instance();
-        $sitecontext = context_course::instance(SITEID);
-        $roleid = $generator->create_role();
-
-        $generator->role_assign($roleid, $user->id, $systemcontext->id);
-        $generator->role_assign($roleid, $user->id, $sitecontext->id);
-        $this->setUser($user);
+        $course1 = $generator->create_course();
+        $course2 = $generator->create_course();
+        $course3 = $generator->create_course();
+        $context = context_course::instance($course1->id);
 
-        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $sitecontext, true);
-        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $systemcontext, true);
+        $this->setAdminUser();
+        $admin = clone $USER;
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertEmpty($types);
-    }
+        $teacher = $generator->create_user();
+        $generator->enrol_user($teacher->id, $course1->id, 'teacher');
+        $generator->enrol_user($admin->id, $course1->id, 'teacher');
 
-    public function test_calendar_get_all_allowed_types_user() {
-        $generator = $this->getDataGenerator();
-        $user = $generator->create_user();
-        $context = context_system::instance();
-        $roleid = $generator->create_role();
+        $CFG->calendar_adminseesall = false;
 
-        $generator->role_assign($roleid, $user->id, $context->id);
-        $this->setUser($user);
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course + current course.
+        $this->assertCount(2, $courses);
+        $CFG->calendar_adminseesall = true;
+        $courses = calendar_get_default_courses();
+        // All courses + SITE.
+        $this->assertCount(4, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // All courses + SITE.
+        $this->assertCount(4, $courses);
 
-        assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true);
+        $this->setUser($teacher);
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertTrue($types['user']);
+        $CFG->calendar_adminseesall = false;
 
-        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true);
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
+        // This setting should not affect teachers.
+        $CFG->calendar_adminseesall = true;
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertArrayNotHasKey('user', $types);
     }
 
-    public function test_calendar_get_all_allowed_types_site() {
+    /**
+     * 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();
-        $context = context_course::instance(SITEID);
-        $roleid = $generator->create_role();
+        $skipnavigation = true;
+        $skipevents = true;
+        $event = create_event([
+            'eventtype' => 'user',
+            'userid' => $user->id
+        ]);
 
-        $generator->role_assign($roleid, $user->id, $context->id);
         $this->setUser($user);
+        $calendar = \calendar_information::create(time() - 10, SITEID, null);
 
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
-
-        $types = calendar_get_all_allowed_types();
-        $this->assertTrue($types['site']);
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
+        $this->assertEmpty($data->events);
 
-        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $skipevents = false;
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertArrayNotHasKey('site', $types);
+        $this->assertEquals($event->id, $data->events[0]->id);
     }
 
-    public function test_calendar_get_all_allowed_types_course() {
+    public function test_calendar_get_allowed_event_types_course() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course1 = $generator->create_course(); // Has capability.
@@ -504,35 +531,18 @@ class core_calendar_lib_testcase extends advanced_testcase {
 
         // The user only has the correct capability in course 1 so that is the only
         // one that should be in the results.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[$course1->id]->id);
+        $types = calendar_get_allowed_event_types($course1->id);
+        $this->assertTrue($types['course']);
 
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context1, true);
 
         // The user only now has the correct capability in both course 1 and 2 so we
         // expect both to be in the results.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typecourses, function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        });
-
-        $this->assertCount(2, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[0]->id);
-        $this->assertEquals($course2->id, $typecourses[1]->id);
+        $types = calendar_get_allowed_event_types($course3->id);
+        $this->assertFalse($types['course']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_no_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_acces_to_diff_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
@@ -545,224 +555,76 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->setUser($user);
 
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
 
-        // The user has the correct capability in the course but there are
-        // no groups so we shouldn't see a group type.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[$course->id]->id);
-        $this->assertArrayNotHasKey('group', $types);
-        $this->assertArrayNotHasKey('groupcourses', $types);
+        // The user has the correct capability in the course but they aren't a member
+        // of any of the groups and don't have the accessallgroups capability.
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['course']);
+        $this->assertFalse($types['group']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_no_acces_to_diff_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
         $context = context_course::instance($course->id);
-        $group1 = $generator->create_group(array('courseid' => $course->id));
-        $group2 = $generator->create_group(array('courseid' => $course->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course->id, 'student');
         $generator->role_assign($roleid, $user->id, $context->id);
-
         $this->setUser($user);
-
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
-        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
-
-        // The user has the correct capability in the course but they aren't a member
-        // of any of the groups and don't have the accessallgroups capability.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[$course->id]->id);
-        $this->assertArrayNotHasKey('group', $types);
-        $this->assertArrayNotHasKey('groupcourses', $types);
+        // The user has the correct capability in the course but there are
+        // no groups so we shouldn't see a group type.
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['course']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_access_all_groups() {
+    public function test_calendar_get_allowed_event_types_group_access_all_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course1 = $generator->create_course();
         $course2 = $generator->create_course();
+        $generator->create_group(array('courseid' => $course1->id));
+        $generator->create_group(array('courseid' => $course2->id));
         $context1 = context_course::instance($course1->id);
         $context2 = context_course::instance($course2->id);
-        $group1 = $generator->create_group(array('courseid' => $course1->id));
-        $group2 = $generator->create_group(array('courseid' => $course1->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course1->id, 'student');
         $generator->enrol_user($user->id, $course2->id, 'student');
         $generator->role_assign($roleid, $user->id, $context1->id);
         $generator->role_assign($roleid, $user->id, $context2->id);
-
         $this->setUser($user);
-
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
         assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context1, true);
         assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context2, true);
-
         // The user has the correct capability in the course and has
         // the accessallgroups capability.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $typegroups = $types['group'];
-        $typegroupcourses = $types['groupcourses'];
-        $idascfunc = function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        };
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typecourses, $idascfunc);
-        usort($typegroups, $idascfunc);
-
-        $this->assertCount(2, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[0]->id);
-        $this->assertEquals($course2->id, $typecourses[1]->id);
-        $this->assertCount(1, $typegroupcourses);
-        $this->assertEquals($course1->id, $typegroupcourses[$course1->id]->id);
-        $this->assertCount(2, $typegroups);
-        $this->assertEquals($group1->id, $typegroups[0]->id);
-        $this->assertEquals($group2->id, $typegroups[1]->id);
+        $types = calendar_get_allowed_event_types($course1->id);
+        $this->assertTrue($types['group']);
     }
-
-    public function test_calendar_get_all_allowed_types_group_no_access_all_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_access_all_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
         $context = context_course::instance($course->id);
         $group1 = $generator->create_group(array('courseid' => $course->id));
         $group2 = $generator->create_group(array('courseid' => $course->id));
-        $group3 = $generator->create_group(array('courseid' => $course->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course->id, 'student');
         $generator->role_assign($roleid, $user->id, $context->id);
         $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user->id));
         $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user->id));
-
         $this->setUser($user);
-
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
         assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
-
         // The user has the correct capability in the course but can't access
         // groups that they are not a member of.
-        $types = calendar_get_all_allowed_types();
-        $typegroups = $types['group'];
-        $typegroupcourses = $types['groupcourses'];
-        $idascfunc = function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        };
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typegroups, $idascfunc);
-
-        $this->assertCount(1, $typegroupcourses);
-        $this->assertEquals($course->id, $typegroupcourses[$course->id]->id);
-        $this->assertCount(2, $typegroups);
-        $this->assertEquals($group1->id, $typegroups[0]->id);
-        $this->assertEquals($group2->id, $typegroups[1]->id);
-    }
-
-    public function test_calendar_get_default_courses() {
-        global $USER, $CFG;
-
-        $this->resetAfterTest(true);
-
-        $generator = $this->getDataGenerator();
-        $user = $generator->create_user();
-        $course1 = $generator->create_course();
-        $course2 = $generator->create_course();
-        $course3 = $generator->create_course();
-        $context = context_course::instance($course1->id);
-
-        $this->setAdminUser();
-        $admin = clone $USER;
-
-        $teacher = $generator->create_user();
-        $generator->enrol_user($teacher->id, $course1->id, 'teacher');
-        $generator->enrol_user($admin->id, $course1->id, 'teacher');
-
-        $CFG->calendar_adminseesall = false;
-
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course + current course.
-        $this->assertCount(2, $courses);
-        $CFG->calendar_adminseesall = true;
-        $courses = calendar_get_default_courses();
-        // All courses + SITE.
-        $this->assertCount(4, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // All courses + SITE.
-        $this->assertCount(4, $courses);
-
-        $this->setUser($teacher);
-
-        $CFG->calendar_adminseesall = false;
-
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course only (ignore current).
-        $this->assertCount(1, $courses);
-        // This setting should not affect teachers.
-        $CFG->calendar_adminseesall = true;
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course only (ignore current).
-        $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);
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertFalse($types['group']);
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true);
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['group']);
     }
 }
index 9034c84..07f7a5b 100644 (file)
@@ -493,7 +493,8 @@ class comment {
                 $textareaattrs = array(
                     'name' => 'content',
                     'rows' => 2,
-                    'id' => 'dlg-content-'.$this->cid
+                    'id' => 'dlg-content-'.$this->cid,
+                    'aria-label' => get_string('addcomment')
                 );
                 if (!$this->fullwidth) {
                     $textareaattrs['cols'] = '20';
index b3b8a0d..d6f79c3 100644 (file)
@@ -156,4 +156,4 @@ echo $OUTPUT->footer();
 // Thats all folks.
 // Don't ever even consider putting anything after this. It just wouldn't make sense.
 // But you already knew that, you smart developer you.
-exit;
\ No newline at end of file
+exit;
index eca4de1..e2a4192 100644 (file)
@@ -247,8 +247,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                 'i/empty',
                 '',
                 'moodle',
-                array('class' => 'tree-icon', 'title' => get_string('showcategory', 'moodle', $text))
-            );
+                array('class' => 'tree-icon'));
             $icon = html_writer::span($icon, 'float-left');
         }
         $actions = \core_course\management\helper::get_category_listitem_actions($category);
@@ -288,7 +287,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::end_div();
         if ($isexpanded) {
             $html .= html_writer::start_tag('ul',
-                array('class' => 'ml-1', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
+                array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
             $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
             $catatlevel[] = array_shift($selectedcategories);
             $catatlevel = array_unique($catatlevel);
index d697a8b..2ced5d6 100644 (file)
Binary files a/enrol/manual/amd/build/form-potential-user-selector.min.js and b/enrol/manual/amd/build/form-potential-user-selector.min.js differ
index 49bdcbf..066f59d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
+define(['jquery', 'core/ajax', 'core/templates', 'core/str'], function($, Ajax, Templates, Str) {
+
+    /** @var {Number} Maximum number of users to show. */
+    var MAXUSERS = 100;
 
     return /** @alias module:enrol_manual/form-potential-user-selector */ {
 
         processResults: function(selector, results) {
             var users = [];
-            $.each(results, function(index, user) {
-                users.push({
-                    value: user.id,
-                    label: user._label
+            if ($.isArray(results)) {
+                $.each(results, function(index, user) {
+                    users.push({
+                        value: user.id,
+                        label: user._label
+                    });
                 });
-            });
-            return users;
+                return users;
+
+            } else {
+                return results;
+            }
         },
 
         transport: function(selector, query, success, failure) {
@@ -57,7 +65,7 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
                     search: query,
                     searchanywhere: true,
                     page: 0,
-                    perpage: 30
+                    perpage: MAXUSERS + 1
                 }
             }]);
 
@@ -65,30 +73,38 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
                 var promises = [],
                     i = 0;
 
-                // Render the label.
-                $.each(results, function(index, user) {
-                    var ctx = user,
-                        identity = [];
-                    $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
-                        if (typeof user[k] !== 'undefined' && user[k] !== '') {
-                            ctx.hasidentity = true;
-                            identity.push(user[k]);
-                        }
+                if (results.length <= MAXUSERS) {
+                    // Render the label.
+                    $.each(results, function(index, user) {
+                        var ctx = user,
+                            identity = [];
+                        $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
+                            if (typeof user[k] !== 'undefined' && user[k] !== '') {
+                                ctx.hasidentity = true;
+                                identity.push(user[k]);
+                            }
+                        });
+                        ctx.identity = identity.join(', ');
+                        promises.push(Templates.render('enrol_manual/form-user-selector-suggestion', ctx));
                     });
-                    ctx.identity = identity.join(', ');
-                    promises.push(Templates.render('enrol_manual/form-user-selector-suggestion', ctx));
-                });
 
-                // Apply the label to the results.
-                return $.when.apply($.when, promises).then(function() {
-                    var args = arguments;
-                    $.each(results, function(index, user) {
-                        user._label = args[i];
-                        i++;
+                    // Apply the label to the results.
+                    return $.when.apply($.when, promises).then(function() {
+                        var args = arguments;
+                        $.each(results, function(index, user) {
+                            user._label = args[i];
+                            i++;
+                        });
+                        success(results);
+                        return;
                     });
-                    success(results);
-                    return;
-                });
+
+                } else {
+                    return Str.get_string('toomanyuserstoshow', 'core', '>' + MAXUSERS).then(function(toomanyuserstoshow) {
+                        success(toomanyuserstoshow);
+                        return;
+                    });
+                }
 
             }).fail(failure);
         }
diff --git a/enrol/manual/tests/behat/quickenrolment.feature b/enrol/manual/tests/behat/quickenrolment.feature
new file mode 100644 (file)
index 0000000..c5e3549
--- /dev/null
@@ -0,0 +1,155 @@
+@enrol @enrol_manual
+Feature: Teacher can search and enrol users one by one into the course
+  In order to quickly enrol particular students into my course
+  As a teacher
+  I can search for the students and enrol them into the course
+
+  Background:
+    Given the following "users" exist:
+      | username    | firstname | lastname | email                   |
+      | teacher001  | Teacher   | 001      | teacher001@example.com  |
+      | student001  | Student   | 001      | student001@example.com  |
+      | student002  | Student   | 002      | student002@example.com  |
+      | student003  | Student   | 003      | student003@example.com  |
+      | student004  | Student   | 004      | student004@example.com  |
+      | student005  | Student   | 005      | student005@example.com  |
+      | student006  | Student   | 006      | student006@example.com  |
+      | student007  | Student   | 007      | student007@example.com  |
+      | student008  | Student   | 008      | student008@example.com  |
+      | student009  | Student   | 009      | student009@example.com  |
+      | student010  | Student   | 010      | student010@example.com  |
+      | student011  | Student   | 011      | student011@example.com  |
+      | student012  | Student   | 012      | student012@example.com  |
+      | student013  | Student   | 013      | student013@example.com  |
+      | student014  | Student   | 014      | student014@example.com  |
+      | student015  | Student   | 015      | student015@example.com  |
+      | student016  | Student   | 016      | student016@example.com  |
+      | student017  | Student   | 017      | student017@example.com  |
+      | student018  | Student   | 018      | student018@example.com  |
+      | student019  | Student   | 019      | student019@example.com  |
+      | student020  | Student   | 020      | student020@example.com  |
+      | student021  | Student   | 021      | student021@example.com  |
+      | student022  | Student   | 022      | student022@example.com  |
+      | student023  | Student   | 023      | student023@example.com  |
+      | student024  | Student   | 024      | student024@example.com  |
+      | student025  | Student   | 025      | student025@example.com  |
+      | student026  | Student   | 026      | student026@example.com  |
+      | student027  | Student   | 027      | student027@example.com  |
+      | student028  | Student   | 028      | student028@example.com  |
+      | student029  | Student   | 029      | student029@example.com  |
+      | student030  | Student   | 030      | student030@example.com  |
+      | student031  | Student   | 031      | student031@example.com  |
+      | student032  | Student   | 032      | student032@example.com  |
+      | student033  | Student   | 033      | student033@example.com  |
+      | student034  | Student   | 034      | student034@example.com  |
+      | student035  | Student   | 035      | student035@example.com  |
+      | student036  | Student   | 036      | student036@example.com  |
+      | student037  | Student   | 037      | student037@example.com  |
+      | student038  | Student   | 038      | student038@example.com  |
+      | student039  | Student   | 039      | student039@example.com  |
+      | student040  | Student   | 040      | student040@example.com  |
+      | student041  | Student   | 041      | student041@example.com  |
+      | student042  | Student   | 042      | student042@example.com  |
+      | student043  | Student   | 043      | student043@example.com  |
+      | student044  | Student   | 044      | student044@example.com  |
+      | student045  | Student   | 045      | student045@example.com  |
+      | student046  | Student   | 046      | student046@example.com  |
+      | student047  | Student   | 047      | student047@example.com  |
+      | student048  | Student   | 048      | student048@example.com  |
+      | student049  | Student   | 049      | student049@example.com  |
+      | student050  | Student   | 050      | student050@example.com  |
+      | student051  | Student   | 051      | student051@example.com  |
+      | student052  | Student   | 052      | student052@example.com  |
+      | student053  | Student   | 053      | student053@example.com  |
+      | student054  | Student   | 054      | student054@example.com  |
+      | student055  | Student   | 055      | student055@example.com  |
+      | student056  | Student   | 056      | student056@example.com  |
+      | student057  | Student   | 057      | student057@example.com  |
+      | student058  | Student   | 058      | student058@example.com  |
+      | student059  | Student   | 059      | student059@example.com  |
+      | student060  | Student   | 060      | student060@example.com  |
+      | student061  | Student   | 061      | student061@example.com  |
+      | student062  | Student   | 062      | student062@example.com  |
+      | student063  | Student   | 063      | student063@example.com  |
+      | student064  | Student   | 064      | student064@example.com  |
+      | student065  | Student   | 065      | student065@example.com  |
+      | student066  | Student   | 066      | student066@example.com  |
+      | student067  | Student   | 067      | student067@example.com  |
+      | student068  | Student   | 068      | student068@example.com  |
+      | student069  | Student   | 069      | student069@example.com  |
+      | student070  | Student   | 070      | student070@example.com  |
+      | student071  | Student   | 071      | student071@example.com  |
+      | student072  | Student   | 072      | student072@example.com  |
+      | student073  | Student   | 073      | student073@example.com  |
+      | student074  | Student   | 074      | student074@example.com  |
+      | student075  | Student   | 075      | student075@example.com  |
+      | student076  | Student   | 076      | student076@example.com  |
+      | student077  | Student   | 077      | student077@example.com  |
+      | student078  | Student   | 078      | student078@example.com  |
+      | student079  | Student   | 079      | student079@example.com  |
+      | student080  | Student   | 080      | student080@example.com  |
+      | student081  | Student   | 081      | student081@example.com  |
+      | student082  | Student   | 082      | student082@example.com  |
+      | student083  | Student   | 083      | student083@example.com  |
+      | student084  | Student   | 084      | student084@example.com  |
+      | student085  | Student   | 085      | student085@example.com  |
+      | student086  | Student   | 086      | student086@example.com  |
+      | student087  | Student   | 087      | student087@example.com  |
+      | student088  | Student   | 088      | student088@example.com  |
+      | student089  | Student   | 089      | student089@example.com  |
+      | student090  | Student   | 090      | student090@example.com  |
+      | student091  | Student   | 091      | student091@example.com  |
+      | student092  | Student   | 092      | student092@example.com  |
+      | student093  | Student   | 093      | student093@example.com  |
+      | student094  | Student   | 094      | student094@example.com  |
+      | student095  | Student   | 095      | student095@example.com  |
+      | student096  | Student   | 096      | student096@example.com  |
+      | student097  | Student   | 097      | student097@example.com  |
+      | student098  | Student   | 098      | student098@example.com  |
+      | student099  | Student   | 099      | student099@example.com  |
+    And the following "courses" exist:
+      | fullname    | shortname |
+      | Course 001  | C001      |
+    And the following "course enrolments" exist:
+      | user        | course    | role            |
+      | teacher001  | C001      | editingteacher  |
+    And I log in as "teacher001"
+    And I am on "Course 001" course homepage
+
+  @javascript
+  Scenario: Teacher can search and enrol one particular student
+    Given I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "student001"
+    And I should see "Student 001"
+    And I click on "Enrol users" "button" in the "Enrol users" "dialogue"
+    Then I should see "Active" in the "Student 001" "table_row"
+
+  @javascript
+  Scenario: Searching for a non-existing user
+    Given I navigate to course participants
+    And I press "Enrol users"
+    And I set the field "Select users" to "qwertyuiop"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "No suggestions"
+
+  @javascript
+  Scenario: If there are less than 100 matching users, all are displayed for selection
+    Given I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Student 099" item in the autocomplete list
+    Then I should see "Student 099"
+
+  @javascript
+  Scenario: If there are more than 100 matching users, inform there are too many.
+    Given the following "users" exist:
+      | username    | firstname | lastname | email                   |
+      | student100  | Student   | 100      | student100@example.com  |
+      | student101  | Student   | 101      | student101@example.com  |
+    And I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "Too many users (>100) to show"
index a52720f..509f04f 100644 (file)
@@ -65,6 +65,8 @@ $string['expirymessageenrolledbody'] = 'Dear {$a->user},
 This is a notification that your enrolment in the course \'{$a->course}\' is due to expire on {$a->timeend}.
 
 If you need help, please contact {$a->enroller}.';
+$string['expirynotifyall'] = 'Teacher and enrolled user';
+$string['expirynotifyenroller'] = 'Teacher only';
 $string['groupkey'] = 'Use group enrolment keys';
 $string['groupkey_desc'] = 'Use group enrolment keys by default.';
 $string['groupkey_help'] = 'In addition to restricting access to the course to only those who know the key, use of group enrolment keys means users are automatically added to groups when they enrol in the course.
index 87e9c14..bac3887 100644 (file)
@@ -657,8 +657,8 @@ class enrol_self_plugin extends enrol_plugin {
      */
     protected function get_expirynotify_options() {
         $options = array(0 => get_string('no'),
-                         1 => get_string('expirynotifyenroller', 'core_enrol'),
-                         2 => get_string('expirynotifyall', 'core_enrol'));
+                         1 => get_string('expirynotifyenroller', 'enrol_self'),
+                         2 => get_string('expirynotifyall', 'enrol_self'));
         return $options;
     }
 
index 25dadf0..6375ea2 100644 (file)
@@ -85,7 +85,9 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configduration('enrol_self/enrolperiod',
         get_string('enrolperiod', 'enrol_self'), get_string('enrolperiod_desc', 'enrol_self'), 0));
 
-    $options = array(0 => get_string('no'), 1 => get_string('expirynotifyenroller', 'core_enrol'), 2 => get_string('expirynotifyall', 'core_enrol'));
+    $options = array(0 => get_string('no'),
+                     1 => get_string('expirynotifyenroller', 'enrol_self'),
+                     2 => get_string('expirynotifyall', 'enrol_self'));
     $settings->add(new admin_setting_configselect('enrol_self/expirynotify',
         get_string('expirynotify', 'core_enrol'), get_string('expirynotify_help', 'core_enrol'), 0, $options));
 
index cf656d3..6c38f65 100644 (file)
@@ -1301,8 +1301,7 @@ $string['cachesession'] = 'Session cache';
 $string['cachesessionhelp'] = 'User specific cache that expires when the user\'s session ends. Designed to alleviate session bloat/strain.';
 $string['cacheapplication'] = 'Application cache';
 $string['cacheapplicationhelp'] = 'Cached items are shared among all users and expire by a determined time to live (ttl).';
-// Deprecated since Moodle 3.2.
-$string['mobile'] = 'Mobile';
+
 // Deprecated since Moodle 3.3.
 $string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
 $string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
index d93fb9c..6003668 100644 (file)
@@ -266,9 +266,6 @@ $string['when'] = 'When';
 $string['yesterday'] = 'Yesterday';
 $string['youcandeleteallrepeats'] = 'This event is part of a repeating event series. You can delete this event only, or all {$a} events in the series at once.';
 
-// Deprecated since Moodle 3.2.
-$string['for'] = 'for';
-
 // Deprecated since Moodle 3.4.
 $string['quickdownloadcalendar'] = 'Quick download / subscribe to calendar';
 $string['ical'] = 'iCal';
index 606cac2..ca114fc 100644 (file)
@@ -200,6 +200,3 @@ $string['usercompetencystatus_idle'] = 'Idle';
 $string['usercompetencystatus_inreview'] = 'In review';
 $string['usercompetencystatus_waitingforreview'] = 'Waiting for review';
 $string['userplans'] = 'Learning plans';
-
-// Deprecated since Moodle 3.2.
-$string['invalidpersistent'] = 'Invalid persistent';
index db1cf82..d3b31d1 100644 (file)
@@ -5,34 +5,6 @@ myfilesmanage,core
 mypreferences,core_grades
 myprofile,core
 viewallmyentries,core_blog
-modchooserenable,core
-modchooserdisable,core
-invalidpersistent,core_competency
-revealpassword,core_form
-mediasettings,core_media
-legacyheading,core_media
-legacyheading_desc,core_media
-mobile,core_admin
-for,core_calendar
-context,core_message
-discussion,core_message
-emptysearchstring,core_message
-formorethan,core_message
-keywords,core_message
-messagehistory,core_message
-newsearch,core_message
-nosearchresults,core_message
-onlymycourses,core_message
-pagerefreshes,core_message
-page-message-x,core_message
-recent,core_message
-savemysettings,core_message
-search,core_message
-settingssaved,core_message
-strftimedaydatetime,core_message
-timenosee,core_message
-timesent,core_message
-userssearchresults,core_message
 loginpasswordautocomplete,core_admin
 loginpasswordautocomplete_help,core_admin
 deletecomment,core
index 3f18137..6344ad5 100644 (file)
@@ -85,6 +85,3 @@ $string['timeunit'] = 'Time unit';
 $string['timing'] = 'Timing';
 $string['unmaskpassword'] = 'Unmask';
 $string['year'] = 'Year';
-
-// Deprecated since 3.2.
-$string['revealpassword'] = 'Reveal';
index 03fe641..76372d5 100644 (file)
@@ -36,8 +36,3 @@ Where two players support the same format, enabling both increases compatibility
 $string['privacy:metadata'] = 'Media embedding does not store any personal data.';
 $string['supports'] = 'Supports';
 $string['videoextensions'] = 'Video: {$a}';
-
-// Deprecated since Moodle 3.2.
-$string['mediasettings'] = 'Media embedding';
-$string['legacyheading'] = 'Legacy media players';
-$string['legacyheading_desc'] = 'These players are not frequently used on the Web and require browser plugins that are less widely installed.';
index 93ec3a2..917d352 100644 (file)
@@ -76,7 +76,6 @@ $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will
 $string['newonlymsg'] = 'Show only new';
 $string['newmessage'] = 'New message';
 $string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
-$string['newsearch'] = 'New search';
 $string['noframesjs'] = 'Use more accessible interface';
 $string['nocontacts'] = 'No contacts';
 $string['nomessages'] = 'No messages';
@@ -180,24 +179,3 @@ $string['viewnotificationresource'] = 'Go to: {$a}';
 $string['viewunreadmessageswith'] = 'View unread messages with {$a}';
 $string['writeamessage'] = 'Write a message...';
 $string['you'] = 'You:';
-
-// Deprecated since Moodle 3.2.
-$string['context'] = 'context';
-$string['discussion'] = 'Discussion';
-$string['emptysearchstring'] = 'You must search for something';
-$string['formorethan'] = 'For more than';
-$string['keywords'] = 'Keywords';
-$string['messagehistory'] = 'Message history';
-$string['newsearch'] = 'New search';
-$string['nosearchresults'] = 'There were no results from your search';
-$string['onlymycourses'] = 'Only in my courses';
-$string['pagerefreshes'] = 'This page refreshes automatically every {$a} seconds';
-$string['page-message-x'] = 'Any message pages';
-$string['recent'] = 'Recent';
-$string['savemysettings'] = 'Save my settings';
-$string['search'] = 'Search';
-$string['settingssaved'] = 'Your settings have been saved';
-$string['strftimedaydatetime'] = '%A, %d %B %Y, %I:%M %p';
-$string['timenosee'] = 'Minutes since I was last seen online';
-$string['timesent'] = 'Time sent';
-$string['userssearchresults'] = 'Users found: {$a}';
index e3521f0..84b7020 100644 (file)
@@ -2205,10 +2205,6 @@ $string['yourwordforx'] = 'Your word for \'{$a}\'';
 $string['zippingbackup'] = 'Zipping backup';
 $string['deprecatedeventname'] = '{$a} (no longer in use)';
 
-// Deprecated since Moodle 3.2.
-$string['modchooserenable'] = 'Activity chooser on';
-$string['modchooserdisable'] = 'Activity chooser off';
-
 // Deprecated since Moodle 3.3.
 $string['deletecomment'] = 'Delete this comment';
 $string['sectionusedefaultname'] = 'Use default section name';
index b6cf1de..1aac870 100644 (file)
@@ -186,6 +186,7 @@ $string['service'] = 'Service';
 $string['servicehelpexplanation'] = 'A service is a set of functions. A service can be accessed by all users or just specified users.';
 $string['servicename'] = 'Service name';
 $string['servicenotavailable'] = 'Web service is not available (it doesn\'t exist or might be disabled)';
+$string['servicerequireslogin'] = 'Web service is not available (the session has been logged out or has expired)';
 $string['servicesbuiltin'] = 'Built-in services';
 $string['servicescustom'] = 'Custom services';
 $string['serviceusers'] = 'Authorised users';
index 8247f32..a585b99 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 954829b..c59dc5d 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 481247c..088deca 100644 (file)
@@ -25,7 +25,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
+define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Log, URL) {
 
     // Keeps track of when the user leaves the page so we know not to show an error.
     var unloading = false;
@@ -79,9 +79,14 @@ define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
         }
         // Something failed, reject the remaining promises.
         if (exception !== null) {
-            for (; i < requests.length; i++) {
-                request = requests[i];
-                request.deferred.reject(exception);
+            // If the user isn't doing anything too important, redirect to the login page.
+            if (exception.errorcode === "servicerequireslogin") {
+                window.location = URL.relativeUrl("/login/index.php");
+            } else {
+                for (; i < requests.length; i++) {
+                    request = requests[i];
+                    request.deferred.reject(exception);
+                }
             }
         }
     };
index ea2fab3..afe6e3f 100644 (file)
@@ -355,7 +355,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             });
             // If we found any matches, show the list.
             inputElement.attr('aria-expanded', true);
-            if (matchingElements) {
+            if (originalSelect.attr('data-notice')) {
+                // Display a notice rather than actual suggestions.
+                suggestionsElement.html(originalSelect.attr('data-notice'));
+            } else if (matchingElements) {
                 // We only activate the first item in the list if tags is false,
                 // because otherwise "Enter" would select the first item, instead of
                 // creating a new tag.
@@ -512,15 +515,21 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 var option = $('<option>');
                 originalSelect.append(option);
             }
-            // And add all the new ones returned from ajax.
-            $.each(processedResults, function(resultIndex, result) {
-                if (existingValues.indexOf(String(result.value)) === -1) {
-                    var option = $('<option>');
-                    option.append(result.label);
-                    option.attr('value', result.value);
-                    originalSelect.append(option);
-                }
-            });
+            if ($.isArray(processedResults)) {
+                // Add all the new ones returned from ajax.
+                $.each(processedResults, function(resultIndex, result) {
+                    if (existingValues.indexOf(String(result.value)) === -1) {
+                        var option = $('<option>');
+                        option.append(result.label);
+                        option.attr('value', result.value);
+                        originalSelect.append(option);
+                    }
+                });
+                originalSelect.attr('data-notice', '');
+            } else {
+                // The AJAX handler returned a string instead of the array.
+                originalSelect.attr('data-notice', processedResults);
+            }
             // Update the list of suggestions now from the new values in the select list.
             updateSuggestions(options, state, '', originalSelect);
             M.util.js_complete(pendingKey);
index 7285e39..75c727f 100644 (file)
@@ -132,37 +132,19 @@ class behat_config_manager {
     }
 
     /**
-     * Search feature files for set of tags.
-     *
-     * @param array $features set of feature files.
-     * @param string $tags list of tags (currently support && only.)
-     * @return array filtered list of feature files with tags.
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    public static function get_features_with_tags($features, $tags) {
-
-        debugging('Use of get_features_with_tags is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->filtered_features_with_tags($features, $tags);
+    public static function get_features_with_tags() {
+        throw new coding_exception('get_features_with_tags() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Gets the list of Moodle steps definitions
-     *
-     * Class name as a key and the filepath as value
-     *
-     * Externalized from update_config_file() to use
-     * it from the steps definitions web interface
-     *
-     * @return array
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
     public static function get_components_steps_definitions() {
-
-        debugging('Use of get_components_steps_definitions is deprecated, please see behat_config_util::get_components_contexts',
-            DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_components_contexts();
+        throw new coding_exception('get_components_steps_definitions() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
@@ -321,160 +303,59 @@ class behat_config_manager {
     }
 
     /**
-     * Behat config file specifing the main context class,
-     * the required Behat extensions and Moodle test wwwroot.
-     *
-     * @param array $features The system feature files
-     * @param array $stepsdefinitions The system steps definitions
-     * @return string
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function get_config_file_contents($features, $stepsdefinitions) {
-
-        debugging('Use of get_config_file_contents is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_config_file_contents($features, $stepsdefinitions);
+    protected static function get_config_file_contents() {
+        throw new coding_exception('get_config_file_contents() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Parse $CFG->behat_config and return the array with required config structure for behat.yml
-     *
-     * @param string $profile profile name
-     * @param array $values values for profile
-     * @return array
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function merge_behat_config($profile, $values) {
-
-        debugging('Use of merge_behat_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        self::get_behat_config_util()->get_behat_config_for_profile($profile, $values);
+    protected static function merge_behat_config() {
+        throw new coding_exception('merge_behat_config() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Parse $CFG->behat_profile and return the array with required config structure for behat.yml.
-     *
-     * $CFG->behat_profiles = array(
-     *     'profile' = array(
-     *         'browser' => 'firefox',
-     *         'tags' => '@javascript',
-     *         'wd_host' => 'http://127.0.0.1:4444/wd/hub',
-     *         'capabilities' => array(
-     *             'platform' => 'Linux',
-     *             'version' => 44
-     *         )
-     *     )
-     * );
-     *
-     * @param string $profile profile name
-     * @param array $values values for profile.
-     * @return array
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function get_behat_profile($profile, $values) {
-        // Values should be an array.
-        if (!is_array($values)) {
-            return array();
-        }
-
-        // Check suite values.
-        $behatprofilesuites = array();
-        // Fill tags information.
-        if (isset($values['tags'])) {
-            $behatprofilesuites = array(
-                'suites' => array(
-                    'default' => array(
-                        'filters' => array(
-                            'tags' => $values['tags'],
-                        )
-                    )
-                )
-            );
-        }
-
-        // Selenium2 config values.
-        $behatprofileextension = array();
-        $seleniumconfig = array();
-        if (isset($values['browser'])) {
-            $seleniumconfig['browser'] = $values['browser'];
-        }
-        if (isset($values['wd_host'])) {
-            $seleniumconfig['wd_host'] = $values['wd_host'];
-        }
-        if (isset($values['capabilities'])) {
-            $seleniumconfig['capabilities'] = $values['capabilities'];
-        }
-        if (!empty($seleniumconfig)) {
-            $behatprofileextension = array(
-                'extensions' => array(
-                    'Behat\MinkExtension' => array(
-                        'selenium2' => $seleniumconfig,
-                    )
-                )
-            );
-        }
-
-        return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
+    protected static function get_behat_profile() {
+        throw new coding_exception('get_behat_profile() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Attempt to split feature list into fairish buckets using timing information, if available.
-     * Simply add each one to lightest buckets until all files allocated.
-     * PGA = Profile Guided Allocation. I made it up just now.
-     * CAUTION: workers must agree on allocation, do not be random anywhere!
-     *
-     * @param array $features Behat feature files array
-     * @param int $nbuckets Number of buckets to divide into
-     * @param int $instance Index number of this instance
-     * @return array Feature files array, sorted into allocations
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function profile_guided_allocate($features, $nbuckets, $instance) {
-
-        debugging('Use of profile_guided_allocate is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->profile_guided_allocate($features, $nbuckets, $instance);
+    protected static function profile_guided_allocate() {
+        throw new coding_exception('profile_guided_allocate() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Overrides default config with local config values
-     *
-     * array_merge does not merge completely the array's values
-     *
-     * @param mixed $config The node of the default config
-     * @param mixed $localconfig The node of the local config
-     * @return mixed The merge result
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function merge_config($config, $localconfig) {
-
-        debugging('Use of merge_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->merge_config($config, $localconfig);
+    protected static function merge_config() {
+        throw new coding_exception('merge_config() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Cleans the path returned by get_components_with_tests() to standarize it
-     *
-     * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/
-     * @param string $path
-     * @return string The string without the last /tests part
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected final static function clean_path($path) {
-
-        debugging('Use of clean_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->clean_path($path);
+    protected final static function clean_path() {
+        throw new coding_exception('clean_path() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * The relative path where components stores their behat tests
-     *
-     * @return string
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
     protected final static function get_behat_tests_path() {
-        debugging('Use of get_behat_tests_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_behat_tests_path();
+        throw new coding_exception('get_behat_tests_path() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
 }
index 84ccd70..cd69bbc 100644 (file)
@@ -383,5 +383,8 @@ $definitions = array(
         'simplekeys' => true,
         'simpledata' => true,
         'ttl' => 1800,
+        'invalidationevents' => array(
+            'createduser',
+        )
     ),
 );
index 3493fca..e8f44f4 100644 (file)
@@ -764,6 +764,7 @@ $functions = array(
         'classpath' => 'group/externallib.php',
         'description' => 'Returns all groups in specified course.',
         'type' => 'read',
+        'ajax' => true,
         'capabilities' => 'moodle/course:managegroups'
     ),
     'core_group_get_course_user_groups' => array(
index dd3643f..9cf7394 100644 (file)
@@ -2268,5 +2268,30 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018062800.03);
     }
 
+    if ($oldversion < 2018072500.00) {
+        // Find all duplicate top level categories per context.
+        $duplicates = $DB->get_records_sql("SELECT qc1.*
+                                              FROM {question_categories} qc1
+                                              JOIN {question_categories} qc2
+                                                ON qc1.contextid = qc2.contextid AND qc1.id <> qc2.id
+                                             WHERE qc1.parent = 0 AND qc2.parent = 0
+                                          ORDER BY qc1.contextid, qc1.id");
+
+        // For each context, let the first top category to remain as top category and make the rest its children.
+        $currentcontextid = 0;
+        $chosentopid = 0;
+        foreach ($duplicates as $duplicate) {
+            if ($currentcontextid != $duplicate->contextid) {
+                $currentcontextid = $duplicate->contextid;
+                $chosentopid = $duplicate->id;
+            } else {
+                $DB->set_field('question_categories', 'parent', $chosentopid, ['id' => $duplicate->id]);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018072500.00);
+    }
+
     return true;
 }
index 9e9b519..daf12c4 100644 (file)
@@ -6394,3 +6394,222 @@ function message_delete_message($message, $userid) {
 
     return \core_message\api::delete_message($userid, $message->id);
 }
+
+/**
+ * Get all of the allowed types for all of the courses and groups
+ * the logged in user belongs to.
+ *
+ * The returned array will optionally have 5 keys:
+ *      'user' : true if the logged in user can create user events
+ *      'site' : true if the logged in user can create site events
+ *      'category' : array of course categories that the user can create events for
+ *      'course' : array of courses that the user can create events for
+ *      'group': array of groups that the user can create events for
+ *      'groupcourses' : array of courses that the groups belong to (can
+ *                       be different from the list in 'course'.
+ * @deprecated since 3.6
+ * @return array The array of allowed types.
+ */
+function calendar_get_all_allowed_types() {
+    debugging('calendar_get_all_allowed_types() is deprecated. Please use calendar_get_allowed_types() instead.',
+        DEBUG_DEVELOPER);
+
+    global $CFG, $USER, $DB;
+
+    require_once($CFG->libdir . '/enrollib.php');
+
+    $types = [];
+
+    $allowed = new stdClass();
+
+    calendar_get_allowed_types($allowed);
+
+    if ($allowed->user) {
+        $types['user'] = true;
+    }
+
+    if ($allowed->site) {
+        $types['site'] = true;
+    }
+
+    if (coursecat::has_manage_capability_on_any()) {
+        $types['category'] = coursecat::make_categories_list('moodle/category:manage');
+    }
+
+    // This function warms the context cache for the course so the calls
+    // to load the course context in calendar_get_allowed_types don't result
+    // in additional DB queries.
+    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
+
+    // We want to pre-fetch all of the groups for each course in a single
+    // query to avoid calendar_get_allowed_types from hitting the DB for
+    // each separate course.
+    $groups = groups_get_all_groups_for_courses($courses);
+
+    foreach ($courses as $course) {
+        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
+        calendar_get_allowed_types($allowed, $course, $coursegroups);
+
+        if (!empty($allowed->courses)) {
+            $types['course'][$course->id] = $course;
+        }
+
+        if (!empty($allowed->groups)) {
+            $types['groupcourses'][$course->id] = $course;
+
+            if (!isset($types['group'])) {
+                $types['group'] = array_values($allowed->groups);
+            } else {
+                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
+            }
+        }
+    }
+
+    return $types;
+}
+
+/**
+ * Gets array of all groups in a set of course.
+ *
+ * @category group
+ * @param array $courses Array of course objects or course ids.
+ * @return array Array of groups indexed by course id.
+ */
+function groups_get_all_groups_for_courses($courses) {
+    global $DB;
+
+    if (empty($courses)) {
+        return [];
+    }
+
+    $groups = [];
+    $courseids = [];
+
+    foreach ($courses as $course) {
+        $courseid = is_object($course) ? $course->id : $course;
+        $groups[$courseid] = [];
+        $courseids[] = $courseid;
+    }
+
+    $groupfields = [
+        'g.id as gid',
+        'g.courseid',
+        'g.idnumber',
+        'g.name',
+        'g.description',
+        'g.descriptionformat',
+        'g.enrolmentkey',
+        'g.picture',
+        'g.hidepicture',
+        'g.timecreated',
+        'g.timemodified'
+    ];
+
+    $groupsmembersfields = [
+        'gm.id as gmid',
+        'gm.groupid',
+        'gm.userid',
+        'gm.timeadded',
+        'gm.component',
+        'gm.itemid'
+    ];
+
+    $concatidsql = $DB->sql_concat_join("'-'", ['g.id', 'COALESCE(gm.id, 0)']) . ' AS uniqid';
+    list($courseidsql, $params) = $DB->get_in_or_equal($courseids);
+    $groupfieldssql = implode(',', $groupfields);
+    $groupmembersfieldssql = implode(',', $groupsmembersfields);
+    $sql = "SELECT {$concatidsql}, {$groupfieldssql}, {$groupmembersfieldssql}
+              FROM {groups} g
+         LEFT JOIN {groups_members} gm
+                ON gm.groupid = g.id
+             WHERE g.courseid {$courseidsql}";
+
+    $results = $DB->get_records_sql($sql, $params);
+
+    // The results will come back as a flat dataset thanks to the left
+    // join so we will need to do some post processing to blow it out
+    // into a more usable data structure.
+    //
+    // This loop will extract the distinct groups from the result set
+    // and add it's list of members to the object as a property called
+    // 'members'. Then each group will be added to the result set indexed
+    // by it's course id.
+    //
+    // The resulting data structure for $groups should be:
+    // $groups = [
+    //      '1' = [
+    //          '1' => (object) [
+    //              'id' => 1,
+    //              <rest of group properties>
+    //              'members' => [
+    //                  '1' => (object) [
+    //                      <group member properties>
+    //                  ],
+    //                  '2' => (object) [
+    //                      <group member properties>
+    //                  ]
+    //              ]
+    //          ],
+    //          '2' => (object) [
+    //              'id' => 2,
+    //              <rest of group properties>
+    //              'members' => [
+    //                  '1' => (object) [
+    //                      <group member properties>
+    //                  ],
+    //                  '3' => (object) [
+    //                      <group member properties>
+    //                  ]
+    //              ]
+    //          ]
+    //      ]
+    // ]
+    //
+    foreach ($results as $key => $result) {
+        $groupid = $result->gid;
+        $courseid = $result->courseid;
+        $coursegroups = $groups[$courseid];
+        $groupsmembersid = $result->gmid;
+        $reducefunc = function($carry, $field) use ($result) {
+            // Iterate over the groups properties and pull
+            // them out into a separate object.
+            list($prefix, $field) = explode('.', $field);
+
+            if (property_exists($result, $field)) {
+                $carry[$field] = $result->{$field};
+            }
+
+            return $carry;
+        };
+
+        if (isset($coursegroups[$groupid])) {
+            $group = $coursegroups[$groupid];
+        } else {
+            $initial = [
+                'id' => $groupid,
+                'members' => []
+            ];
+            $group = (object) array_reduce(
+                $groupfields,
+                $reducefunc,
+                $initial
+            );
+        }
+
+        if (!empty($groupsmembersid)) {
+            $initial = ['id' => $groupsmembersid];
+            $groupsmembers = (object) array_reduce(
+                $groupsmembersfields,
+                $reducefunc,
+                $initial
+            );
+
+            $group->members[$groupsmembers->userid] = $groupsmembers;
+        }
+
+        $coursegroups[$groupid] = $group;
+        $groups[$courseid] = $coursegroups;
+    }
+
+    return $groups;
+}
diff --git a/lib/editor/atto/tests/behat/disablecontrol.feature b/lib/editor/atto/tests/behat/disablecontrol.feature
new file mode 100644 (file)
index 0000000..491e6e1
--- /dev/null
@@ -0,0 +1,45 @@
+@editor @editor_atto @atto @editor_moodleform
+Feature: Atto with enable/disable function.
+  In order to test enable/disable function
+  I create a sample page to test this feature.
+  As a user
+  I need to enable/disable all buttons/plugins and content of editor if "enable/disable" feature enabled.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "activities" exist:
+      | activity | name | intro                                                                                              | course | idnumber |
+      | label    | L1   | <a href="../lib/editor/tests/fixtures/disable_control_example.php">Control Enable/Disable Atto</a> | C1     | label1   |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I follow "Control Enable/Disable Atto"
+
+  @javascript
+  Scenario: Check disable Atto editor.
+    When I set the field "mycontrol" to "Disable"
+    Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_title_button" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_bold_button_bold" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_italic_button_italic" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_link_button" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should contain "disabled"
+    And the "disabled" attribute of "button.atto_image_button" "css_element" should contain "disabled"
+    And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "false"
+
+  @javascript
+  Scenario: Check enable Atto editor.
+    When I set the field "mycontrol" to "Enable"
+    Then "button.atto_collapse_button[disabled]" "css_element" should not exist
+    And "button.atto_title_button[disabled]" "css_element" should not exist
+    And "button.atto_bold_button_bold[disabled]" "css_element" should not exist
+    And "button.atto_italic_button_italic[disabled]" "css_element" should not exist
+    And "button.atto_unorderedlist_button_insertUnorderedList[disabled]" "css_element" should not exist
+    And "button.atto_orderedlist_button_insertOrderedList[disabled]" "css_element" should not exist
+    And "button.atto_link_button[disabled]" "css_element" should not exist
+    And "button.atto_link_button_unlink[disabled]" "css_element" should not exist
+    And "button.atto_image_button[disabled]" "css_element" should not exist
+    And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "true"
index 90e0996..9fce41e 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 22df82b..0bb1434 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 1b88fea..cf12f6c 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index a28e936..b846bcd 100644 (file)
@@ -225,6 +225,12 @@ Y.extend(Editor, Y.Base, {
         // Hide the old textarea.
         this.textarea.hide();
 
+        // Set up custom event for editor updated.
+        Y.mix(Y.Node.DOM_EVENTS, {'form:editorUpdated': true});
+        this.textarea.on('form:editorUpdated', function() {
+            this.updateEditorState();
+        }, this);
+
         // Copy the text to the contenteditable div.
         this.updateFromTextArea();
 
@@ -387,6 +393,20 @@ Y.extend(Editor, Y.Base, {
         }
     },
 
+    /**
+     * Update the state of the editor.
+     */
+    updateEditorState: function() {
+        var disabled = this.textarea.hasAttribute('readonly'),
+            editorfield = Y.one('#' + this.get('elementid') + 'editable');
+        // Enable/Disable all plugins.
+        this._setPluginState(!disabled);
+        // Enable/Disable content of editor.
+        if (editorfield) {
+            editorfield.setAttribute('contenteditable', !disabled);
+        }
+    },
+
     /**
      * Register an event handle for disposal in the destructor.
      *
diff --git a/lib/editor/tests/fixtures/disable_control_example.php b/lib/editor/tests/fixtures/disable_control_example.php
new file mode 100644 (file)
index 0000000..752c0bc
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Demonstrates use of editor with enable/disable function.
+ *
+ * This fixture is only used by the Behat test.
+ *
+ * @package core_editor
+ * @copyright 2018 Jake Hau <phuchau1509@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../../config.php');
+require_once('./editor_form.php');
+
+// Behat test fixture only.
+defined('BEHAT_SITE_RUNNING') || die('Only available on Behat test server');
+
+// Require login.
+require_login();
+
+$PAGE->set_url('/lib/editor/tests/fixtures/disable_control_example.php');
+$PAGE->set_context(context_system::instance());
+
+// Create moodle form.
+$mform = new editor_form();
+
+echo $OUTPUT->header();
+
+// Display moodle form.
+$mform->display();
+
+echo $OUTPUT->footer();
diff --git a/lib/editor/tests/fixtures/editor_form.php b/lib/editor/tests/fixtures/editor_form.php
new file mode 100644 (file)
index 0000000..645728a
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Provides {@link lib/editor/tests/fixtures/editor_form} class.
+ *
+ * @package core_editor
+ * @copyright 2018 Jake Hau <phuchau1509@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Class editor_form
+ *
+ * Demonstrates use of editor with disabledIf function.
+ * This fixture is only used by the Behat test.
+ *
+ * @package core_editor
+ * @copyright 2018 Jake Hau <phuchau1509@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor_form extends moodleform {
+
+    /**
+     * Form definition. Abstract method - always override!
+     */
+    protected function definition() {
+        $mform = $this->_form;
+        $editoroptions = $this->_customdata['editoroptions'];
+
+        // Add header.
+        $mform->addElement('header', 'myheader', 'Editor in Moodle form');
+
+        // Add element control.
+        $mform->addElement('select', 'mycontrol', 'My control', ['Enable', 'Disable']);
+
+        // Add editor.
+        $mform->addElement('editor', 'myeditor', 'My Editor', null, $editoroptions);
+        $mform->setType('myeditor', PARAM_RAW);
+
+        // Add control.
+        $mform->disabledIf('myeditor', 'mycontrol', 'eq', 1);
+    }
+}
diff --git a/lib/editor/textarea/tests/behat/disablecontrol.feature b/lib/editor/textarea/tests/behat/disablecontrol.feature
new file mode 100644 (file)
index 0000000..0d82331
--- /dev/null
@@ -0,0 +1,31 @@
+@editor @editor_textarea @texarea @editor_moodleform
+Feature: Text area with enable/disable function.
+  In order to test enable/disable function
+  I set default editor is Text area editor, and I create a sample page to test this feature.
+  As a user
+  I need to enable/disable content of editor if "enable/disable" feature enabled.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "activities" exist:
+      | activity | name | intro                                                                                                   | course | idnumber |
+      | label    | L1   | <a href="../lib/editor/tests/fixtures/disable_control_example.php">Control Enable/Disable Text area</a> | C1     | label1   |
+    And I log in as "admin"
+    And I follow "Preferences" in the user menu
+    And I follow "Editor preferences"
+    And I set the field "Text editor" to "Plain text area"
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I follow "Control Enable/Disable Text area"
+
+  @javascript
+  Scenario: Check disable Text area editor.
+    When I set the field "mycontrol" to "Disable"
+    Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should contain "readonly"
+
+  @javascript
+  Scenario: Check enable Text area editor.
+    When I set the field "mycontrol" to "Enable"
+    Then "textarea#id_myeditor[readonly]" "css_element" should not exist
index b7b7904..0bcfd47 100644 (file)
@@ -96,6 +96,10 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
     if (item) {
         item.parentNode.removeChild(item);
     }
+
+    document.getElementById(editorid).addEventListener('form:editorUpdated', function() {
+        M.editor_tinymce.updateEditorState(editorid);
+    });
 };
 
 M.editor_tinymce.init_callback = function() {
@@ -110,6 +114,25 @@ M.editor_tinymce.toggle = function(id) {
     tinyMCE.execCommand('mceToggleEditor', false, id);
 };
 
+/**
+ * Update the state of the editor.
+ * @param {String} id
+ */
+M.editor_tinymce.updateEditorState = function(id) {
+    var instance = window.tinyMCE.get(id),
+        content = instance.getBody(),
+        controls = instance.controlManager.controls,
+        disabled = instance.getElement().readOnly;
+    // Enable/Disable all plugins.
+    for (var key in controls) {
+        if (controls.hasOwnProperty(key)) {
+            controls[key].setDisabled(disabled);
+        }
+    }
+    // Enable/Disable body content.
+    content.setAttribute('contenteditable', !disabled);
+};
+
 M.editor_tinymce.filepicker_callback = function(args) {
 };
 
diff --git a/lib/editor/tinymce/tests/behat/disablecontrol.feature b/lib/editor/tinymce/tests/behat/disablecontrol.feature
new file mode 100644 (file)
index 0000000..d8cc6da
--- /dev/null
@@ -0,0 +1,55 @@
+@editor @editor_tinymce @tinymce @editor_moodleform
+Feature: Tinymce with enable/disable function.
+  In order to test enable/disable function
+  I set default editor is Tinymce editor, and I create a sample page to test this feature.
+  As a user
+  I need to enable/disable all buttons/plugins and content of editor if "enable/disable" feature enabled.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "activities" exist:
+      | activity | name | intro                                                                                                 | course | idnumber |
+      | label    | L1   | <a href="../lib/editor/tests/fixtures/disable_control_example.php">Control Enable/Disable Tinymce</a> | C1     | label1   |
+    And I log in as "admin"
+    And I follow "Preferences" in the user menu
+    And I follow "Editor preferences"
+    And I set the field "Text editor" to "TinyMCE HTML editor"
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I follow "Control Enable/Disable Tinymce"
+
+  @javascript
+  Scenario: Check disable Tinymce editor.
+    When I click on "option[value=1]" "css_element" in the "select#id_mycontrol" "css_element"
+    Then the "class" attribute of "a#id_myeditor_pdw_toggle" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "table#id_myeditor_formatselect" "css_element" should contain "mceListBoxDisabled"
+    And the "class" attribute of "a#id_myeditor_bold" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_italic" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_bullist" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_numlist" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_link" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_unlink" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_moodlenolink" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_image" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_moodlemedia" "css_element" should contain "mceButtonDisabled"
+    And I switch to "id_myeditor_ifr" iframe
+    And the "contenteditable" attribute of "body" "css_element" should contain "false"
+
+  @javascript
+  Scenario: Check enable Tinymce editor.
+    When I click on "option[value=0]" "css_element" in the "select#id_mycontrol" "css_element"
+    Then the "class" attribute of "a#id_myeditor_pdw_toggle" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "table#id_myeditor_formatselect" "css_element" should contain "mceListBoxEnabled"
+    And the "class" attribute of "a#id_myeditor_bold" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "a#id_myeditor_italic" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "a#id_myeditor_bullist" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "a#id_myeditor_numlist" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "a#id_myeditor_link" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_unlink" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_moodlenolink" "css_element" should contain "mceButtonDisabled"
+    And the "class" attribute of "a#id_myeditor_image" "css_element" should contain "mceButtonEnabled"
+    And the "class" attribute of "a#id_myeditor_moodlemedia" "css_element" should contain "mceButtonEnabled"
+    And I switch to "id_myeditor_ifr" iframe
+    And the "contenteditable" attribute of "body" "css_element" should contain "true"
index 667eed8..44fbd29 100644 (file)
@@ -208,10 +208,10 @@ class external_api {
             // Do not allow access to write or delete webservices as a public user.
             if ($externalfunctioninfo->loginrequired) {
                 if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) {
-                    throw new moodle_exception('servicenotavailable', 'webservice');
+                    throw new moodle_exception('servicerequireslogin', 'webservice');
                 }
                 if (!isloggedin()) {
-                    throw new moodle_exception('servicenotavailable', 'webservice');
+                    throw new moodle_exception('servicerequireslogin', 'webservice');
                 } else {
                     require_sesskey();
                 }
index 50f5960..1c6dacc 100644 (file)
@@ -250,8 +250,10 @@ if (typeof M.form.dependencyManager === 'undefined') {
          * @param {Boolean} disabled True to disable, false to enable.
          */
         _disableElement: function(name, disabled) {
-            var els = this.elementsByName(name);
-            var filepicker = this.isFilePicker(name);
+            var els = this.elementsByName(name),
+                filepicker = this.isFilePicker(name),
+                editors = this.get('form').all('.fitem [data-fieldtype="editor"] textarea[name="' + name + '[text]"]');
+
             els.each(function(node) {
                 if (disabled) {
                     node.setAttribute('disabled', 'disabled');
@@ -271,6 +273,14 @@ if (typeof M.form.dependencyManager === 'undefined') {
                     }
                 }
             });
+            editors.each(function(editor) {
+                if (disabled) {
+                    editor.setAttribute('readonly', 'readonly');
+                } else {
+                    editor.removeAttribute('readonly', 'readonly');
+                }
+                editor.getDOMNode().dispatchEvent(new Event('form:editorUpdated'));
+            });
         },
         /**
          * Hides or shows all form elements with the given name.
index 469a012..336e372 100644 (file)
@@ -295,152 +295,6 @@ function groups_get_all_groups($courseid, $userid=0, $groupingid=0, $fields='g.*
     return $results;
 }
 
-/**
- * Gets array of all groups in a set of course.
- *
- * @category group
- * @param array $courses Array of course objects or course ids.
- * @return array Array of groups indexed by course id.
- */
-function groups_get_all_groups_for_courses($courses) {
-    global $DB;
-
-    if (empty($courses)) {
-        return [];
-    }
-
-    $groups = [];
-    $courseids = [];
-
-    foreach ($courses as $course) {
-        $courseid = is_object($course) ? $course->id : $course;
-        $groups[$courseid] = [];
-        $courseids[] = $courseid;
-    }
-
-    $groupfields = [
-        'g.id as gid',
-        'g.courseid',
-        'g.idnumber',
-        'g.name',
-        'g.description',
-        'g.descriptionformat',
-        'g.enrolmentkey',
-        'g.picture',
-        'g.hidepicture',
-        'g.timecreated',
-        'g.timemodified'
-    ];
-
-    $groupsmembersfields = [
-        'gm.id as gmid',
-        'gm.groupid',
-        'gm.userid',
-        'gm.timeadded',
-        'gm.component',
-        'gm.itemid'
-    ];
-
-    $concatidsql = $DB->sql_concat_join("'-'", ['g.id', 'COALESCE(gm.id, 0)']) . ' AS uniqid';
-    list($courseidsql, $params) = $DB->get_in_or_equal($courseids);
-    $groupfieldssql = implode(',', $groupfields);
-    $groupmembersfieldssql = implode(',', $groupsmembersfields);
-    $sql = "SELECT {$concatidsql}, {$groupfieldssql}, {$groupmembersfieldssql}
-              FROM {groups} g
-         LEFT JOIN {groups_members} gm
-                ON gm.groupid = g.id
-             WHERE g.courseid {$courseidsql}";
-
-    $results = $DB->get_records_sql($sql, $params);
-
-    // The results will come back as a flat dataset thanks to the left
-    // join so we will need to do some post processing to blow it out
-    // into a more usable data structure.
-    //
-    // This loop will extract the distinct groups from the result set
-    // and add it's list of members to the object as a property called
-    // 'members'. Then each group will be added to the result set indexed
-    // by it's course id.
-    //
-    // The resulting data structure for $groups should be:
-    // $groups = [
-    //      '1' = [
-    //          '1' => (object) [
-    //              'id' => 1,
-    //              <rest of group properties>
-    //              'members' => [
-    //                  '1' => (object) [
-    //                      <group member properties>
-    //                  ],
-    //                  '2' => (object) [
-    //                      <group member properties>
-    //                  ]
-    //              ]
-    //          ],
-    //          '2' => (object) [
-    //              'id' => 2,
-    //              <rest of group properties>
-    //              'members' => [
-    //                  '1' => (object) [
-    //                      <group member properties>
-    //                  ],
-    //                  '3' => (object) [
-    //                      <group member properties>
-    //                  ]
-    //              ]
-    //          ]
-    //      ]
-    // ]
-    //
-    foreach ($results as $key => $result) {
-        $groupid = $result->gid;
-        $courseid = $result->courseid;
-        $coursegroups = $groups[$courseid];
-        $groupsmembersid = $result->gmid;
-        $reducefunc = function($carry, $field) use ($result) {
-            // Iterate over the groups properties and pull
-            // them out into a separate object.
-            list($prefix, $field) = explode('.', $field);
-
-            if (property_exists($result, $field)) {
-                $carry[$field] = $result->{$field};
-            }
-
-            return $carry;
-        };
-
-        if (isset($coursegroups[$groupid])) {
-            $group = $coursegroups[$groupid];
-        } else {
-            $initial = [
-                'id' => $groupid,
-                'members' => []
-            ];
-            $group = (object) array_reduce(
-                $groupfields,
-                $reducefunc,
-                $initial
-            );
-        }
-
-        if (!empty($groupsmembersid)) {
-            $initial = ['id' => $groupsmembersid];
-            $groupsmembers = (object) array_reduce(
-                $groupsmembersfields,
-                $reducefunc,
-                $initial
-            );
-
-            $group->members[$groupsmembers->userid] = $groupsmembers;
-        }
-
-        $coursegroups[$groupid] = $group;
-        $groups[$courseid] = $coursegroups;
-    }
-
-    return $groups;
-}
-
 /**
  * Gets array of all groups in current user.
  *
diff --git a/lib/password_compat/lib/password.php b/lib/password_compat/lib/password.php
deleted file mode 100644 (file)
index a49a2e1..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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/>.
-
-/**
- * Deprecation notice for password_compat.
- *
- * @package    core
- * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-debugging('password_compat is now standard in all versions of PHP that Moodle supports. '
-    . 'You no longer need to include the lib/password_compat/lib/password.php',
-    DEBUG_DEVELOPER);
index 91d734c..ebd6796 100644 (file)
@@ -504,7 +504,7 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace,
     // Make a category in the parent context to move the questions to.
     if (is_null($newcategory)) {
         $newcategory = new stdClass();
-        $newcategory->parent = 0;
+        $newcategory->parent = question_get_top_category($newcontextid, true)->id;
         $newcategory->contextid = $newcontextid;
         $newcategory->name = get_string('questionsrescuedfrom', 'question', $oldplace);
         $newcategory->info = get_string('questionsrescuedfrominfo', 'question', $oldplace);
index ddd1a88..9516095 100644 (file)
@@ -41,34 +41,6 @@ use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
  */
 class behat_deprecated extends behat_base {
 
-    /**
-     * Sets the specified value to the field.
-     *
-     * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline$/
-     * @throws ElementNotFoundException Thrown by behat_base::find
-     * @param string $field
-     * @param PyStringNode $value
-     * @deprecated since Moodle 3.2 MDL-55406 - please do not use this step any more.
-     */
-    public function i_set_the_field_to_multiline($field, PyStringNode $value) {
-
-        $alternative = 'I set the field "' . $this->escape($field) . '"  to multiline:';
-        $this->deprecated_message($alternative);
-
-        $this->execute('behat_forms::i_set_the_field_to_multiline', array($field, $value));
-    }
-
-    /**
-     * Click on a given link in the moodle-actionmenu that is currently open.
-     * @Given /^I follow "(?P<link_string>(?:[^"]|\\")*)" in the open menu$/
-     * @param string $linkstring the text (or id, etc.) of the link to click.
-     * @deprecated since Moodle 3.2 MDL-55839 - please do not use this step any more.
-     */
-    public function i_follow_in_the_open_menu($linkstring) {
-        $alternative = 'I choose "' . $this->escape($linkstring) . '" from the open action menu';
-        $this->deprecated_message($alternative, true);
-    }
-
     /**
      * Navigates to the course gradebook and selects a specified item from the grade navigation tabs.
      * @Given /^I go to "(?P<gradepath_string>(?:[^"]|\\")*)" in the course gradebook$/
index 6951b15..13ac090 100644 (file)
@@ -971,6 +971,9 @@ class behat_general extends behat_base {
      * @param string $taskname Name of task e.g. 'mod_whatever\task\do_something'
      */
     public function i_run_the_scheduled_task($taskname) {
+        global $CFG;
+        require_once("{$CFG->libdir}/cronlib.php");
+
         $task = \core\task\manager::get_scheduled_task($taskname);
         if (!$task) {
             throw new DriverException('The "' . $taskname . '" scheduled task does not exist');
@@ -997,16 +1000,26 @@ class behat_general extends behat_base {
         }
 
         try {
+            // Prepare the renderer.
+            cron_prepare_core_renderer();
+
             // Discard task output as not appropriate for Behat output!
             ob_start();
             $task->execute();
             ob_end_clean();
 
+            // Restore the previous renderer.
+            cron_prepare_core_renderer(true);
+
             // Mark task complete.
             \core\task\manager::scheduled_task_complete($task);
         } catch (Exception $e) {
+            // Restore the previous renderer.
+            cron_prepare_core_renderer(true);
+
             // Mark task failed and throw exception.
             \core\task\manager::scheduled_task_failed($task);
+
             throw new DriverException('The "' . $taskname . '" scheduled task failed', 0, $e);
         }
     }
@@ -1025,24 +1038,35 @@ class behat_general extends behat_base {
      * @throws DriverException
      */
     public function i_run_all_adhoc_tasks() {
+        global $CFG, $DB;
+        require_once("{$CFG->libdir}/cronlib.php");
+
         // Do setup for cron task.
         cron_setup_user();
 
-        // Run tasks. Locking is handled by get_next_adhoc_task.
-        $now = time();
-        ob_start(); // Discard task output as not appropriate for Behat output!
-        while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) {
-
-            try {
-                $task->execute();
-
-                // Mark task complete.
-                \core\task\manager::adhoc_task_complete($task);
-            } catch (Exception $e) {
-                // Mark task failed and throw exception.
-                \core\task\manager::adhoc_task_failed($task);
-                ob_end_clean();
-                throw new DriverException('An adhoc task failed', 0, $e);
+        // Discard task output as not appropriate for Behat output!
+        ob_start();
+
+        // Run all tasks which have a scheduled runtime of before now.
+        $timenow = time();
+
+        while (!\core\task\manager::static_caches_cleared_since($timenow) &&
+                $task = \core\task\manager::get_next_adhoc_task($timenow)) {
+            // Clean the output buffer between tasks.
+            ob_clean();
+
+            // Run the task.
+            cron_run_inner_adhoc_task($task);
+
+            // Check whether the task record still exists.
+            // If a task was successful it will be removed.
+            // If it failed then it will still exist.
+            if ($DB->record_exists('task_adhoc', ['id' => $task->get_id()])) {
+                // End ouptut buffering and flush the current buffer.
+                // This should be from just the current task.
+                ob_end_flush();
+
+                throw new DriverException('An adhoc task failed', 0);
             }
         }
         ob_end_clean();
index 3641327..f9bfbee 100644 (file)
@@ -1547,168 +1547,4 @@ class core_grouplib_testcase extends advanced_testcase {
         $this->assertCount(2, $members);    // Now I see members of group 3.
         $this->assertEquals([$user1->id, $user3->id], array_keys($members), '', 0.0, 10, true);
     }
-
-    /**
-     * Test groups_get_all_groups_for_courses() method.
-     */
-    public function test_groups_get_all_groups_for_courses_no_courses() {
-        $this->resetAfterTest(true);
-        $generator = $this->getDataGenerator();
-
-        $this->assertEquals([], groups_get_all_groups_for_courses([]));
-    }
-
-    /**
-     * Test groups_get_all_groups_for_courses() method.
-     */
-    public function test_groups_get_all_groups_for_courses_with_courses() {
-        global $DB;
-
-        $this->resetAfterTest(true);
-        $generator = $this->getDataGenerator();
-
-        // Create courses.
-        $course1 = $generator->create_course(); // no groups.
-        $course2 = $generator->create_course(); // one group, no members.
-        $course3 = $generator->create_course(); // one group, one member.
-        $course4 = $generator->create_course(); // one group, multiple members.
-        $course5 = $generator->create_course(); // two groups, no members.
-        $course6 = $generator->create_course(); // two groups, one member.
-        $course7 = $generator->create_course(); // two groups, multiple members.
-
-        $courses = [$course1, $course2, $course3, $course4, $course5, $course6, $course7];
-        // Create users.
-        $user1 = $generator->create_user();
-        $user2 = $generator->create_user();
-        $user3 = $generator->create_user();
-        $user4 = $generator->create_user();
-
-        // Enrol users.
-        foreach ($courses as $course) {
-            $generator->enrol_user($user1->id, $course->id);
-            $generator->enrol_user($user2->id, $course->id);
-            $generator->enrol_user($user3->id, $course->id);
-            $generator->enrol_user($user4->id, $course->id);
-        }
-
-        // Create groups.
-        $group1 = $generator->create_group(array('courseid' => $course2->id)); // no members.
-        $group2 = $generator->create_group(array('courseid' => $course3->id)); // one member.
-        $group3 = $generator->create_group(array('courseid' => $course4->id)); // multiple members.
-        $group4 = $generator->create_group(array('courseid' => $course5->id)); // no members.
-        $group5 = $generator->create_group(array('courseid' => $course5->id)); // no members.
-        $group6 = $generator->create_group(array('courseid' => $course6->id)); // one member.
-        $group7 = $generator->create_group(array('courseid' => $course6->id)); // one member.
-        $group8 = $generator->create_group(array('courseid' => $course7->id)); // multiple members.
-        $group9 = $generator->create_group(array('courseid' => $course7->id)); // multiple members.
-
-        // Assign users to groups.
-        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id));
-        $generator->create_group_member(array('groupid' => $group6->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group7->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group8->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group8->id, 'userid' => $user2->id));
-        $generator->create_group_member(array('groupid' => $group9->id, 'userid' => $user1->id));
-        $generator->create_group_member(array('groupid' => $group9->id, 'userid' => $user2->id));
-
-        // The process of modifying group members changes the timemodified of the group.
-        // Refresh the group records.
-        $group1 = $DB->get_record('groups', ['id' => $group1->id]);
-        $group2 = $DB->get_record('groups', ['id' => $group2->id]);
-        $group3 = $DB->get_record('groups', ['id' => $group3->id]);
-        $group4 = $DB->get_record('groups', ['id' => $group4->id]);
-        $group5 = $DB->get_record('groups', ['id' => $group5->id]);
-        $group6 = $DB->get_record('groups', ['id' => $group6->id]);
-        $group7 = $DB->get_record('groups', ['id' => $group7->id]);
-        $group8 = $DB->get_record('groups', ['id' => $group8->id]);
-        $group9 = $DB->get_record('groups', ['id' => $group9->id]);
-
-        $result = groups_get_all_groups_for_courses($courses);
-        $assertpropertiesmatch = function($expected, $actual) {
-            $props = get_object_vars($expected);
-
-            foreach ($props as $name => $val) {
-                $got = $actual->{$name};
-                $this->assertEquals(
-                    $val,
-                    $actual->{$name},
-                    "Failed asserting that {$got} equals {$val} for property {$name}"
-                );
-            }
-        };
-
-        // Course 1 has no groups.
-        $this->assertEquals([], $result[$course1->id]);
-
-        // Course 2 has one group with no members.
-        $coursegroups = $result[$course2->id];
-        $coursegroup = $coursegroups[$group1->id];
-        $this->assertCount(1, $coursegroups);
-        $this->assertEquals([], $coursegroup->members);
-        $assertpropertiesmatch($group1, $coursegroup);
-
-        // Course 3 has one group with one member.
-        $coursegroups = $result[$course3->id];
-        $coursegroup = $coursegroups[$group2->id];
-        $groupmember1 = $coursegroup->members[$user1->id];
-        $this->assertCount(1, $coursegroups);
-        $this->assertCount(1, $coursegroup->members);
-        $assertpropertiesmatch($group2, $coursegroup);
-        $this->assertEquals($user1->id, $groupmember1->userid);
-
-        // Course 4 has one group with multiple members.
-        $coursegroups = $result[$course4->id];
-        $coursegroup = $coursegroups[$group3->id];
-        $groupmember1 = $coursegroup->members[$user1->id];
-        $groupmember2 = $coursegroup->members[$user2->id];
-        $this->assertCount(1, $coursegroups);
-        $this->assertCount(2, $coursegroup->members);
-        $assertpropertiesmatch($group3, $coursegroup);
-        $this->assertEquals($user1->id, $groupmember1->userid);
-        $this->assertEquals($user2->id, $groupmember2->userid);
-
-        // Course 5 has multiple groups with no members.
-        $coursegroups = $result[$course5->id];
-        $coursegroup1 = $coursegroups[$group4->id];
-        $coursegroup2 = $coursegroups[$group5->id];
-        $this->assertCount(2, $coursegroups);
-        $this->assertEquals([], $coursegroup1->members);
-        $this->assertEquals([], $coursegroup2->members);
-        $assertpropertiesmatch($group4, $coursegroup1);
-        $assertpropertiesmatch($group5, $coursegroup2);
-
-        // Course 6 has multiple groups with one member.
-        $coursegroups = $result[$course6->id];
-        $coursegroup1 = $coursegroups[$group6->id];
-        $coursegroup2 = $coursegroups[$group7->id];
-        $group1member1 = $coursegroup1->members[$user1->id];
-        $group2member1 = $coursegroup2->members[$user1->id];
-        $this->assertCount(2, $coursegroups);
-        $this->assertCount(1, $coursegroup1->members);
-        $this->assertCount(1, $coursegroup2->members);
-        $assertpropertiesmatch($group6, $coursegroup1);
-        $assertpropertiesmatch($group7, $coursegroup2);
-        $this->assertEquals($user1->id, $group1member1->userid);
-        $this->assertEquals($user1->id, $group2member1->userid);
-
-        // Course 7 has multiple groups with multiple members.
-        $coursegroups = $result[$course7->id];
-        $coursegroup1 = $coursegroups[$group8->id];
-        $coursegroup2 = $coursegroups[$group9->id];
-        $group1member1 = $coursegroup1->members[$user1->id];
-        $group1member2 = $coursegroup1->members[$user2->id];
-        $group2member1 = $coursegroup2->members[$user1->id];
-        $group2member2 = $coursegroup2->members[$user2->id];
-        $this->assertCount(2, $coursegroups);
-        $this->assertCount(2, $coursegroup1->members);
-        $this->assertCount(2, $coursegroup2->members);
-        $assertpropertiesmatch($group8, $coursegroup1);
-        $assertpropertiesmatch($group9, $coursegroup2);
-        $this->assertEquals($user1->id, $group1member1->userid);
-        $this->assertEquals($user2->id, $group1member2->userid);
-        $this->assertEquals($user1->id, $group2member1->userid);
-        $this->assertEquals($user2->id, $group2member2->userid);
-    }
 }
index dde35cf..da003ff 100644 (file)
@@ -458,6 +458,29 @@ class core_questionlib_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
     }
 
+    /**
+     * This function tests the question_save_from_deletion function when it is supposed to make a new category and
+     * move question categories to that new category.
+     */
+    public function test_question_save_from_deletion() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        list($category, $course, $quiz, $qcat, $questions) = $this->setup_quiz_and_questions();
+
+        $context = context::instance_by_id($qcat->contextid);
+
+        $newcat = question_save_from_deletion(array_column($questions, 'id'),
+                $context->get_parent_context()->id, $context->get_context_name());
+
+        // Verify that the newcat itself is not a tep level category.
+        $this->assertNotEquals(0, $newcat->parent);
+
+        // Verify there is just a single top-level category.
+        $this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
+    }
+
     public function test_question_remove_stale_questions_from_category() {
         global $DB;
         $this->resetAfterTest(true);
index 3f12ad3..9389ffd 100644 (file)
@@ -75,11 +75,11 @@ class core_string_manager_standard_testcase extends advanced_testcase {
         $this->assertFalse($stringman->string_deprecated('hidden', 'grades'));
 
         // Check deprecated string.
-        $this->assertTrue($stringman->string_deprecated('modchooserenable', 'core'));
-        $this->assertTrue($stringman->string_exists('modchooserenable', 'core'));
+        $this->assertTrue($stringman->string_deprecated('groupextendenrol', 'core'));
+        $this->assertTrue($stringman->string_exists('groupextendenrol', 'core'));
         $this->assertDebuggingNotCalled();
-        $this->assertEquals('Activity chooser on', get_string('modchooserenable', 'core'));
-        $this->assertDebuggingCalled('String [modchooserenable,core] is deprecated. '.
+        $this->assertEquals('Extend enrolment (common)', get_string('groupextendenrol', 'core'));
+        $this->assertDebuggingCalled('String [groupextendenrol,core] is deprecated. '.
             'Either you should no longer be using that string, or the string has been incorrectly deprecated, in which case you should report this as a bug. '.
             'Please refer to https://docs.moodle.org/dev/String_deprecation');
     }
index be7297d..f2e8d0b 100644 (file)
@@ -3,9 +3,25 @@ information provided here is intended especially for developers.
 
 === 3.6 ===
 
+* Custom AJAX handlers for the form autocomplete fields can now optionally return string in their processResults()
+  callback. If a string is returned, it is displayed instead of the list if suggested items. This can be used, for
+  example, to inform the user that there are too many items matching the current search criteria.
 * The following functions have been finally deprecated and can not be used any more:
-
-- external_function_info()
+  - external_function_info()
+* Following api's have been removed in behat_config_manager, please use behat_config_util instead.
+    - get_features_with_tags()
+    - get_components_steps_definitions()
+    - get_config_file_contents()
+    - merge_behat_config()
+    - get_behat_profile()
+    - profile_guided_allocate()
+    - merge_config()
+    - clean_path()
+    - get_behat_tests_path()
+* Following behat steps have been removed from core:
+    - I set the field "<field_string>" to multiline
+    - I follow "<link_string>"" in the open menu
+* Removed the lib/password_compat/lib/password.php file.
 
 === 3.5 ===
 
index 13ab17a..8dc6d31 100644 (file)
@@ -430,6 +430,8 @@ function upgrade_stale_php_files_present() {
     global $CFG;
 
     $someexamplesofremovedfiles = array(
+        // Removed in 3.6.
+        '/lib/password_compat/lib/password.php',
         // Removed in 3.5.
         '/lib/dml/mssql_native_moodle_database.php',
         '/lib/dml/mssql_native_moodle_recordset.php',
index 557f188..06775e5 100644 (file)
@@ -585,6 +585,3 @@ $string['viewsubmissiongradingtable'] = 'View submission grading table.';
 $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
 $string['workflowfilter'] = 'Workflow filter';
 $string['xofy'] = '{$a->x} of {$a->y}';
-
-// Deprecated since Moodle 3.2.
-$string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.';
index 76d8f54..e69de29 100644 (file)
@@ -1 +0,0 @@
-changegradewarning,mod_assign
\ No newline at end of file
index 6084a7f..28d233e 100644 (file)
@@ -400,17 +400,3 @@ $string['viewfromdate'] = 'Read only from';
 $string['viewtodate'] = 'Read only to';
 $string['viewtodatevalidation'] = 'The read only to date cannot be before the read only from date.';
 $string['wrongdataid'] = 'Wrong data id provided';
-
-// Deprecated since Moodle 3.2.
-$string['namedate'] = 'Date field';
-$string['namefile'] = 'File field';
-$string['namecheckbox'] = 'Checkbox field';
-$string['namelatlong'] = 'Latitude/longitude field';
-$string['namemenu'] = 'Menu field';
-$string['namemultimenu'] = 'Multiple-selection menu field';
-$string['namenumber'] = 'Number field';
-$string['namepicture'] = 'Picture field';
-$string['nameradiobutton'] = 'Radio button field';
-$string['nametext'] = 'Text field';
-$string['nametextarea'] = 'Textarea field';
-$string['nameurl'] = 'URL field';
index cf44132..e69de29 100644 (file)
@@ -1,12 +0,0 @@
-namedate,mod_data
-namefile,mod_data
-namecheckbox,mod_data
-namelatlong,mod_data
-namemenu,mod_data
-namemultimenu,mod_data
-namenumber,mod_data
-namepicture,mod_data
-nameradiobutton,mod_data
-nametext,mod_data
-nametextarea,mod_data
-nameurl,mod_data
\ No newline at end of file
index fe76fa7..e69de29 100644 (file)
@@ -1,2 +0,0 @@
-start,mod_feedback
-stop,mod_feedback
\ No newline at end of file
index f87fb98..23d1ab5 100644 (file)
@@ -284,6 +284,3 @@ $string['use_one_line_for_each_value'] = 'Use one line for each answer!';
 $string['use_this_template'] = 'Use this template';
 $string['using_templates'] = 'Use a template';
 $string['vertical'] = 'Vertical';
-// Deprecated since Moodle 3.2.
-$string['start'] = 'Start';
-$string['stop'] = 'End';
index f31971f..d629736 100644 (file)
@@ -266,8 +266,8 @@ if (empty($students)) {
     }
     if (has_capability('moodle/course:bulkmessaging', $coursecontext)) {
         echo '<div class="buttons"><br />';
-        echo '<input type="button" id="checkall" value="'.get_string('selectall').'" /> ';
-        echo '<input type="button" id="checknone" value="'.get_string('deselectall').'" /> ';
+        echo '<input type="button" id="checkall" value="'.get_string('selectall').'" class="btn btn-secondary" /> ';
+        echo '<input type="button" id="checknone" value="'.get_string('deselectall').'" class="btn btn-secondary" /> ';
         echo '</div>';
         echo '<fieldset class="clearfix">';
         echo '<legend class="ftoggler">'.get_string('send_message', 'feedback').'</legend>';
@@ -279,7 +279,7 @@ if (empty($students)) {
         print_string('formathtml');
         echo '<input type="hidden" name="format" value="'.FORMAT_HTML.'" />';
         echo '<br /><div class="buttons">';
-        echo '<input type="submit" name="send_message" value="'.get_string('send', 'feedback').'" />';
+        echo '<input type="submit" name="send_message" value="'.get_string('send', 'feedback').'" class="btn btn-secondary" />';
         echo '</div>';
         echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
         echo '<input type="hidden" name="action" value="sendmessage" />';
index 84652e4..ce227ac 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -37,32 +36,32 @@ $confirm = optional_param('confirm', 0, PARAM_INT);
 $groupid = optional_param('groupid', null, PARAM_INT);
 
 $PAGE->set_url('/mod/forum/post.php', array(
-        'reply' => $reply,
-        'forum' => $forum,
-        'edit'  => $edit,
-        'delete'=> $delete,
-        'prune' => $prune,
-        'name'  => $name,
-        'confirm'=>$confirm,
-        'groupid'=>$groupid,
-        ));
-//these page_params will be passed as hidden variables later in the form.
-$page_params = array('reply'=>$reply, 'forum'=>$forum, 'edit'=>$edit);
+    'reply' => $reply,
+    'forum' => $forum,
+    'edit'  => $edit,
+    'delete' => $delete,
+    'prune' => $prune,
+    'name'  => $name,
+    'confirm' => $confirm,
+    'groupid' => $groupid,
+));
+// These page_params will be passed as hidden variables later in the form.
+$pageparams = array('reply' => $reply, 'forum' => $forum, 'edit' => $edit);
 
 $sitecontext = context_system::instance();
 
 if (!isloggedin() or isguestuser()) {
 
     if (!isloggedin() and !get_local_referer()) {
-        // No referer+not logged in - probably coming in via email  See MDL-9052
+        // No referer+not logged in - probably coming in via email  See MDL-9052.
         require_login();
     }
 
-    if (!empty($forum)) {      // User is starting a new discussion in a forum
+    if (!empty($forum)) {      // User is starting a new discussion in a forum.
         if (! $forum = $DB->get_record('forum', array('id' => $forum))) {
             print_error('invalidforumid', 'forum');
         }
-    } else if (!empty($reply)) {      // User is writing a new reply
+    } else if (!empty($reply)) {      // User is writing a new reply.
         if (! $parent = forum_get_post_full($reply)) {
             print_error('invalidparentpostid', 'forum');
         }
@@ -77,7 +76,7 @@ if (!isloggedin() or isguestuser()) {
         print_error('invalidcourseid');
     }
 
-    if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs
+    if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs.
         print_error('invalidcoursemodule');
     } else {
         $modcontext = context_module::instance($cm->id);
@@ -95,9 +94,9 @@ if (!isloggedin() or isguestuser()) {
     exit;
 }
 
-require_login(0, false);   // Script is useless unless they're logged in
+require_login(0, false);   // Script is useless unless they're logged in.
 
-if (!empty($forum)) {      // User is starting a new discussion in a forum
+if (!empty($forum)) {      // User is starting a new discussion in a forum.
     if (! $forum = $DB->get_record("forum", array("id" => $forum))) {
         print_error('invalidforumid', 'forum');
     }
@@ -138,7 +137,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     $post = new stdClass();
     $post->course        = $course->id;
     $post->forum         = $forum->id;
-    $post->discussion    = 0;           // ie discussion # not defined yet
+    $post->discussion    = 0;           // Ie discussion # not defined yet.
     $post->parent        = 0;
     $post->subject       = '';
     $post->userid        = $USER->id;
@@ -155,7 +154,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     // Unsetting this will allow the correct return URL to be calculated later.
     unset($SESSION->fromdiscussion);
 
-} else if (!empty($reply)) {      // User is writing a new reply
+} else if (!empty($reply)) {      // User is writing a new reply.
 
     if (! $parent = forum_get_post_full($reply)) {
         print_error('invalidparentpostid', 'forum');
@@ -173,7 +172,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         print_error('invalidcoursemodule');
     }
 
-    // Ensure lang, theme, etc. is set up properly. MDL-6926
+    // Ensure lang, theme, etc. is set up properly. MDL-6926.
     $PAGE->set_cm($cm, $course, $forum);
 
     // Retrieve the contexts.
@@ -193,9 +192,9 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         print_error('nopostforum', 'forum');
     }
 
-    // Make sure user can post here
+    // Make sure user can post here.
     if (isset($cm->groupmode) && empty($course->groupmodeforce)) {
-        $groupmode =  $cm->groupmode;
+        $groupmode = $cm->groupmode;
     } else {
         $groupmode = $course->groupmode;
     }
@@ -234,7 +233,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     // Unsetting this will allow the correct return URL to be calculated later.
     unset($SESSION->fromdiscussion);
 
-} else if (!empty($edit)) {  // User is editing their own post
+} else if (!empty($edit)) {  // User is editing their own post.
 
     if (! $post = forum_get_post_full($edit)) {
         print_error('invalidpostid', 'forum');
@@ -264,12 +263,12 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
 
     if (!($forum->type == 'news' && !$post->parent && $discussion->timestart > time())) {
         if (((time() - $post->created) > $CFG->maxeditingtime) and
-                    !has_capability('mod/forum:editanypost', $modcontext)) {
+            !has_capability('mod/forum:editanypost', $modcontext)) {
             print_error('maxtimehaspassed', 'forum', '', format_time($CFG->maxeditingtime));
         }
     }
     if (($post->userid <> $USER->id) and
-                !has_capability('mod/forum:editanypost', $modcontext)) {
+        !has_capability('mod/forum:editanypost', $modcontext)) {
         print_error('cannoteditposts', 'forum');
     }
 
@@ -285,7 +284,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     // Unsetting this will allow the correct return URL to be calculated later.
     unset($SESSION->fromdiscussion);
 
-}else if (!empty($delete)) {  // User is deleting a post
+} else if (!empty($delete)) {  // User is deleting a post.
 
     if (! $post = forum_get_post_full($delete)) {
         print_error('invalidpostid', 'forum');
@@ -307,34 +306,34 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     $modcontext = context_module::instance($cm->id);
 
     if ( !(($post->userid == $USER->id && has_capability('mod/forum:deleteownpost', $modcontext))
-                || has_capability('mod/forum:deleteanypost', $modcontext)) ) {
+        || has_capability('mod/forum:deleteanypost', $modcontext)) ) {
         print_error('cannotdeletepost', 'forum');
     }
 
 
     $replycount = forum_count_replies($post);
 
-    if (!empty($confirm) && confirm_sesskey()) {    // User has confirmed the delete
-        //check user capability to delete post.
+    if (!empty($confirm) && confirm_sesskey()) {    // User has confirmed the delete.
+        // Check user capability to delete post.
         $timepassed = time() - $post->created;
         if (($timepassed > $CFG->maxeditingtime) && !has_capability('mod/forum:deleteanypost', $modcontext)) {
             print_error("cannotdeletepost", "forum",
-                        forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
+                forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
         }
 
         if ($post->totalscore) {
             notice(get_string('couldnotdeleteratings', 'rating'),
-                   forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
+                forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
 
         } else if ($replycount && !has_capability('mod/forum:deleteanypost', $modcontext)) {
             print_error("couldnotdeletereplies", "forum",
-                        forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
+                forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
 
         } else {
-            if (! $post->parent) {  // post is a discussion topic as well, so delete discussion
+            if (! $post->parent) {  // Post is a discussion topic as well, so delete discussion.
                 if ($forum->type == 'single') {
                     notice("Sorry, but you are not allowed to delete that discussion!",
-                           forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
+                        forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
                 }
                 forum_delete_discussion($discussion, false, $course, $cm, $forum);
 
@@ -371,7 +370,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         }
 
 
-    } else { // User just asked to delete something
+    } else { // User just asked to delete something.
 
         forum_set_return();
         $PAGE->navbar->add(get_string('delete', 'forum'));
@@ -381,13 +380,13 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         if ($replycount) {
             if (!has_capability('mod/forum:deleteanypost', $modcontext)) {
                 print_error("couldnotdeletereplies", "forum",
-                      forum_go_back_to(new moodle_url('/mod/forum/discuss.php', array('d' => $post->discussion), 'p'.$post->id)));
+                    forum_go_back_to(new moodle_url('/mod/forum/discuss.php', array('d' => $post->discussion), 'p'.$post->id)));
             }
             echo $OUTPUT->header();
             echo $OUTPUT->heading(format_string($forum->name), 2);
-            echo $OUTPUT->confirm(get_string("deletesureplural", "forum", $replycount+1),
-                         "post.php?delete=$delete&confirm=$delete",
-                         $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
+            echo $OUTPUT->confirm(get_string("deletesureplural", "forum", $replycount + 1),
+                "post.php?delete=$delete&confirm=$delete",
+                $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
 
             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
 
@@ -400,8 +399,8 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
             echo $OUTPUT->header();
             echo $OUTPUT->heading(format_string($forum->name), 2);
             echo $OUTPUT->confirm(get_string("deletesure", "forum", $replycount),
-                         "post.php?delete=$delete&confirm=$delete",
-                         $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
+                "post.php?delete=$delete&confirm=$delete",
+                $CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
         }
 
@@ -410,7 +409,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     die;
 
 
-} else if (!empty($prune)) {  // Pruning
+} else if (!empty($prune)) {  // Pruning.
 
     if (!$post = forum_get_post_full($prune)) {
         print_error('invalidpostid', 'forum');
@@ -427,7 +426,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     if (!$post->parent) {
         print_error('alreadyfirstpost', 'forum');
     }
-    if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $forum->course)) { // For the logs
+    if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $forum->course)) { // For the logs.
         print_error('invalidcoursemodule');
     } else {
         $modcontext = context_module::instance($cm->id);
@@ -512,7 +511,8 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     } else {
         // Display the prune form.
         $course = $DB->get_record('course', array('id' => $forum->course));
-        $PAGE->navbar->add(format_string($post->subject, true), new moodle_url('/mod/forum/discuss.php', array('d'=>$discussion->id)));
+        $subjectstr = format_string($post->subject, true);
+        $PAGE->navbar->add($subjectstr, new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)));
         $PAGE->navbar->add(get_string("prune", "forum"));
         $PAGE->set_title(format_string($discussion->name).": ".format_string($post->subject));
         $PAGE->set_heading($course->fullname);
@@ -538,46 +538,48 @@ if (!isset($coursecontext)) {
 }
 
 
-// from now on user must be logged on properly
+// From now on user must be logged on properly.
 
-if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs
+if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs.
     print_error('invalidcoursemodule');
 }
 $modcontext = context_module::instance($cm->id);
 require_login($course, false, $cm);
 
 if (isguestuser()) {
-    // just in case
+    // Just in case.
     print_error('noguest');
 }
 
-if (!isset($forum->maxattachments)) {  // TODO - delete this once we add a field to the forum table
+if (!isset($forum->maxattachments)) {  // TODO - delete this once we add a field to the forum table.
     $forum->maxattachments = 3;
 }
 
 $thresholdwarning = forum_check_throttling($forum, $cm);
-$mform_post = new mod_forum_post_form('post.php', array('course' => $course,
-                                                        'cm' => $cm,
-                                                        'coursecontext' => $coursecontext,
-                                                        'modcontext' => $modcontext,
-                                                        'forum' => $forum,
-                                                        'post' => $post,
-                                                        'subscribe' => \mod_forum\subscriptions::is_subscribed($USER->id, $forum,
-                                                                null, $cm),
-                                                        'thresholdwarning' => $thresholdwarning,
-                                                        'edit' => $edit), 'post', '', array('id' => 'mformforum'));
+$mformpost = new mod_forum_post_form('post.php', array('course' => $course,
+    'cm' => $cm,
+    'coursecontext' => $coursecontext,
+    'modcontext' => $modcontext,
+    'forum' => $forum,
+    'post' => $post,
+    'subscribe' => \mod_forum\subscriptions::is_subscribed($USER->id, $forum,
+        null, $cm),
+    'thresholdwarning' => $thresholdwarning,
+    'edit' => $edit), 'post', '', array('id' => 'mformforum'));
 
 $draftitemid = file_get_submitted_draft_itemid('attachments');
-file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment', empty($post->id)?null:$post->id, mod_forum_post_form::attachment_options($forum));
+$postid = empty($post->id) ? null : $post->id;
+$attachoptions = mod_forum_post_form::attachment_options($forum);
+file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment', $postid, $attachoptions);
 
-//load data into form NOW!
+// Load data into form NOW!
 
-if ($USER->id != $post->userid) {   // Not the original author, so add a message to the end
+if ($USER->id != $post->userid) {   // Not the original author, so add a message to the end.
     $data = new stdClass();
     $data->date = userdate($post->modified);
     if ($post->messageformat == FORMAT_HTML) {
         $data->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$USER->id.'&course='.$post->course.'">'.
-                       fullname($USER).'</a>';
+            fullname($USER).'</a>';
         $post->message .= '<p><span class="edited">('.get_string('editedby', 'forum', $data).')</span></p>';
     } else {
         $data->name = fullname($USER);
@@ -599,8 +601,9 @@ if (!empty($parent)) {
 }
 
 $postid = empty($post->id) ? null : $post->id;
-$draftid_editor = file_get_submitted_draft_itemid('message');
-$currenttext = file_prepare_draft_area($draftid_editor, $modcontext->id, 'mod_forum', 'post', $postid, mod_forum_post_form::editor_options($modcontext, $postid), $post->message);
+$draftideditor = file_get_submitted_draft_itemid('message');
+$editoropts = mod_forum_post_form::editor_options($modcontext, $postid);
+$currenttext = file_prepare_draft_area($draftideditor, $modcontext->id, 'mod_forum', 'post', $postid, $editoropts, $post->message);
 
 $manageactivities = has_capability('moodle/course:manageactivities', $coursecontext);
 if (\mod_forum\subscriptions::subscription_disabled($forum) && !$manageactivities) {
@@ -622,54 +625,47 @@ if (\mod_forum\subscriptions::subscription_disabled($forum) && !$manageactivitie
     }
 }
 
-$mform_post->set_data(array(        'attachments'=>$draftitemid,
-                                    'general'=>$heading,
-                                    'subject'=>$post->subject,
-                                    'message'=>array(
-                                        'text'=>$currenttext,
-                                        'format'=>empty($post->messageformat) ? editors_get_preferred_format() : $post->messageformat,
-                                        'itemid'=>$draftid_editor
-                                    ),
-                                    'discussionsubscribe' => $discussionsubscribe,
-                                    'mailnow'=>!empty($post->mailnow),
-                                    'userid'=>$post->userid,
-                                    'parent'=>$post->parent,
-                                    'discussion'=>$post->discussion,
-                                    'course'=>$course->id) +
-                                    $page_params +
-
-                            (isset($post->format)?array(
-                                    'format'=>$post->format):
-                                array())+
-
-                            (isset($discussion->timestart)?array(
-                                    'timestart'=>$discussion->timestart):
-                                array())+
-
-                            (isset($discussion->timeend)?array(
-                                    'timeend'=>$discussion->timeend):
-                                array())+
-
-                            (isset($discussion->pinned) ? array(
-                                     'pinned' => $discussion->pinned) :
-                                array()) +
-
-                            (isset($post->groupid)?array(
-                                    'groupid'=>$post->groupid):
-                                array())+
-
-                            (isset($discussion->id)?
-                                    array('discussion'=>$discussion->id):
-                                    array()));
-
-if ($mform_post->is_cancelled()) {
+$mformpost->set_data(
+    array(
+        'attachments' => $draftitemid,
+        'general' => $heading,
+        'subject' => $post->subject,
+        'message' => array(
+            'text' => $currenttext,
+            'format' => empty($post->messageformat) ? editors_get_preferred_format() : $post->messageformat,
+            'itemid' => $draftideditor
+        ),
+        'discussionsubscribe' => $discussionsubscribe,
+        'mailnow' => !empty($post->mailnow),
+        'userid' => $post->userid,
+        'parent' => $post->parent,
+        'discussion' => $post->discussion,
+        'course' => $course->id
+    ) +
+
+    $pageparams +
+
+    (isset($post->format) ? array('format' => $post->format) : array()) +
+
+    (isset($discussion->timestart) ? array('timestart' => $discussion->timestart) : array()) +
+
+    (isset($discussion->timeend) ? array('timeend' => $discussion->timeend) : array()) +
+
+    (isset($discussion->pinned) ? array('pinned' => $discussion->pinned) : array()) +
+
+    (isset($post->groupid) ? array('groupid' => $post->groupid) : array()) +
+
+    (isset($discussion->id) ? array('discussion' => $discussion->id) : array())
+);
+
+if ($mformpost->is_cancelled()) {
     if (!isset($discussion->id) || $forum->type === 'qanda') {
         // Q and A forums don't have a discussion page, so treat them like a new thread..
         redirect(new moodle_url('/mod/forum/view.php', array('f' => $forum->id)));
     } else {
         redirect(new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)));
     }
-} else if ($fromform = $mform_post->get_data()) {
+} else if ($fromform = $mformpost->get_data()) {
 
     if (empty($SESSION->fromurl)) {
         $errordestination = "$CFG->wwwroot/mod/forum/view.php?f=$forum->id";
@@ -683,25 +679,25 @@ if ($mform_post->is_cancelled()) {
     // WARNING: the $fromform->message array has been overwritten, do not use it anymore!
     $fromform->messagetrust  = trusttext_trusted($modcontext);
 
-    if ($fromform->edit) {           // Updating a post
+    if ($fromform->edit) {           // Updating a post.
         unset($fromform->groupid);
         $fromform->id = $fromform->edit;
         $message = '';
 
-        //fix for bug #4314
+        // Fix for bug #4314.
         if (!$realpost = $DB->get_record('forum_posts', array('id' => $fromform->id))) {
             $realpost = new stdClass();
             $realpost->userid = -1;
         }
 
 
-        // if user has edit any post capability
+        // If user has edit any post capability
         // or has either startnewdiscussion or reply capability and is editting own post
         // then he can proceed
-        // MDL-7066
+        // MDL-7066.
         if ( !(($realpost->userid == $USER->id && (has_capability('mod/forum:replypost', $modcontext)
-                            || has_capability('mod/forum:startdiscussion', $modcontext))) ||
-                            has_capability('mod/forum:editanypost', $modcontext)) ) {
+                    || has_capability('mod/forum:startdiscussion', $modcontext))) ||
+            has_capability('mod/forum:editanypost', $modcontext)) ) {
             print_error('cannotupdatepost', 'forum');
         }
 
@@ -715,7 +711,7 @@ if ($mform_post->is_cancelled()) {
                 print_error('cannotupdatepost', 'forum');
             }
 
-            $DB->set_field('forum_discussions' ,'groupid' , $fromform->groupinfo, array('firstpost' => $fromform->id));
+            $DB->set_field('forum_discussions', 'groupid', $fromform->groupinfo, array('firstpost' => $fromform->id));
         }
         // When editing first post/discussion.
         if (!$fromform->parent) {
@@ -727,14 +723,15 @@ if ($mform_post->is_cancelled()) {
                 unset($fromform->pinned);
             }
         }
-        $updatepost = $fromform; //realpost
+        $updatepost = $fromform; // Realpost.
         $updatepost->forum = $forum->id;
-        if (!forum_update_post($updatepost, $mform_post)) {
+        if (!forum_update_post($updatepost, $mformpost)) {
             print_error("couldnotupdate", "forum", $errordestination);
         }
 
-        // MDL-11818
-        if (($forum->type == 'single') && ($updatepost->parent == '0')){ // updating first post of single discussion type -> updating forum intro
+        // MDL-11818.
+        if (($forum->type == 'single') && ($updatepost->parent == '0')) {
+            // Updating first post of single discussion type -> updating forum intro.
             $forum->intro = $updatepost->message;
             $forum->timemodified = time();
             $DB->update_record("forum", $forum);
@@ -776,11 +773,11 @@ if ($mform_post->is_cancelled()) {
         $event->trigger();
 
         redirect(
-                forum_go_back_to($discussionurl),
-                $message . $subscribemessage,
-                null,
-                \core\output\notification::NOTIFY_SUCCESS
-            );
+            forum_go_back_to($discussionurl),
+            $message . $subscribemessage,
+            null,
+            \core\output\notification::NOTIFY_SUCCESS
+        );
 
     } else if ($fromform->discussion) { // Adding a new post to an existing discussion
         // Before we add this we must check that the user will not exceed the blocking threshold.
@@ -789,8 +786,8 @@ if ($mform_post->is_cancelled()) {
         unset($fromform->groupid);
         $message = '';
         $addpost = $fromform;
-        $addpost->forum=$forum->id;
-        if ($fromform->id = forum_add_new_post($addpost, $mform_post)) {
+        $addpost->forum = $forum->id;
+        if ($fromform->id = forum_add_new_post($addpost, $mformpost)) {
             $fromform->deleted = 0;
             $subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
 
@@ -824,19 +821,19 @@ if ($mform_post->is_cancelled()) {
             $event->add_record_snapshot('forum_discussions', $discussion);
             $event->trigger();
 
-            // Update completion state
-            $completion=new completion_info($course);
-            if($completion->is_enabled($cm) &&
+            // Update completion state.
+            $completion = new completion_info($course);
+            if ($completion->is_enabled($cm) &&
                 ($forum->completionreplies || $forum->completionposts)) {
-                $completion->update_state($cm,COMPLETION_COMPLETE);
+                $completion->update_state($cm, COMPLETION_COMPLETE);
             }
 
             redirect(
-                    forum_go_back_to($discussionurl),
-                    $message . $subscribemessage,
-                    null,
-                    \core\output\notification::NOTIFY_SUCCESS
-                );
+                forum_go_back_to($discussionurl),
+                $message . $subscribemessage,
+                null,
+                \core\output\notification::NOTIFY_SUCCESS
+            );
 
         } else {
             print_error("couldnotadd", "forum", $errordestination);
@@ -904,7 +901,7 @@ if ($mform_post->is_cancelled()) {
 
             $discussion->groupid = $group;
             $message = '';
-            if ($discussion->id = forum_add_discussion($discussion, $mform_post)) {
+            if ($discussion->id = forum_add_discussion($discussion, $mformpost)) {
 
                 $params = array(
                     'context' => $modcontext,
@@ -933,17 +930,17 @@ if ($mform_post->is_cancelled()) {
         // Update completion status.
         $completion = new completion_info($course);
         if ($completion->is_enabled($cm) &&
-                ($forum->completiondiscussions || $forum->completionposts)) {
+            ($forum->completiondiscussions || $forum->completionposts)) {
             $completion->update_state($cm, COMPLETION_COMPLETE);
         }
 
         // Redirect back to the discussion.
         redirect(
-                forum_go_back_to($redirectto->out()),
-                $message . $subscribemessage,
-                null,
-                \core\output\notification::NOTIFY_SUCCESS
-            );
+            forum_go_back_to($redirectto->out()),
+            $message . $subscribemessage,
+            null,
+            \core\output\notification::NOTIFY_SUCCESS
+        );
     }
 }
 
@@ -953,7 +950,7 @@ if ($mform_post->is_cancelled()) {
 // variable will be loaded with all the particulars,
 // so bring up the form.
 
-// $course, $forum are defined.  $discussion is for edit and reply only.
+// Vars $course, $forum are defined. $discussion is for edit and reply only.
 
 if ($post->discussion) {
     if (! $toppost = $DB->get_record("forum_posts", array("discussion" => $post->discussion, "parent" => 0))) {
@@ -962,7 +959,7 @@ if ($post->discussion) {
 } else {
     $toppost = new stdClass();
     $toppost->subject = ($forum->type == "news") ? get_string("addanewtopic", "forum") :
-                                                   get_string("addanewdiscussion", "forum");
+        get_string("addanewdiscussion", "forum");
 }
 
 if (empty($post->edit)) {
@@ -985,7 +982,7 @@ if ($forum->type == 'single') {
     $strdiscussionname = format_string($discussion->name).':';
 }
 
-$forcefocus = empty($reply) ? NULL : 'message';
+$forcefocus = empty($reply) ? null : 'message';
 
 if (!empty($discussion->id)) {
     $PAGE->navbar->add(format_string($toppost->subject, true), "discuss.php?d=$discussion->id");
@@ -1005,7 +1002,7 @@ $PAGE->set_heading($course->fullname);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($forum->name), 2);
 
-// checkup
+// Checkup.
 if (!empty($parent) && !forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
     print_error('cannotreply', 'forum');
 }
@@ -1014,10 +1011,10 @@ if (empty($parent) && empty($edit) && !forum_user_can_post_discussion($forum, $g
 }
 
 if ($forum->type == 'qanda'
-            && !has_capability('mod/forum:viewqandawithoutposting', $modcontext)
-            && !empty($discussion->id)
-            && !forum_user_has_posted($forum->id, $discussion->id, $USER->id)) {
-    echo $OUTPUT->notification(get_string('qandanotify','forum'));
+    && !has_capability('mod/forum:viewqandawithoutposting', $modcontext)
+    && !empty($discussion->id)
+    && !forum_user_has_posted($forum->id, $discussion->id, $USER->id)) {
+    echo $OUTPUT->notification(get_string('qandanotify', 'forum'));
 }
 
 // If there is a warning message and we are not editing a post we need to handle the warning.
@@ -1042,14 +1039,15 @@ if (!empty($parent)) {
 } else {
     if (!empty($forum->intro)) {
         echo $OUTPUT->box(format_module_intro('forum', $forum, $cm->id), 'generalbox', 'intro');
-
-        if (!empty($CFG->enableplagiarism)) {
-            require_once($CFG->libdir.'/plagiarismlib.php');
-            echo plagiarism_print_disclosure($cm->id);
-        }
     }
 }
 
+// Call print disclosure for enabled plagiarism plugins.
+if (!empty($CFG->enableplagiarism)) {
+    require_once($CFG->libdir.'/plagiarismlib.php');
+    echo plagiarism_print_disclosure($cm->id);
+}
+
 if (!empty($formheading)) {
     echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide'));
 }
@@ -1057,9 +1055,9 @@ if (!empty($formheading)) {
 $data = new StdClass();
 if (isset($postid)) {
     $data->tags = core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postid);
-    $mform_post->set_data($data);
+    $mformpost->set_data($data);
 }
 
-$mform_post->display();
+$mformpost->display();
 
 echo $OUTPUT->footer();
index 8ff8747..b3d09f6 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/quiz/db" VERSION="20180407" COMMENT="XMLDB file for Moodle mod/quiz"
+<XMLDB PATH="mod/quiz/db" VERSION="20180719" COMMENT="XMLDB file for Moodle mod/quiz"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -71,7 +71,7 @@
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="quizid" TYPE="foreign" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
         <KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
-        <KEY NAME="questioncategoryid" TYPE="foreign" FIELDS="questioncategoryid" REFTABLE="questioncategory" REFFIELDS="id"/>
+        <KEY NAME="questioncategoryid" TYPE="foreign" FIELDS="questioncategoryid" REFTABLE="question_categories" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="quizid-slot" UNIQUE="true" FIELDS="quizid, slot"/>
index f3b19ee..f1b726b 100644 (file)
@@ -106,7 +106,7 @@ function xmldb_quiz_upgrade($oldversion) {
         }
 
         // Define key questioncategoryid (foreign) to be added to quiz_slots.
-        $key = new xmldb_key('questioncategoryid', XMLDB_KEY_FOREIGN, array('questioncategoryid'), 'questioncategory', array('id'));
+        $key = new xmldb_key('questioncategoryid', XMLDB_KEY_FOREIGN, array('questioncategoryid'), 'question_categories', ['id']);
         // Launch add key questioncategoryid.
         $dbman->add_key($table, $key);
 
index db845c5..3634cb1 100644 (file)
@@ -484,7 +484,7 @@ function wiki_search_form($cm, $search = '', $subwiki = null) {
         $output .= '<input name="subwikiid" type="hidden" value="' . $subwiki->id . '" />';
     }
     $output .= '<input name="searchwikicontent" type="hidden" value="1" />';
-    $output .= '<input value="' . get_string('searchwikis', 'wiki') . '" type="submit" />';
+    $output .= '<input value="' . get_string('searchwikis', 'wiki') . '" class="btn btn-secondary" type="submit" />';
     $output .= '</fieldset>';
     $output .= '</form>';
     $output .= '</div>';
similarity index 100%
rename from pix/i/emtpy.png
rename to pix/i/empty.png
similarity index 100%
rename from pix/i/emtpy.svg
rename to pix/i/empty.svg
index 28a9c94..476942b 100644 (file)
@@ -317,8 +317,7 @@ class renderer extends \core_course_management_renderer {
                 'i/empty',
                 '',
                 'moodle',
-                array('class' => 'tree-icon', 'title' => get_string('showcategory', 'moodle', $text))
-            );
+                array('class' => 'tree-icon'));
             $icon = html_writer::span($icon, 'float-left');
         }
         $actions = \core_course\management\helper::get_category_listitem_actions($category);
index ed7856e..6b10ff7 100644 (file)
@@ -237,6 +237,10 @@ div#dock {
 .path-mod-lesson .form-inline label.form-check-label {
     display: inline-block;
 }
+.path-mod-lesson .slideshow {
+    overflow: auto;
+    padding: 15px;
+}
 #page-mod-lesson-view .branchbuttoncontainer .singlebutton button[type="submit"] {
     white-space: normal;
 }
index 75d8bfa..ae74bbc 100644 (file)
@@ -15411,6 +15411,10 @@ div#dock {
 .path-mod-lesson .form-inline label.form-check-label {
   display: inline-block; }
 
+.path-mod-lesson .slideshow {
+  overflow: auto;
+  padding: 15px; }
+
 #page-mod-lesson-view .branchbuttoncontainer .singlebutton button[type="submit"] {
   white-space: normal; }
 
index ed6680b..3855b8b 100644 (file)
@@ -118,6 +118,8 @@ a.dimmed_text:visited,
 }
 .unlist,
 .unlist li,
+.list-unstyled,
+.list-unstyled li,
 .inline-list,
 .inline-list li,
 .block .list,
index c5a4a99..0198314 100644 (file)
@@ -2420,6 +2420,8 @@ a.dimmed_text:visited,
 }
 .unlist,
 .unlist li,
+.list-unstyled,
+.list-unstyled li,
 .inline-list,
 .inline-list li,
 .block .list,
index 16fd168..b387464 100644 (file)
@@ -340,7 +340,7 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     }
 
     $mform->addElement('editor', 'description_editor', get_string('userdescription'), null, $editoroptions);
-    $mform->setType('description_editor', PARAM_CLEANHTML);
+    $mform->setType('description_editor', PARAM_RAW);
     $mform->addHelpButton('description_editor', 'userdescription');
 
     if (empty($USER->newadminuser)) {
index 44be4f4..985c402 100644 (file)
@@ -122,6 +122,9 @@ function user_create_user($user, $updatepassword = true, $triggerevent = true) {
         \core\event\user_created::create_from_userid($newuserid)->trigger();
     }
 
+    // Purge the associated caches.
+    cache_helper::purge_by_event('createduser');
+
     return $newuserid;
 }
 
index 94c5931..ac9a23e 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018072000.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018072700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.6dev (Build: 20180720)'; // Human-friendly version name
+$release  = '3.6dev (Build: 20180727)'; // Human-friendly version name
 
 $branch   = '36';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.