+++ /dev/null
-<?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);
- }
-}
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.
*
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);
$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);
$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:
$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);
$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;
$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);
'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' => []
+ ],
];
$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}';
* @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.
$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()
*/
* @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],
];
}
* 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);
--- /dev/null
+@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
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018051402;
+$plugin->version = 2018051403;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
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;
$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';
"name": "Terms & 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">
| 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"
<?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");
} ?>
</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>
$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'];
// 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);
}
}
$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';
-display_un,block_myprofile
* @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'
};
/**
*/
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);
+ });
};
/**
*/
var init = function(formId) {
var formElement = $('#' + formId);
-
- parseGroupSelect(formElement);
addCourseGroupSelectListeners(formElement);
};
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,
submitCreateUpdateForm: submitCreateUpdateForm,
getCalendarMonthData: getCalendarMonthData,
getCalendarDayData: getCalendarDayData,
- getCalendarUpcomingData: getCalendarUpcomingData
+ getCalendarUpcomingData: getCalendarUpcomingData,
+ getCourseGroupsData: getCourseGroupsData
};
});
$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');
}
* @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');
}
}
}
- 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');
}
* @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');
$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;
}
$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.
}
*/
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');
}
$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');
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') {
$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.
$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);
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()) {
$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];
$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();
}
}
-/**
- * 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.
*
*/
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) {
}
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,
);
// 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 {
$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,
];
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;
+}
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
));
}
}
-$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;
$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;
}
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"
$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']);
}
/**
'minute' => 0,
],
'eventtype' => 'group',
- 'groupid' => "{$course->id}-{$group->id}", // The form format.
+ 'groupid' => $group->id,
'groupcourseid' => $course->id,
'description' => [
'text' => '',
'minute' => 0,
],
'eventtype' => 'group',
- 'groupid' => "{$course->id}-{$group->id}", // The form format.
+ 'groupid' => $group->id,
'groupcourseid' => $course->id,
'description' => [
'text' => '',
'minute' => 0,
],
'eventtype' => 'group',
- 'groupid' => "{$course->id}-{$group->id}", // The form format.
+ 'groupid' => $group->id,
'groupcourseid' => $course->id,
'description' => [
'text' => '',
'minute' => 0,
],
'eventtype' => 'group',
- 'groupid' => "{$course->id}-{$group->id}", // The form format.
+ 'groupid' => $group->id,
'groupcourseid' => $course->id,
'description' => [
'text' => '',
$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.
// 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();
$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']);
}
}
$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';
// 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;
'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);
$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);
* @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) {
search: query,
searchanywhere: true,
page: 0,
- perpage: 30
+ perpage: MAXUSERS + 1
}
}]);
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);
}
--- /dev/null
+@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"
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.
*/
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;
}
$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));
$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.';
$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';
$string['usercompetencystatus_inreview'] = 'In review';
$string['usercompetencystatus_waitingforreview'] = 'Waiting for review';
$string['userplans'] = 'Learning plans';
-
-// Deprecated since Moodle 3.2.
-$string['invalidpersistent'] = 'Invalid persistent';
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
$string['timing'] = 'Timing';
$string['unmaskpassword'] = 'Unmask';
$string['year'] = 'Year';
-
-// Deprecated since 3.2.
-$string['revealpassword'] = 'Reveal';
$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.';
$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';
$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}';
$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';
$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';
* @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;
}
// 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);
+ }
}
}
};
});
// 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.
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);
}
/**
- * 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.');
}
/**
}
/**
- * 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.');
}
}
'simplekeys' => true,
'simpledata' => true,
'ttl' => 1800,
+ 'invalidationevents' => array(
+ 'createduser',
+ )
),
);
'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(
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;
}
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;
+}
--- /dev/null
+@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"
// 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();
}
},
+ /**
+ * 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.
*
--- /dev/null
+<?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();
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+@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
if (item) {
item.parentNode.removeChild(item);
}
+
+ document.getElementById(editorid).addEventListener('form:editorUpdated', function() {
+ M.editor_tinymce.updateEditorState(editorid);
+ });
};
M.editor_tinymce.init_callback = function() {
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) {
};
--- /dev/null
+@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"
// 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();
}
* @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');
}
}
});
+ 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.
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.
*
+++ /dev/null
-<?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);
// 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);
*/
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$/
* @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');
}
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);
}
}
* @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();
$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);
- }
}
$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);
$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');
}
=== 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 ===
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',
$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.';
-changegradewarning,mod_assign
\ No newline at end of file
$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';
-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
-start,mod_feedback
-stop,mod_feedback
\ No newline at end of file
$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';
}
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>';
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" />';
<?php
-
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
$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');
}
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);
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');
}
$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;
// 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');
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.
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;
}
// 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');
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');
}
// 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');
$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);
}
- } else { // User just asked to delete something
+ } else { // User just asked to delete something.
forum_set_return();
$PAGE->navbar->add(get_string('delete', '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);
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);
}
die;
-} else if (!empty($prune)) { // Pruning
+} else if (!empty($prune)) { // Pruning.
if (!$post = forum_get_post_full($prune)) {
print_error('invalidpostid', '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);
} 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);
}
-// 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);
}
$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) {
}
}
-$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";
// 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');
}
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) {
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);
$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.
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);
$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);
$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,
// 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
+ );
}
}
// 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))) {
} 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)) {
$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");
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');
}
}
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.
} 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'));
}
$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();
<?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"
>
<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"/>
}
// 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);
$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>';
'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);
.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;
}
.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; }
}
.unlist,
.unlist li,
+.list-unstyled,
+.list-unstyled li,
.inline-list,
.inline-list li,
.block .list,
}
.unlist,
.unlist li,
+.list-unstyled,
+.list-unstyled li,
.inline-list,
.inline-list li,
.block .list,
}
$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)) {
\core\event\user_created::create_from_userid($newuserid)->trigger();
}
+ // Purge the associated caches.
+ cache_helper::purge_by_event('createduser');
+
return $newuserid;
}
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.