diff --git a/.grunt/jsdoc/README.md b/.grunt/jsdoc/README.md index 338303ca01ff5..3e8344a71df3e 100644 --- a/.grunt/jsdoc/README.md +++ b/.grunt/jsdoc/README.md @@ -16,4 +16,4 @@ Moodle - the world's open source learning platform This generated documentation includes API documentation for JavaScript written in the AMD and ES2015 module formats within Moodle. ## Related information -See [https://docs.moodle.org/dev](https://docs.moodle.org/dev) for other related Developer Documentation. +See [https://moodledev.io](https://moodledev.io) for other related Developer Documentation. diff --git a/.grunt/jsdoc/jsdoc.conf.js b/.grunt/jsdoc/jsdoc.conf.js index 240cbe6b16a2f..29684651a1df6 100644 --- a/.grunt/jsdoc/jsdoc.conf.js +++ b/.grunt/jsdoc/jsdoc.conf.js @@ -114,7 +114,7 @@ module.exports = { ], "menu": { "Developer Docs": { - href: "https://docs.moodle.org/dev", + href: "https://moodledev.io", target: "_blank", "class": "menu-item", id: "devdocs" diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index cfadb71b13d01..c94b370310983 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -6,7 +6,7 @@ of developers, designers, teachers, testers, translators and other users. We work in universities, schools, companies and other places. You are very welcome to join us and contribute to the project. -See for the many ways you +See for the many ways you can help, not only with coding. Moodle is open to community contributions to core, though all code must go @@ -35,7 +35,7 @@ submitted patches has evolved. * New features are developed on the master branch. Bug fixes are also backported to currently supported maintenance (stable) branches. -For further details, see . +For further details, see . Moodle plugins -------------- @@ -53,4 +53,4 @@ be easily installed and updated via the Moodle administration interface. the plugins directory. We do not pull from your code repository; you must do it explicitly. -For further details, see . +For further details, see . diff --git a/README.txt b/README.txt index 729dbe4b4f2d8..15655959e1b7c 100644 --- a/README.txt +++ b/README.txt @@ -20,7 +20,7 @@ Moodle is widely used around the world by universities, schools, companies and all manner of organisations and individuals. Moodle is provided freely as open source software, under the GNU General Public -License . +License . Moodle is written in PHP and JavaScript and uses an SQL database for storing the data. diff --git a/admin/category.php b/admin/category.php index fb37fa1d7e0aa..e990383cadbd5 100644 --- a/admin/category.php +++ b/admin/category.php @@ -131,8 +131,7 @@ $outputhtml .= html_writer::end_tag('div'); } -$visiblepathtosection = array_reverse($settingspage->visiblepath); -$PAGE->set_title("$SITE->shortname: " . implode(": ",$visiblepathtosection)); +$PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $settingspage->visiblepath)); $PAGE->set_heading($SITE->fullname); if ($buttons) { $PAGE->set_button($buttons); diff --git a/admin/environment.xml b/admin/environment.xml index 29725ac270183..502d2dab073e6 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -4301,6 +4301,8 @@ + + diff --git a/admin/index.php b/admin/index.php index 27f474bae90ca..906a823985ccd 100644 --- a/admin/index.php +++ b/admin/index.php @@ -224,7 +224,7 @@ $strlicense = get_string('license'); $PAGE->navbar->add($strlicense); - $PAGE->set_title($strinstallation.' - Moodle '.$CFG->target_release); + $PAGE->set_title($strinstallation . moodle_page::TITLE_SEPARATOR . 'Moodle ' . $CFG->target_release, false); $PAGE->set_heading($strinstallation); $PAGE->set_cacheable(false); @@ -267,7 +267,7 @@ upgrade_init_javascript(); $PAGE->navbar->add($strdatabasesetup); - $PAGE->set_title($strinstallation.' - Moodle '.$CFG->target_release); + $PAGE->set_title($strinstallation . moodle_page::TITLE_SEPARATOR . $CFG->target_release, false); $PAGE->set_heading($strinstallation); $PAGE->set_cacheable(false); @@ -811,7 +811,7 @@ if (!has_capability('moodle/site:config', $context)) { // Do not throw exception display an empty page with administration menu if visible for current user. - $PAGE->set_title($SITE->fullname); + $PAGE->set_title(get_string('home')); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); echo $OUTPUT->footer(); diff --git a/admin/localplugins.php b/admin/localplugins.php index eb6862d0d799d..1d6e8f4dca63d 100644 --- a/admin/localplugins.php +++ b/admin/localplugins.php @@ -21,7 +21,7 @@ * Displays the list of found local plugins, their version (if found) and * a link to delete the local plugin. * - * @see http://docs.moodle.org/dev/Local_customisation + * @see https://moodledev.io/docs/apis/plugintypes/local * @package admin * @copyright 2010 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/admin/plagiarism.php b/admin/plagiarism.php index d0a3f5f8372f8..fcb381846a648 100644 --- a/admin/plagiarism.php +++ b/admin/plagiarism.php @@ -20,7 +20,7 @@ * Displays the list of found plagiarism plugins, their version (if found) and * a link to uninstall the plagiarism plugin. * - * @see http://docs.moodle.org/dev/Plagiarism_API + * @see https://moodledev.io/docs/apis/subsystems/plagiarism * @package admin * @copyright 2012 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/admin/renderer.php b/admin/renderer.php index 0c66010ed7305..0d9683e6b23ba 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -44,7 +44,7 @@ public function install_licence_page() { $output .= $this->heading(get_string('copyrightnotice')); $output .= $this->box($copyrightnotice, 'copyrightnotice'); $output .= html_writer::empty_tag('br'); - $output .= $this->confirm(get_string('doyouagree'), $continue, "http://docs.moodle.org/dev/License"); + $output .= $this->confirm(get_string('doyouagree'), $continue, "https://moodledev.io/general/license"); $output .= $this->footer(); return $output; @@ -746,10 +746,10 @@ protected function moodle_copyright() { ////////////////////////////////////////////////////////////////////////////////////////////////// //// IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE /// $copyrighttext = 'Moodle '. - ''.$CFG->release.'
'. + ''.$CFG->release.'
'. 'Copyright © 1999 onwards, Martin Dougiamas
'. 'and many other contributors.
'. - 'GNU Public License'; + 'GNU Public License'; ////////////////////////////////////////////////////////////////////////////////////////////////// return $this->box($copyrighttext, 'copyright'); @@ -898,7 +898,8 @@ protected function campaign_content(bool $showcampaigncontent): string { $url = "https://campaign.moodle.org/current/lms/{$lang}/install/"; $params = [ 'url' => $url, - 'iframeid' => 'campaign-content' + 'iframeid' => 'campaign-content', + 'title' => get_string('campaign', 'admin'), ]; return $this->render_from_template('core/external_content_banner', $params); @@ -919,7 +920,8 @@ protected function services_and_support_content(bool $showservicesandsupport): s $url = "https://campaign.moodle.org/current/lms/{$lang}/servicesandsupport/"; $params = [ 'url' => $url, - 'iframeid' => 'services-support-content' + 'iframeid' => 'services-support-content', + 'title' => get_string('supportandservices', 'admin'), ]; return $this->render_from_template('core/external_content_banner', $params); @@ -991,7 +993,7 @@ protected function moodle_available_update_info(\core\update\info $updateinfo) { * @return string HTML to output. */ protected function release_notes_link() { - $releasenoteslink = get_string('releasenoteslink', 'admin', 'http://docs.moodle.org/dev/Releases'); + $releasenoteslink = get_string('releasenoteslink', 'admin', 'https://moodledev.io/general/releases'); $releasenoteslink = str_replace('target="_blank"', 'onclick="this.target=\'_blank\'"', $releasenoteslink); // extremely ugly validation hack return $this->box($releasenoteslink, 'generalbox alert alert-info'); } diff --git a/admin/searchreindex.php b/admin/searchreindex.php index 8525847e56ec6..158e3fc520df9 100644 --- a/admin/searchreindex.php +++ b/admin/searchreindex.php @@ -44,7 +44,7 @@ // Start page output. $heading = get_string('gradualreindex', 'search', ''); -$PAGE->set_title($PAGE->title . ': ' . $heading); +$PAGE->set_title($areaname . ' - ' . get_string('gradualreindex', 'search', '')); $PAGE->navbar->add($heading); echo $OUTPUT->header(); echo $OUTPUT->heading($heading); diff --git a/admin/settings.php b/admin/settings.php index 30c61121b2347..48171745b3a14 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -129,9 +129,7 @@ $PAGE->set_button($buttons); } - $visiblepathtosection = array_reverse($settingspage->visiblepath); - - $PAGE->set_title("$SITE->shortname: " . implode(": ",$visiblepathtosection)); + $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $settingspage->visiblepath)); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); diff --git a/admin/settings/appearance.php b/admin/settings/appearance.php index e89c09a1f90f3..559af92570180 100644 --- a/admin/settings/appearance.php +++ b/admin/settings/appearance.php @@ -244,6 +244,18 @@ // "htmlsettings" settingpage $temp = new admin_settingpage('htmlsettings', new lang_string('htmlsettings', 'admin')); + $sitenameintitleoptions = [ + 'shortname' => new lang_string('shortname'), + 'fullname' => new lang_string('fullname'), + ]; + $sitenameintitleconfig = new admin_setting_configselect( + 'sitenameintitle', + new lang_string('sitenameintitle', 'admin'), + new lang_string('sitenameintitle_help', 'admin'), + 'shortname', + $sitenameintitleoptions + ); + $temp->add($sitenameintitleconfig); $temp->add(new admin_setting_configcheckbox('formatstringstriptags', new lang_string('stripalltitletags', 'admin'), new lang_string('configstripalltitletags', 'admin'), 1)); $temp->add(new admin_setting_emoticons()); $ADMIN->add('appearance', $temp); diff --git a/admin/tool/componentlibrary/content/library/moodle-templates.md b/admin/tool/componentlibrary/content/library/moodle-templates.md index da71004b699f7..3502219d60681 100644 --- a/admin/tool/componentlibrary/content/library/moodle-templates.md +++ b/admin/tool/componentlibrary/content/library/moodle-templates.md @@ -9,7 +9,7 @@ menu: "main" ## Moodle templates -[Moodle templates](https://docs.moodle.org/dev/Templates) are use to write HTML and Javascript using mustache files. +[Moodle templates](https://moodledev.io/docs/guides/templates) are use to write HTML and Javascript using mustache files. If you are creating your own pages in the UI Component library you can load core templates using this (shortcode) syntax: @@ -51,4 +51,3 @@ This is the result of adding the core/notification template on this page: "extraclasses": "foo bar" } {{< /mustache >}} - diff --git a/admin/tool/componentlibrary/content/moodle/components/form-elements.md b/admin/tool/componentlibrary/content/moodle/components/form-elements.md index fd87e851c1342..93e4fc6cb0495 100644 --- a/admin/tool/componentlibrary/content/moodle/components/form-elements.md +++ b/admin/tool/componentlibrary/content/moodle/components/form-elements.md @@ -11,7 +11,7 @@ tags: ## How to use moodle forms -Forms are constructed using lib/formslib.php. Using the addElement methods in php a set of different form element types can be added to a form. For more info visit the [Moodledocs](https://docs.moodle.org/dev/lib/formslib.php_Form_Definition) page for forms +Forms are constructed using lib/formslib.php. Using the addElement methods in php a set of different form element types can be added to a form. For more info visit the [Moodledocs](https://moodledev.io/docs/apis/subsystems/form) page for forms {{< php >}} $mform->addElement('button', 'intro', get_string("buttonlabel")); diff --git a/admin/tool/componentlibrary/content/moodle/components/toggle.md b/admin/tool/componentlibrary/content/moodle/components/toggle.md index f3194318d59c2..3e896d9a54357 100644 --- a/admin/tool/componentlibrary/content/moodle/components/toggle.md +++ b/admin/tool/componentlibrary/content/moodle/components/toggle.md @@ -61,7 +61,7 @@ Disabled toggle with extra classes. ## Use toggle as a template block -It is also possible to include *core/toggle* in any other template using [blocks](https://docs.moodle.org/dev/Templates#Blocks), instead of rendering it with a context. +It is also possible to include *core/toggle* in any other template using [blocks](https://moodledev.io/docs/guides/templates#blocks), instead of rendering it with a context. The parameters that you can define are: * id: Unique id for the toggle input. * extraclasses: Any extra classes added to the toggle input outer container. diff --git a/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php b/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php index 2a004d9d14965..6366df4b21071 100644 --- a/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php +++ b/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php @@ -58,7 +58,7 @@ class behat_tool_dataprivacy extends behat_base { ); /** - * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures. + * Creates the specified element. More info about available elements in https://moodledev.io/general/development/tools/behat. * * @Given /^the following data privacy "(?P(?:[^"]|\\")*)" exist:$/ * diff --git a/admin/tool/filetypes/delete.php b/admin/tool/filetypes/delete.php index db8b55f3dc788..ea154b48bbb62 100644 --- a/admin/tool/filetypes/delete.php +++ b/admin/tool/filetypes/delete.php @@ -46,7 +46,7 @@ $PAGE->navbar->add($title); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); -$PAGE->set_title($SITE->fullname. ': ' . $title); +$PAGE->set_title($title); $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->set_secondary_active_tab('server'); diff --git a/admin/tool/filetypes/edit.php b/admin/tool/filetypes/edit.php index b23187eb6e4fb..9cdd72a5a88c7 100644 --- a/admin/tool/filetypes/edit.php +++ b/admin/tool/filetypes/edit.php @@ -107,7 +107,7 @@ $PAGE->navbar->add($oldextension ? s($oldextension) : $title); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); -$PAGE->set_title($SITE->fullname. ': ' . $title); +$PAGE->set_title($title); // Display the page. echo $OUTPUT->header(); diff --git a/admin/tool/filetypes/index.php b/admin/tool/filetypes/index.php index 6ac20dcd682cd..a237731cd7ea2 100644 --- a/admin/tool/filetypes/index.php +++ b/admin/tool/filetypes/index.php @@ -34,7 +34,7 @@ $PAGE->set_url(new \moodle_url('/admin/tool/filetypes/index.php')); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); -$PAGE->set_title($SITE->fullname. ': ' . $title); +$PAGE->set_title($title); $renderer = $PAGE->get_renderer('tool_filetypes'); diff --git a/admin/tool/filetypes/revert.php b/admin/tool/filetypes/revert.php index c401f93bc1c2b..b76748d1a18ea 100644 --- a/admin/tool/filetypes/revert.php +++ b/admin/tool/filetypes/revert.php @@ -46,7 +46,7 @@ $PAGE->navbar->add($title); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); -$PAGE->set_title($SITE->fullname. ': ' . $title); +$PAGE->set_title($title); // Display the page. echo $OUTPUT->header(); diff --git a/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php b/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php index 166b4c5509a39..1f30e9c5bfa41 100644 --- a/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php +++ b/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php @@ -94,7 +94,7 @@ class behat_tool_lp_data_generators extends behat_base { ); /** - * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures. + * Creates the specified element. More info about available elements in https://moodledev.io/general/development/tools/behat. * * @Given /^the following lp "(?P(?:[^"]|\\")*)" exist:$/ * diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index 78803c5df712f..d127c78bcbf4b 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -118,8 +118,12 @@ public static function get_plugins_supporting_mobile() { $langs = $stringmanager->get_list_of_translations(true); foreach ($langs as $langid => $langname) { foreach ($addoninfo['lang'] as $stringinfo) { - $lang[$langid][$stringinfo[0]] = - $stringmanager->get_string($stringinfo[0], $stringinfo[1], null, $langid); + $lang[$langid][$stringinfo[0]] = $stringmanager->get_string( + $stringinfo[0], + $stringinfo[1] ?? '', + null, + $langid + ); } } } diff --git a/admin/tool/mobile/logout.php b/admin/tool/mobile/logout.php index ebadfec87ed65..a1881e3d50fff 100644 --- a/admin/tool/mobile/logout.php +++ b/admin/tool/mobile/logout.php @@ -56,7 +56,7 @@ $PAGE->set_url(new \moodle_url('/'.$CFG->admin.'/tool/mobile/logout.php')); $PAGE->navbar->add($title); $PAGE->set_context($context); -$PAGE->set_title($SITE->fullname. ': ' . $title); +$PAGE->set_title($title); // Display the page. echo $OUTPUT->header(); diff --git a/admin/tool/moodlenet/tests/local/remote_resource_test.php b/admin/tool/moodlenet/tests/local/remote_resource_test.php index 500f95af4b2eb..f3195fb7a9c3b 100644 --- a/admin/tool/moodlenet/tests/local/remote_resource_test.php +++ b/admin/tool/moodlenet/tests/local/remote_resource_test.php @@ -98,7 +98,15 @@ public function test_network_features() { ] ); - $this->assertGreaterThan(0, $remoteres->get_download_size()); + // We need to handle size of -1 (missing "Content-Length" header), or where it is set and greater than zero. + $this->assertThat( + $remoteres->get_download_size(), + $this->logicalOr( + $this->equalTo(-1), + $this->greaterThan(0), + ), + ); + [$path, $name] = $remoteres->download_to_requestdir(); $this->assertIsString($path); $this->assertEquals('test.html', $name); diff --git a/admin/webservice/documentation.php b/admin/webservice/documentation.php index 9ccca8b14eba9..265ec883d6c8e 100644 --- a/admin/webservice/documentation.php +++ b/admin/webservice/documentation.php @@ -29,13 +29,6 @@ admin_externalpage_setup('webservicedocumentation'); -// get all the function descriptions -$functions = $DB->get_records('external_functions', array(), 'name'); -$functiondescs = array(); -foreach ($functions as $function) { - $functiondescs[$function->name] = external_api::external_function_info($function); -} - // TODO: MDL-76078 - Incorrect inter-communication, core cannot have plugin dependencies like this. //display the documentation for all documented protocols, @@ -50,6 +43,19 @@ /// OUTPUT echo $OUTPUT->header(); +// Get all the function descriptions. +$functions = $DB->get_records('external_functions', [], 'name'); +$functiondescs = []; +foreach ($functions as $function) { + + // Skip invalid or otherwise incorrectly defined functions, otherwise the entire page is rendered inaccessible. + try { + $functiondescs[$function->name] = external_api::external_function_info($function); + } catch (Throwable $exception) { + echo $OUTPUT->notification($exception->getMessage(), \core\output\notification::NOTIFY_ERROR); + } +} + $renderer = $PAGE->get_renderer('core', 'webservice'); echo $renderer->documentation_html($functiondescs, $printableformat, $protocols, array(), $PAGE->url); diff --git a/auth/ldap/ntlmsso_attempt.php b/auth/ldap/ntlmsso_attempt.php index 6f107caba0fa1..a7c8749a56392 100644 --- a/auth/ldap/ntlmsso_attempt.php +++ b/auth/ldap/ntlmsso_attempt.php @@ -26,7 +26,7 @@ // when we've already left the page that set the timer. $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); -$PAGE->set_title("$site->fullname: $loginsite"); +$PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); diff --git a/auth/ldap/ntlmsso_finish.php b/auth/ldap/ntlmsso_finish.php index b0d4d7373b8f5..9a802ed3bcb4b 100644 --- a/auth/ldap/ntlmsso_finish.php +++ b/auth/ldap/ntlmsso_finish.php @@ -26,7 +26,7 @@ // here (and not add 3 more secs). $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); - $PAGE->set_title("$site->fullname: $loginsite"); + $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); redirect($CFG->wwwroot . '/login/index.php?authldap_skipntlmsso=1', diff --git a/auth/shibboleth/login.php b/auth/shibboleth/login.php index d4fc639ce5bc9..df23826ed2288 100644 --- a/auth/shibboleth/login.php +++ b/auth/shibboleth/login.php @@ -55,7 +55,7 @@ $PAGE->set_url('/auth/shibboleth/login.php'); $PAGE->set_context(context_system::instance()); $PAGE->navbar->add($loginsite); - $PAGE->set_title("$site->fullname: $loginsite"); + $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); $PAGE->set_pagelayout('login'); diff --git a/auth/upgrade.txt b/auth/upgrade.txt index 79a3b80d4be9a..1b0a62202fca5 100644 --- a/auth/upgrade.txt +++ b/auth/upgrade.txt @@ -21,7 +21,7 @@ information provided here is intended especially for developers. * The auth_db and auth_ldap plugins' implementations of update_user_record() have been removed and both now call the new implementation added in the base class. * Self registration plugins should use core_privacy\local\sitepolicy\manager instead of directly checking - $CFG->sitepolicy , especially in custom signup forms. See https://docs.moodle.org/dev/Site_policy_handler + $CFG->sitepolicy , especially in custom signup forms. === 3.3 === diff --git a/availability/condition/date/tests/behat/availability_date_conflict.feature b/availability/condition/date/tests/behat/availability_date_conflict.feature new file mode 100644 index 0000000000000..e9e2429167a81 --- /dev/null +++ b/availability/condition/date/tests/behat/availability_date_conflict.feature @@ -0,0 +1,105 @@ +@availability @availability_date @javascript +Feature: As a teacher I can set availability dates restriction to an activity and see a warning when conflicting dates are set + + Background: + Given the following "courses" exist: + | fullname | shortname | format | enablecompletion | + | Course 1 | C1 | topics | 1 | + And the following "users" exist: + | username | + | teacher1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | intro | introformat | course | content | contentformat | idnumber | + | page | PageName1 | PageDesc1 | 1 | C1 | Page 1 | 1 | 1 | + + Scenario: When I set dates to potential conflicting dates in the same subset, I should see a warning. + Given I am on the PageName1 "page activity editing" page logged in as teacher1 + And I expand all fieldsets + And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1" "availability_date > Date Restriction" to "4" + And I set the field "Direction" in the "1" "availability_date > Date Restriction" to "from" + And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "2" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "2" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "2" "availability_date > Date Restriction" to "6" + And I set the field "Direction" in the "2" "availability_date > Date Restriction" to "until" + And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "3" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "3" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "3" "availability_date > Date Restriction" to "6" + When I set the field "Direction" in the "3" "availability_date > Date Restriction" to "from" + Then I should see "Conflicts with other date restrictions" + + Scenario: If there are conflicting dates in the same subset, I should not see a warning if condition are separated by "any". + Given I am on the PageName1 "page activity editing" page logged in as teacher1 + And I expand all fieldsets + And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" + And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" + And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.1" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.1" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.1" "availability_date > Date Restriction" to "4" + And I set the field "Direction" in the "1.1" "availability_date > Date Restriction" to "from" + And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.2" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.2" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.2" "availability_date > Date Restriction" to "6" + And I set the field "Direction" in the "1.2" "availability_date > Date Restriction" to "until" + And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.3" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.3" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.3" "availability_date > Date Restriction" to "6" + And I set the field "Direction" in the "1.3" "availability_date > Date Restriction" to "from" + When I set the field "Required restrictions" in the "1" "core_availability > Set Of Restrictions" to "any" + Then I should not see "Conflicts with other date restrictions" + + Scenario: There should a conflicting availability dates are in the same subset separated by "all". + Given I am on the PageName1 "page activity editing" page logged in as teacher1 + And I expand all fieldsets + # Root level: Student "must" match the following. + And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" + And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" + # This is the second level: Student "must" match any of the following. + And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" + And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" + # And now the third and final level. + And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.1.1" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.1.1" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.1.1" "availability_date > Date Restriction" to "2" + And I set the field "Direction" in the "1.1.1" "availability_date > Date Restriction" to "from" + And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.1.2" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.1.2" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.1.2" "availability_date > Date Restriction" to "3" + And I set the field "Direction" in the "1.1.2" "availability_date > Date Restriction" to "until" + # Then add a restriction to the second level. + And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" + And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" + And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.2.1" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.2.1" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.2.1" "availability_date > Date Restriction" to "4" + And I set the field "Direction" in the "1.2.1" "availability_date > Date Restriction" to "from" + And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area" + And I click on "Date" "button" in the "Add restriction..." "dialogue" + And I set the field "year" in the "1.2.2" "availability_date > Date Restriction" to "2023" + And I set the field "Month" in the "1.2.2" "availability_date > Date Restriction" to "April" + And I set the field "day" in the "1.2.2" "availability_date > Date Restriction" to "3" + When I set the field "Direction" in the "1.2.2" "availability_date > Date Restriction" to "until" + # Same subset, we can detect conflicts. + Then I should see "Conflicts with other date restrictions" diff --git a/availability/condition/date/tests/behat/behat_availability_date.php b/availability/condition/date/tests/behat/behat_availability_date.php new file mode 100644 index 0000000000000..f6740a3fbe967 --- /dev/null +++ b/availability/condition/date/tests/behat/behat_availability_date.php @@ -0,0 +1,39 @@ +. +use Behat\Mink\Element\NodeElement; + +/** + * Behat availabilty-related steps definitions. + * + * @package availability_date + * @category test + * @copyright 2023 Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_availability_date extends behat_base { + /** + * Return the list of partial named selectors. + * + * @return array + */ + public static function get_partial_named_selectors(): array { + return [ + new behat_component_named_selector( + 'Date Restriction', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Date restriction')]]"] + ), + ]; + } +} diff --git a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js index e1ab688d539a9..5db031832e21c 100644 --- a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js +++ b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js @@ -65,6 +65,11 @@ M.availability_date.form.getNode = function(json) { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } + if (json.nodeUID === undefined) { + var miliTime = new Date(); + json.nodeUID = miliTime.getTime(); + } + node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } @@ -114,7 +119,7 @@ M.availability_date.form.getNode = function(json) { * gets an AJAX response. * * @method updateTime - * @param {Y.Node} component Node for plugin controls + * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the @@ -140,39 +145,53 @@ M.availability_date.form.updateTime = function(node) { M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); + value.nodeUID = node.getData('nodeUID'); }; /** - * List out Date node value in an array node. - * - * This will go through all array node and list from earlier date node to current date node. + * List out Date node value in the same branch. * - * @method convertTreeDateValue - * @param {array} tree Tree node to convert - * @param {array} arrayDateNode - * @param {array} currentNode current node. + * This will go through all array node and list nodes that are sibling of the current node. * - * @return {array} arrayDateNode + * @method findAllDateSiblings + * @param {Array} tree Tree items to convert + * @param {Number} nodeUIDToFind node UID to find. + * @return {Array|null} array of surrounding date avaiability values */ -M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { - var shouldSkip = false; - tree.forEach(function(node) { - if (shouldSkip) { - return; - } - if (node.type === 'date') { - // We go through all tree node, if we meet the current node then return. - if (node.t === parseInt(currentNode.getData('time'), 10) - && currentNode.one('select[name=direction]').get('value') == node.d) { - shouldSkip = true; - return; +M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { + var itemValue = null; + var siblingsFinderRecursive = function(itemsTree) { + var dateSiblings = []; + var nodeFound = false; + var index; + var childDates; + var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; + if (itemsTree.c !== undefined) { + var children = itemsTree.c; + for (index = 0; index < children.length; index++) { + itemValue = children.at(index); + if (itemValue.type === undefined) { + childDates = siblingsFinderRecursive(itemValue); + if (childDates) { + return childDates; + } + } + if (itemValue.type === 'date') { + // We go through all tree node, if we meet the current node then we add all nodes in the current branch. + if (nodeUIDToFind === itemValue.nodeUID) { + nodeFound = true; + } else if (currentOp === '&') { + dateSiblings.push(itemValue); + } + } + } + if (nodeFound) { + return dateSiblings; } - arrayDateNode.push(node); - } else if (node.type === undefined) { - M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); } - }); - return arrayDateNode; + return null; + }; + return siblingsFinderRecursive(tree); }; /** @@ -181,37 +200,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu * This will check current date node with all date node in tree node. * * @method checkConditionDate - * @param {array} currentNode The curent node. + * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; - if (M.core_availability.form.rootList.getValue().op === '&') { - var jsValue = M.core_availability.form.rootList.getValue().c; - var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode); - var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); - var currentNodeTime = parseInt(currentNode.getData('time'), 10); - arrayDateNode.forEach(function(checkNode) { + var currentNodeUID = currentNode.getData('nodeUID'); + var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); + var currentNodeTime = parseInt(currentNode.getData('time'), 10); + var dateSiblings = M.availability_date.form.findAllDateSiblings( + M.core_availability.form.rootList.getValue(), + currentNodeUID); + if (dateSiblings) { + dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. - if (checkNode.d === '<') { - if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { + if (dateSibling.d === '<') { + if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { - if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { + if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); - return error; - } else { - if (currentNode.one('div > .badge-warning')) { - currentNode.one('div > .badge-warning').remove(); - } - return error; } + return error; }; M.availability_date.form.fillErrors = function(errors, node) { diff --git a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js index 57b4ba56ae391..ea7f0646a1850 100644 --- a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js +++ b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js @@ -1 +1 @@ -YUI.add("moodle-availability_date-form",function(o,e){M.availability_date=M.availability_date||{},M.availability_date.form=o.Object(M.core_availability.plugin),M.availability_date.form.initInner=function(e,a){this.html=e,this.defaultTime=a},M.availability_date.form.getNode=function(e){var t,i,a=''+M.util.get_string("direction_before","availability_date")+' "+this.html,n=o.Node.create(""+a+"");return e.t!==undefined?(n.setData("time",e.t),n.all("select:not([name=direction])").each(function(e){e.set("disabled",!0)}),a=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=fromtime&time="+e.t,o.io(a,{on:{success:function(e,a){var t,i,l=o.JSON.parse(a.responseText);for(t in l)(i=n.one("select[name=x\\["+t+"\\]]")).set("value",""+l[t]),i.set("disabled",!1)},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})):n.setData("time",this.defaultTime),e.d!==undefined&&n.one("select[name=direction]").set("value",e.d),M.availability_date.form.addedEvents||(M.availability_date.form.addedEvents=!0,(a=o.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_date select[name=direction]"),a.delegate("change",function(){M.availability_date.form.updateTime(this.ancestor("span.availability_date"))},".availability_date select:not([name=direction])")),n.one("a[href=#]")&&(M.form.dateselector.init_single_date_selector(n),t=n.one("select[name=x\\[year\\]]"),i=t.set,t.set=function(e,a){i.call(t,e,a),"selectedIndex"===e&&setTimeout(function(){M.availability_date.form.updateTime(n)},0)}),n},M.availability_date.form.updateTime=function(t){var e=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=totime&year="+t.one("select[name=x\\[year\\]]").get("value")+"&month="+t.one("select[name=x\\[month\\]]").get("value")+"&day="+t.one("select[name=x\\[day\\]]").get("value")+"&hour="+t.one("select[name=x\\[hour\\]]").get("value")+"&minute="+t.one("select[name=x\\[minute\\]]").get("value");o.io(e,{on:{success:function(e,a){t.setData("time",a.responseText),M.core_availability.form.update()},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})},M.availability_date.form.fillValue=function(e,a){e.d=a.one("select[name=direction]").get("value"),e.t=parseInt(a.getData("time"),10)},M.availability_date.form.convertTreeDateValue=function(e,a,t){var i=!1;return e.forEach(function(e){i||("date"===e.type?e.t!==parseInt(t.getData("time"),10)||t.one("select[name=direction]").get("value")!=e.d?a.push(e):i=!0:e.type===undefined&&M.availability_date.form.convertTreeDateValue(e.c,a,t))}),a},M.availability_date.form.checkConditionDate=function(e){var a,t,i,l=!1;return"&"===M.core_availability.form.rootList.getValue().op?(a=M.core_availability.form.rootList.getValue().c,a=M.availability_date.form.convertTreeDateValue(a,[],e),t=e.one("select[name=direction]").get("value"),i=parseInt(e.getData("time"),10),a.forEach(function(e){return"<"===e.d?">="===t&&i>=e.t&&(l=!0):"<"===t&&i<=e.t&&(l=!0),l})):e.one("div > .badge-warning")&&e.one("div > .badge-warning").remove(),l},M.availability_date.form.fillErrors=function(e,a){M.availability_date.form.checkConditionDate(a)&&e.push("availability_date:error_dateconflict")}},"@VERSION@",{requires:["base","node","event","io","moodle-core_availability-form"]}); \ No newline at end of file +YUI.add("moodle-availability_date-form",function(o,e){M.availability_date=M.availability_date||{},M.availability_date.form=o.Object(M.core_availability.plugin),M.availability_date.form.initInner=function(e,a){this.html=e,this.defaultTime=a},M.availability_date.form.getNode=function(e){var t,i,a=''+M.util.get_string("direction_before","availability_date")+' "+this.html,l=o.Node.create(""+a+"");return e.t!==undefined?(l.setData("time",e.t),l.all("select:not([name=direction])").each(function(e){e.set("disabled",!0)}),a=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=fromtime&time="+e.t,o.io(a,{on:{success:function(e,a){var t,i,n=o.JSON.parse(a.responseText);for(t in n)(i=l.one("select[name=x\\["+t+"\\]]")).set("value",""+n[t]),i.set("disabled",!1)},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})):l.setData("time",this.defaultTime),e.nodeUID===undefined&&(a=new Date,e.nodeUID=a.getTime()),l.setData("nodeUID",e.nodeUID),e.d!==undefined&&l.one("select[name=direction]").set("value",e.d),M.availability_date.form.addedEvents||(M.availability_date.form.addedEvents=!0,(a=o.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_date select[name=direction]"),a.delegate("change",function(){M.availability_date.form.updateTime(this.ancestor("span.availability_date"))},".availability_date select:not([name=direction])")),l.one("a[href=#]")&&(M.form.dateselector.init_single_date_selector(l),t=l.one("select[name=x\\[year\\]]"),i=t.set,t.set=function(e,a){i.call(t,e,a),"selectedIndex"===e&&setTimeout(function(){M.availability_date.form.updateTime(l)},0)}),l},M.availability_date.form.updateTime=function(t){var e=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=totime&year="+t.one("select[name=x\\[year\\]]").get("value")+"&month="+t.one("select[name=x\\[month\\]]").get("value")+"&day="+t.one("select[name=x\\[day\\]]").get("value")+"&hour="+t.one("select[name=x\\[hour\\]]").get("value")+"&minute="+t.one("select[name=x\\[minute\\]]").get("value");o.io(e,{on:{success:function(e,a){t.setData("time",a.responseText),M.core_availability.form.update()},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})},M.availability_date.form.fillValue=function(e,a){e.d=a.one("select[name=direction]").get("value"),e.t=parseInt(a.getData("time"),10),e.nodeUID=a.getData("nodeUID")},M.availability_date.form.findAllDateSiblings=function(e,d){var r,c=function(e){var a,t,i,n=[],l=!1,o=e.op!==undefined?e.op:null;if(e.c!==undefined){for(i=e.c,a=0;a="===i&&n>=e.t&&(a=!0):"<"===i&&n<=e.t&&(a=!0),a}),a},M.availability_date.form.fillErrors=function(e,a){M.availability_date.form.checkConditionDate(a)&&e.push("availability_date:error_dateconflict")}},"@VERSION@",{requires:["base","node","event","io","moodle-core_availability-form"]}); \ No newline at end of file diff --git a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js index e1ab688d539a9..5db031832e21c 100644 --- a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js +++ b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js @@ -65,6 +65,11 @@ M.availability_date.form.getNode = function(json) { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } + if (json.nodeUID === undefined) { + var miliTime = new Date(); + json.nodeUID = miliTime.getTime(); + } + node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } @@ -114,7 +119,7 @@ M.availability_date.form.getNode = function(json) { * gets an AJAX response. * * @method updateTime - * @param {Y.Node} component Node for plugin controls + * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the @@ -140,39 +145,53 @@ M.availability_date.form.updateTime = function(node) { M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); + value.nodeUID = node.getData('nodeUID'); }; /** - * List out Date node value in an array node. - * - * This will go through all array node and list from earlier date node to current date node. + * List out Date node value in the same branch. * - * @method convertTreeDateValue - * @param {array} tree Tree node to convert - * @param {array} arrayDateNode - * @param {array} currentNode current node. + * This will go through all array node and list nodes that are sibling of the current node. * - * @return {array} arrayDateNode + * @method findAllDateSiblings + * @param {Array} tree Tree items to convert + * @param {Number} nodeUIDToFind node UID to find. + * @return {Array|null} array of surrounding date avaiability values */ -M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { - var shouldSkip = false; - tree.forEach(function(node) { - if (shouldSkip) { - return; - } - if (node.type === 'date') { - // We go through all tree node, if we meet the current node then return. - if (node.t === parseInt(currentNode.getData('time'), 10) - && currentNode.one('select[name=direction]').get('value') == node.d) { - shouldSkip = true; - return; +M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { + var itemValue = null; + var siblingsFinderRecursive = function(itemsTree) { + var dateSiblings = []; + var nodeFound = false; + var index; + var childDates; + var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; + if (itemsTree.c !== undefined) { + var children = itemsTree.c; + for (index = 0; index < children.length; index++) { + itemValue = children.at(index); + if (itemValue.type === undefined) { + childDates = siblingsFinderRecursive(itemValue); + if (childDates) { + return childDates; + } + } + if (itemValue.type === 'date') { + // We go through all tree node, if we meet the current node then we add all nodes in the current branch. + if (nodeUIDToFind === itemValue.nodeUID) { + nodeFound = true; + } else if (currentOp === '&') { + dateSiblings.push(itemValue); + } + } + } + if (nodeFound) { + return dateSiblings; } - arrayDateNode.push(node); - } else if (node.type === undefined) { - M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); } - }); - return arrayDateNode; + return null; + }; + return siblingsFinderRecursive(tree); }; /** @@ -181,37 +200,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu * This will check current date node with all date node in tree node. * * @method checkConditionDate - * @param {array} currentNode The curent node. + * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; - if (M.core_availability.form.rootList.getValue().op === '&') { - var jsValue = M.core_availability.form.rootList.getValue().c; - var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode); - var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); - var currentNodeTime = parseInt(currentNode.getData('time'), 10); - arrayDateNode.forEach(function(checkNode) { + var currentNodeUID = currentNode.getData('nodeUID'); + var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); + var currentNodeTime = parseInt(currentNode.getData('time'), 10); + var dateSiblings = M.availability_date.form.findAllDateSiblings( + M.core_availability.form.rootList.getValue(), + currentNodeUID); + if (dateSiblings) { + dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. - if (checkNode.d === '<') { - if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { + if (dateSibling.d === '<') { + if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { - if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { + if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); - return error; - } else { - if (currentNode.one('div > .badge-warning')) { - currentNode.one('div > .badge-warning').remove(); - } - return error; } + return error; }; M.availability_date.form.fillErrors = function(errors, node) { diff --git a/availability/condition/date/yui/src/form/js/form.js b/availability/condition/date/yui/src/form/js/form.js index ca0ca736d4440..b2d1866e19d15 100644 --- a/availability/condition/date/yui/src/form/js/form.js +++ b/availability/condition/date/yui/src/form/js/form.js @@ -63,6 +63,11 @@ M.availability_date.form.getNode = function(json) { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } + if (json.nodeUID === undefined) { + var miliTime = new Date(); + json.nodeUID = miliTime.getTime(); + } + node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } @@ -112,7 +117,7 @@ M.availability_date.form.getNode = function(json) { * gets an AJAX response. * * @method updateTime - * @param {Y.Node} component Node for plugin controls + * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the @@ -138,39 +143,53 @@ M.availability_date.form.updateTime = function(node) { M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); + value.nodeUID = node.getData('nodeUID'); }; /** - * List out Date node value in an array node. - * - * This will go through all array node and list from earlier date node to current date node. + * List out Date node value in the same branch. * - * @method convertTreeDateValue - * @param {array} tree Tree node to convert - * @param {array} arrayDateNode - * @param {array} currentNode current node. + * This will go through all array node and list nodes that are sibling of the current node. * - * @return {array} arrayDateNode + * @method findAllDateSiblings + * @param {Array} tree Tree items to convert + * @param {Number} nodeUIDToFind node UID to find. + * @return {Array|null} array of surrounding date avaiability values */ -M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, currentNode) { - var shouldSkip = false; - tree.forEach(function(node) { - if (shouldSkip) { - return; - } - if (node.type === 'date') { - // We go through all tree node, if we meet the current node then return. - if (node.t === parseInt(currentNode.getData('time'), 10) - && currentNode.one('select[name=direction]').get('value') == node.d) { - shouldSkip = true; - return; +M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { + var itemValue = null; + var siblingsFinderRecursive = function(itemsTree) { + var dateSiblings = []; + var nodeFound = false; + var index; + var childDates; + var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; + if (itemsTree.c !== undefined) { + var children = itemsTree.c; + for (index = 0; index < children.length; index++) { + itemValue = children.at(index); + if (itemValue.type === undefined) { + childDates = siblingsFinderRecursive(itemValue); + if (childDates) { + return childDates; + } + } + if (itemValue.type === 'date') { + // We go through all tree node, if we meet the current node then we add all nodes in the current branch. + if (nodeUIDToFind === itemValue.nodeUID) { + nodeFound = true; + } else if (currentOp === '&') { + dateSiblings.push(itemValue); + } + } + } + if (nodeFound) { + return dateSiblings; } - arrayDateNode.push(node); - } else if (node.type === undefined) { - M.availability_date.form.convertTreeDateValue(node.c, arrayDateNode, currentNode); } - }); - return arrayDateNode; + return null; + }; + return siblingsFinderRecursive(tree); }; /** @@ -179,37 +198,34 @@ M.availability_date.form.convertTreeDateValue = function(tree, arrayDateNode, cu * This will check current date node with all date node in tree node. * * @method checkConditionDate - * @param {array} currentNode The curent node. + * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; - if (M.core_availability.form.rootList.getValue().op === '&') { - var jsValue = M.core_availability.form.rootList.getValue().c; - var arrayDateNode = M.availability_date.form.convertTreeDateValue(jsValue, [], currentNode); - var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); - var currentNodeTime = parseInt(currentNode.getData('time'), 10); - arrayDateNode.forEach(function(checkNode) { + var currentNodeUID = currentNode.getData('nodeUID'); + var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); + var currentNodeTime = parseInt(currentNode.getData('time'), 10); + var dateSiblings = M.availability_date.form.findAllDateSiblings( + M.core_availability.form.rootList.getValue(), + currentNodeUID); + if (dateSiblings) { + dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. - if (checkNode.d === '<') { - if (currentNodeDirection === '>=' && currentNodeTime >= checkNode.t) { + if (dateSibling.d === '<') { + if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { - if (currentNodeDirection === '<' && currentNodeTime <= checkNode.t) { + if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); - return error; - } else { - if (currentNode.one('div > .badge-warning')) { - currentNode.one('div > .badge-warning').remove(); - } - return error; } + return error; }; M.availability_date.form.fillErrors = function(errors, node) { diff --git a/availability/tests/behat/behat_availability.php b/availability/tests/behat/behat_availability.php new file mode 100644 index 0000000000000..4d89157d2c1de --- /dev/null +++ b/availability/tests/behat/behat_availability.php @@ -0,0 +1,67 @@ +. + +require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); + +/** + * Availability related behat steps and selectors definitions. + * + * @package core_availability + * @category test + * @copyright 2023 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_availability extends behat_base { + + /** + * Return the list of partial named selectors. + * + * @return array + */ + public static function get_partial_named_selectors(): array { + return [ + new behat_component_named_selector( + 'Activity availability', [ + ".//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" + . "[descendant::*[contains(normalize-space(.), %locator%)]]//div[@data-region='availabilityinfo']", + ] + ), + new behat_component_named_selector( + 'Section availability', [".//li[@id = %locator%]//div[@data-region='availabilityinfo']"], + ), + new behat_component_named_selector( + 'Set Of Restrictions', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Set of')]]"], + ), + ]; + } + + /** + * Return the list of exact named selectors + * + * @return array + */ + public static function get_exact_named_selectors(): array { + return [ + new behat_component_named_selector( + 'Availability Button Area', + [ + "//h3[@data-restriction-order=%locator%]/following-sibling::div[contains(@class,'availability-inner')]/" + . "div[contains(@class,'availability-button')]", + ], + ), + ]; + } +} diff --git a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js index 34c0df0af14a8..8477127903fd5 100644 --- a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js +++ b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js @@ -560,7 +560,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) { } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); - + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; @@ -1008,6 +1008,7 @@ M.core_availability.Item.prototype.renumber = function(number) { headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** diff --git a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js index 7d34137541246..4f1927b6c2674 100644 --- a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js +++ b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js @@ -1,3 +1,3 @@ YUI.add("moodle-core_availability-form",function(d,i){M.core_availability=M.core_availability||{},M.core_availability.form={plugins:{},field:null,mainDiv:null,rootList:null,idCounter:0,restrictByGroup:null,init:function(i){var t,e,a,l,n;for(t in i)e=i[t],(a=M[e[0]].form).init.apply(a,e);if(this.field=d.one("#id_availabilityconditionsjson"),this.field.setAttribute("aria-hidden","true"),this.mainDiv=d.Node.create(''),this.field.insert(this.mainDiv,"after"),n=null,""!==(l=this.field.get("value")))try{n=d.JSON.parse(l)}catch(o){this.field.set("value","")}this.rootList=new M.core_availability.List(n,!0),this.mainDiv.appendChild(this.rootList.node),this.update(),this.rootList.renumber(),this.mainDiv.setAttribute("aria-live","polite"),this.field.ancestor("form").on("submit",function(){this.mainDiv.all("input,textarea,select").set("disabled",!0)},this),this.restrictByGroup=d.one("#restrictbygroup"),this.restrictByGroup&&(this.restrictByGroup.on("click",this.addRestrictByGroup,this),l=d.one("#id_groupmode"),n=d.one("#id_groupingid"),l&&l.on("change",this.updateRestrictByGroup,this),n&&n.on("change",this.updateRestrictByGroup,this),this.updateRestrictByGroup())},update:function(){var i=this.rootList.getValue(),t=[];this.rootList.fillErrors(t),0!==t.length&&(i.errors=t),this.field.set("value",d.JSON.stringify(i)),this.updateRestrictByGroup()},updateRestrictByGroup:function(){var i,t,e,a;this.restrictByGroup&&("&"!==this.rootList.getValue().op||(this.rootList.hasItemOfType("group")||this.rootList.hasItemOfType("grouping"))?this.restrictByGroup.set("disabled",!0):(i=d.one("#id_groupmode"),t=d.one("#id_groupingid"),e=1===Number(this.restrictByGroup.getData("groupavailability")),a=1===Number(this.restrictByGroup.getData("groupingavailability")),i&&0!==Number(i.get("value"))&&e||t&&0!==Number(t.get("value"))&&a?this.restrictByGroup.set("disabled",!1):this.restrictByGroup.set("disabled",!0)))},addRestrictByGroup:function(i){var t,e,a,l;i.preventDefault(),i=d.one("#id_groupmode"),t=d.one("#id_groupingid"),e=1===Number(this.restrictByGroup.getData("groupavailability")),a=1===Number(this.restrictByGroup.getData("groupingavailability")),t&&0!==Number(t.get("value"))&&a?l=new M.core_availability.Item({type:"grouping",id:Number(t.get("value"))},!0):i&&e&&(l=new M.core_availability.Item({type:"group"},!0)),null!==l&&(this.rootList.addChild(l),this.update(),this.rootList.renumber(),this.rootList.updateHtml())}},M.core_availability.plugin={allowAdd:!1,init:function(i,t,e){i=i.replace(/^availability_/,"");this.allowAdd=t,(M.core_availability.form.plugins[i]=this).initInner.apply(this,e)},initInner:function(){},getNode:function(){throw"getNode not implemented"},fillValue:function(){throw"fillValue not implemented"},fillErrors:function(){},focusAfterAdd:function(i){i.one("input:not([disabled]),select:not([disabled])").focus()}},M.core_availability.List=function(i,t,e){var a,l,n;if(this.children=[],t!==undefined&&(this.root=t),this.node=d.Node.create('

'+M.util.get_string("listheader_sign_before","availability")+' '+M.util.get_string("listheader_single","availability")+''+M.util.get_string("listheader_multi_before","availability")+' "+M.util.get_string("listheader_multi_after","availability")+'
'+M.util.get_string("none","moodle")+'
'),t||this.node.addClass("availability-childlist d-sm-flex align-items-center"),this.inner=this.node.one("> .availability-inner"),a=!0,t?(i&&i.show!==undefined&&(a=i.show),this.eyeIcon=new M.core_availability.EyeIcon(!1,a),this.node.one(".availability-header").get("firstChild").insert(this.eyeIcon.span,"before")):e&&(i&&i.showc!==undefined&&(a=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!1,a),this.inner.insert(this.eyeIcon.span,"before")),t||(e=new M.core_availability.DeleteIcon(this),(a=this.node.one(".availability-none")).appendChild(document.createTextNode(" ")),a.appendChild(e.span),a.appendChild(d.Node.create(''+M.util.get_string("invalid","availability")+""))),(t=d.Node.create('")).on("click",function(){this.clickAdd()},this),this.node.one("div.availability-button").appendChild(t),i){switch(i.op){case"&":case"|":this.node.one(".availability-neg").set("value","");break;case"!&":case"!|":this.node.one(".availability-neg").set("value","!")}switch(i.op){case"&":case"!&":this.node.one(".availability-op").set("value","&");break;case"|":case"!|":this.node.one(".availability-op").set("value","|")}for(l=0;l')),this.children.push(i),this.inner.one(".availability-children").appendChild(i.node)},M.core_availability.List.prototype.focusAfterAdd=function(){this.inner.one("button").focus()},M.core_availability.List.prototype.isIndividualShowIcons=function(){var i,t;if(!this.root)throw"Can only call this on root list";return i="!"===this.node.one(".availability-neg").get("value"),t="|"===this.node.one(".availability-op").get("value"),!i&&!t||i&&t},M.core_availability.List.prototype.renumber=function(i){var t,e={count:this.children.length},a=i===undefined?e.number="":(e.number=i+":",i+"."),i=M.util.get_string("setheading","availability",e);for(this.node.one("> h3").set("innerHTML",i),t=0;t .availability-children").removeAttribute("aria-hidden"),this.inner.one("> .availability-none").setAttribute("aria-hidden","true"),this.inner.one("> .availability-header").removeAttribute("aria-hidden"),1 .availability-children").setAttribute("aria-hidden","true"),this.inner.one("> .availability-none").removeAttribute("aria-hidden"),this.inner.one("> .availability-header").setAttribute("aria-hidden","true")),this.root){for(i=this.isIndividualShowIcons(),t=0;t .availability-children > .availability-connector span.label").each(function(i){i.set("innerHTML",a)})},M.core_availability.List.prototype.deleteDescendant=function(i){for(var t,e,a=0;a .availability-children").removeChild(e),M.core_availability.form.update(),this.updateHtml(),this.inner.one("> .availability-button").one("button").focus(),!0;if(t instanceof M.core_availability.List&&t.deleteDescendant(i))return!0}return!1},M.core_availability.List.prototype.clickAdd=function(){var i,t,e,a,l,n=d.Node.create('
    "),o=n.one("button"),s={dialog:null},r=n.one("ul");for(l in M.core_availability.form.plugins)M.core_availability.form.plugins[l].allowAdd&&(i=d.Node.create('
  • '),(e=d.Node.create('
    ")).on("click",this.getAddHandler(l,s),this),i.appendChild(e),a=d.Node.create('
    "),i.appendChild(a),r.appendChild(i));i=d.Node.create('
  • '),(e=d.Node.create('
    ")).on("click",this.getAddHandler(null,s),this),i.appendChild(e),a=d.Node.create('
    "),i.appendChild(a),r.appendChild(i),n={headerContent:M.util.get_string("addrestriction","availability"),bodyContent:n,additionalBaseClass:"availability-dialogue",draggable:!0,modal:!0,closeButton:!1,width:"450px"},s.dialog=new M.core.dialogue(n),s.dialog.show(),o.on("click",function(){s.dialog.destroy(),this.inner.one("> .availability-button").one("button").focus()},this)},M.core_availability.List.prototype.getAddHandler=function(t,e){return function(){var i=t?new M.core_availability.Item({type:t,creating:!0},this.root):new M.core_availability.List({c:[],showc:!0},!1,this.root);this.addChild(i),M.core_availability.form.update(),M.core_availability.form.rootList.renumber(),this.updateHtml(),e.dialog.destroy(),i.focusAfterAdd()}},M.core_availability.List.prototype.getValue=function(){var i,t={};for(t.op=this.node.one(".availability-neg").get("value")+this.node.one(".availability-op").get("value"),t.c=[],i=0;i'+M.util.get_string("missingplugin","availability")+"")):(this.plugin=M.core_availability.form.plugins[i.type],this.pluginNode=this.plugin.getNode(i),this.pluginNode.addClass("availability_"+i.type)),this.node=d.Node.create('

    '),t&&(t=!0,i.showc!==undefined&&(t=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!0,t),this.node.appendChild(this.eyeIcon.span)),this.pluginNode.addClass("availability-plugincontrols"),this.node.appendChild(this.pluginNode),i=new M.core_availability.DeleteIcon(this),this.node.appendChild(i.span),this.node.appendChild(document.createTextNode(" ")),this.node.appendChild(d.Node.create(''))},M.core_availability.Item.prototype.getValue=function(){var i={type:this.pluginType};return this.plugin&&this.plugin.fillValue(i,this.pluginNode),i},M.core_availability.Item.prototype.fillErrors=function(i){var t,e,a,l=i.length;this.plugin?this.plugin.fillErrors(i,this.pluginNode):i.push("core_availability:item_unknowntype"),t=this.node.one("> .badge-warning"),i.length===l||t.get("firstChild")?i.length===l&&t.get("firstChild")&&t.get("firstChild").remove():(l="",e=(i=i[i.length-1].split(":"))[0],a="[["+(i=i[1])+","+e+"]]",(l=M.util.get_string(i,e))===a&&(l=M.util.get_string("invalid","availability")),t.appendChild(document.createTextNode(l)))},M.core_availability.Item.prototype.renumber=function(i){var t={number:i};this.plugin?t.type=M.util.get_string("title","availability_"+this.pluginType):t.type="["+this.pluginType+"]",t.number=i+":",i=M.util.get_string("itemheading","availability",t),this.node.one("> h3").set("innerHTML",i)},M.core_availability.Item.prototype.focusAfterAdd=function(){this.plugin.focusAfterAdd(this.pluginNode)},M.core_availability.Item.prototype.pluginType=null,M.core_availability.Item.prototype.plugin=null,M.core_availability.Item.prototype.eyeIcon=null,M.core_availability.Item.prototype.node=null,M.core_availability.Item.prototype.pluginNode=null,M.core_availability.EyeIcon=function(i,t){var e,a,l,n;this.individual=i,this.span=d.Node.create(''),e=d.Node.create(""),this.span.appendChild(e),a=i?"_individual":"_all",l=function(){var i=M.util.get_string("hidden"+a,"availability");e.set("src",M.util.image_url("i/show","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("show_verb","availability"))},n=function(){var i=M.util.get_string("shown"+a,"availability");e.set("src",M.util.image_url("i/hide","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("hide_verb","availability"))},(t?n:l).call(this),this.span.on("click",i=function(i){i.preventDefault(),(this.isHidden()?n:l).call(this),M.core_availability.form.update()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.EyeIcon.prototype.individual=!1,M.core_availability.EyeIcon.prototype.span=null,M.core_availability.EyeIcon.prototype.isHidden=function(){var i=this.individual?"_individual":"_all",i=M.util.get_string("hidden"+i,"availability");return this.span.one("img").get("alt")===i},M.core_availability.DeleteIcon=function(t){var i;this.span=d.Node.create(''),i=d.Node.create(''+M.util.get_string('),this.span.appendChild(i),this.span.on("click",i=function(i){i.preventDefault(),M.core_availability.form.rootList.deleteDescendant(t),M.core_availability.form.rootList.renumber()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.DeleteIcon.prototype.span=null},"@VERSION@",{requires:["base","node","event","event-delegate","panel","moodle-core-notification-dialogue","json"]}); \ No newline at end of file +):new M.core_availability.List(n,!1,this.root),this.addChild(n)}this.node.one(".availability-neg").on("change",function(){M.core_availability.form.update(),this.updateHtml()},this),this.node.one(".availability-op").on("change",function(){M.core_availability.form.update(),this.updateHtml()},this),this.updateHtml()},M.core_availability.List.prototype.addChild=function(i){0')),this.children.push(i),this.inner.one(".availability-children").appendChild(i.node)},M.core_availability.List.prototype.focusAfterAdd=function(){this.inner.one("button").focus()},M.core_availability.List.prototype.isIndividualShowIcons=function(){var i,t;if(!this.root)throw"Can only call this on root list";return i="!"===this.node.one(".availability-neg").get("value"),t="|"===this.node.one(".availability-op").get("value"),!i&&!t||i&&t},M.core_availability.List.prototype.renumber=function(i){var t,e={count:this.children.length},a=i===undefined?e.number="":(e.number=i+":",i+"."),e=M.util.get_string("setheading","availability",e);for(this.node.one("> h3").set("innerHTML",e),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root",t=0;t .availability-children").removeAttribute("aria-hidden"),this.inner.one("> .availability-none").setAttribute("aria-hidden","true"),this.inner.one("> .availability-header").removeAttribute("aria-hidden"),1 .availability-children").setAttribute("aria-hidden","true"),this.inner.one("> .availability-none").removeAttribute("aria-hidden"),this.inner.one("> .availability-header").setAttribute("aria-hidden","true")),this.root){for(i=this.isIndividualShowIcons(),t=0;t .availability-children > .availability-connector span.label").each(function(i){i.set("innerHTML",a)})},M.core_availability.List.prototype.deleteDescendant=function(i){for(var t,e,a=0;a .availability-children").removeChild(e),M.core_availability.form.update(),this.updateHtml(),this.inner.one("> .availability-button").one("button").focus(),!0;if(t instanceof M.core_availability.List&&t.deleteDescendant(i))return!0}return!1},M.core_availability.List.prototype.clickAdd=function(){var i,t,e,a,l,n=d.Node.create('
      "),o=n.one("button"),s={dialog:null},r=n.one("ul");for(l in M.core_availability.form.plugins)M.core_availability.form.plugins[l].allowAdd&&(i=d.Node.create('
    • '),(e=d.Node.create('
      ")).on("click",this.getAddHandler(l,s),this),i.appendChild(e),a=d.Node.create('
      "),i.appendChild(a),r.appendChild(i));i=d.Node.create('
    • '),(e=d.Node.create('
      ")).on("click",this.getAddHandler(null,s),this),i.appendChild(e),a=d.Node.create('
      "),i.appendChild(a),r.appendChild(i),n={headerContent:M.util.get_string("addrestriction","availability"),bodyContent:n,additionalBaseClass:"availability-dialogue",draggable:!0,modal:!0,closeButton:!1,width:"450px"},s.dialog=new M.core.dialogue(n),s.dialog.show(),o.on("click",function(){s.dialog.destroy(),this.inner.one("> .availability-button").one("button").focus()},this)},M.core_availability.List.prototype.getAddHandler=function(t,e){return function(){var i=t?new M.core_availability.Item({type:t,creating:!0},this.root):new M.core_availability.List({c:[],showc:!0},!1,this.root);this.addChild(i),M.core_availability.form.update(),M.core_availability.form.rootList.renumber(),this.updateHtml(),e.dialog.destroy(),i.focusAfterAdd()}},M.core_availability.List.prototype.getValue=function(){var i,t={};for(t.op=this.node.one(".availability-neg").get("value")+this.node.one(".availability-op").get("value"),t.c=[],i=0;i'+M.util.get_string("missingplugin","availability")+"")):(this.plugin=M.core_availability.form.plugins[i.type],this.pluginNode=this.plugin.getNode(i),this.pluginNode.addClass("availability_"+i.type)),this.node=d.Node.create('

      '),t&&(t=!0,i.showc!==undefined&&(t=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!0,t),this.node.appendChild(this.eyeIcon.span)),this.pluginNode.addClass("availability-plugincontrols"),this.node.appendChild(this.pluginNode),i=new M.core_availability.DeleteIcon(this),this.node.appendChild(i.span),this.node.appendChild(document.createTextNode(" ")),this.node.appendChild(d.Node.create(''))},M.core_availability.Item.prototype.getValue=function(){var i={type:this.pluginType};return this.plugin&&this.plugin.fillValue(i,this.pluginNode),i},M.core_availability.Item.prototype.fillErrors=function(i){var t,e,a,l=i.length;this.plugin?this.plugin.fillErrors(i,this.pluginNode):i.push("core_availability:item_unknowntype"),t=this.node.one("> .badge-warning"),i.length===l||t.get("firstChild")?i.length===l&&t.get("firstChild")&&t.get("firstChild").remove():(l="",e=(i=i[i.length-1].split(":"))[0],a="[["+(i=i[1])+","+e+"]]",(l=M.util.get_string(i,e))===a&&(l=M.util.get_string("invalid","availability")),t.appendChild(document.createTextNode(l)))},M.core_availability.Item.prototype.renumber=function(i){var t={number:i};this.plugin?t.type=M.util.get_string("title","availability_"+this.pluginType):t.type="["+this.pluginType+"]",t.number=i+":",t=M.util.get_string("itemheading","availability",t),this.node.one("> h3").set("innerHTML",t),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root"},M.core_availability.Item.prototype.focusAfterAdd=function(){this.plugin.focusAfterAdd(this.pluginNode)},M.core_availability.Item.prototype.pluginType=null,M.core_availability.Item.prototype.plugin=null,M.core_availability.Item.prototype.eyeIcon=null,M.core_availability.Item.prototype.node=null,M.core_availability.Item.prototype.pluginNode=null,M.core_availability.EyeIcon=function(i,t){var e,a,l,n;this.individual=i,this.span=d.Node.create('
      '),e=d.Node.create(""),this.span.appendChild(e),a=i?"_individual":"_all",l=function(){var i=M.util.get_string("hidden"+a,"availability");e.set("src",M.util.image_url("i/show","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("show_verb","availability"))},n=function(){var i=M.util.get_string("shown"+a,"availability");e.set("src",M.util.image_url("i/hide","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("hide_verb","availability"))},(t?n:l).call(this),this.span.on("click",i=function(i){i.preventDefault(),(this.isHidden()?n:l).call(this),M.core_availability.form.update()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.EyeIcon.prototype.individual=!1,M.core_availability.EyeIcon.prototype.span=null,M.core_availability.EyeIcon.prototype.isHidden=function(){var i=this.individual?"_individual":"_all",i=M.util.get_string("hidden"+i,"availability");return this.span.one("img").get("alt")===i},M.core_availability.DeleteIcon=function(t){var i;this.span=d.Node.create(''),i=d.Node.create(''+M.util.get_string('),this.span.appendChild(i),this.span.on("click",i=function(i){i.preventDefault(),M.core_availability.form.rootList.deleteDescendant(t),M.core_availability.form.rootList.renumber()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.DeleteIcon.prototype.span=null},"@VERSION@",{requires:["base","node","event","event-delegate","panel","moodle-core-notification-dialogue","json"]}); \ No newline at end of file diff --git a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js index 34c0df0af14a8..8477127903fd5 100644 --- a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js +++ b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js @@ -560,7 +560,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) { } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); - + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; @@ -1008,6 +1008,7 @@ M.core_availability.Item.prototype.renumber = function(number) { headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** diff --git a/availability/yui/src/form/js/form.js b/availability/yui/src/form/js/form.js index 5e5e739959854..7e80eee41b6e4 100644 --- a/availability/yui/src/form/js/form.js +++ b/availability/yui/src/form/js/form.js @@ -558,7 +558,7 @@ M.core_availability.List.prototype.renumber = function(parentNumber) { } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); - + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; @@ -1006,6 +1006,7 @@ M.core_availability.Item.prototype.renumber = function(number) { headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); + this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** diff --git a/blog/edit.php b/blog/edit.php index 435445d748764..2c77cbb998d41 100644 --- a/blog/edit.php +++ b/blog/edit.php @@ -143,7 +143,7 @@ 'sesskey' => sesskey(), 'courseid' => $courseid); $optionsno = array('userid' => $entry->userid, 'courseid' => $courseid); - $PAGE->set_title("$SITE->shortname: $strblogs"); + $PAGE->set_title($strblogs); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); @@ -164,11 +164,11 @@ } } else if ($action == 'add') { $editmodetitle = $strblogs . ': ' . get_string('addnewentry', 'blog'); - $PAGE->set_title("$SITE->shortname: $editmodetitle"); + $PAGE->set_title($editmodetitle); $PAGE->set_heading(fullname($USER)); } else if ($action == 'edit') { $editmodetitle = $strblogs . ': ' . get_string('editentry', 'blog'); - $PAGE->set_title("$SITE->shortname: $editmodetitle"); + $PAGE->set_title($editmodetitle); $PAGE->set_heading(fullname($USER)); } diff --git a/blog/external_blog_edit.php b/blog/external_blog_edit.php index 37fb2a5ce5214..b8cc1ee0f82b9 100644 --- a/blog/external_blog_edit.php +++ b/blog/external_blog_edit.php @@ -138,7 +138,7 @@ $PAGE->navbar->add(get_string('addnewexternalblog', 'blog')); $PAGE->set_heading(fullname($USER)); -$PAGE->set_title("$SITE->shortname: $strblogs: $strexternalblogs"); +$PAGE->set_title("$strblogs: $strexternalblogs"); echo $OUTPUT->header(); echo $OUTPUT->heading($strformheading, 2); diff --git a/blog/external_blogs.php b/blog/external_blogs.php index 996ee7a342206..667f4b70c1a5a 100644 --- a/blog/external_blogs.php +++ b/blog/external_blogs.php @@ -67,7 +67,7 @@ $blogs = $DB->get_records('blog_external', array('userid' => $USER->id)); $PAGE->set_heading(fullname($USER)); -$PAGE->set_title("$SITE->shortname: $strblogs: $strexternalblogs"); +$PAGE->set_title("$strblogs: $strexternalblogs"); $PAGE->set_pagelayout('standard'); echo $OUTPUT->header(); diff --git a/blog/lib.php b/blog/lib.php index 174b610d0b4e0..1cd66666f5531 100644 --- a/blog/lib.php +++ b/blog/lib.php @@ -716,9 +716,10 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu // Note: if action is set to 'add' or 'edit', we do this at the end. if (empty($entryid) && empty($modid) && empty($courseid) && empty($userid) && !in_array($action, array('edit', 'add'))) { $PAGE->navbar->add($strblogentries, $blogurl); - $PAGE->set_title($site->fullname); + $strsiteblog = get_string('siteblogheading', 'blog'); + $PAGE->set_title($strsiteblog); $PAGE->set_heading($site->fullname); - $headers['heading'] = get_string('siteblogheading', 'blog'); + $headers['heading'] = $strsiteblog; } // Case 2: only entryid is requested, ignore all other filters. courseid is used to give more contextual information. @@ -742,9 +743,10 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu $blogurl->remove_params('userid'); $PAGE->navbar->add($entry->subject, $blogurl); - $PAGE->set_title("$shortname: " . fullname($user) . ": $entry->subject"); + $blogentryby = get_string('blogentrybyuser', 'blog', fullname($user)); + $PAGE->set_title($entry->subject . moodle_page::TITLE_SEPARATOR . $blogentryby); $PAGE->set_heading("$shortname: " . fullname($user) . ": $entry->subject"); - $headers['heading'] = get_string('blogentrybyuser', 'blog', fullname($user)); + $headers['heading'] = $blogentryby; // We ignore tag and search params. if (empty($action) || !$CFG->useblogassociations) { @@ -758,7 +760,7 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu $shortname = format_string($site->shortname, true, array('context' => context_course::instance(SITEID))); $blogurl->param('userid', $userid); - $PAGE->set_title("$shortname: " . fullname($user) . ": " . get_string('blog', 'blog')); + $PAGE->set_title(fullname($user) . ": " . get_string('blog', 'blog')); $PAGE->set_heading("$shortname: " . fullname($user) . ": " . get_string('blog', 'blog')); $headers['heading'] = get_string('userblog', 'blog', fullname($user)); $headers['strview'] = get_string('viewuserentries', 'blog', fullname($user)); @@ -766,9 +768,10 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu } else if (!$CFG->useblogassociations && empty($userid) && !in_array($action, array('edit', 'add'))) { // Case 4: No blog associations, no userid. - $PAGE->set_title($site->fullname); + $strsiteblog = get_string('siteblogheading', 'blog'); + $PAGE->set_title($strsiteblog); $PAGE->set_heading($site->fullname); - $headers['heading'] = get_string('siteblogheading', 'blog'); + $headers['heading'] = $strsiteblog; } else if (!empty($userid) && !empty($modid) && empty($entryid)) { // Case 5: Blog entries associated with an activity by a specific user (courseid ignored). @@ -781,7 +784,7 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu $PAGE->navbar->add(fullname($user), "$CFG->wwwroot/user/view.php?id=$user->id"); $PAGE->navbar->add($strblogentries, $blogurl); - $PAGE->set_title("$shortname: $cm->name: " . fullname($user) . ': ' . get_string('blogentries', 'blog')); + $PAGE->set_title(fullname($user) . ': ' . get_string('blogentries', 'blog') . moodle_page::TITLE_SEPARATOR . $cm->name); $PAGE->set_heading("$shortname: $cm->name: " . fullname($user) . ': ' . get_string('blogentries', 'blog')); $a = new stdClass(); diff --git a/blog/preferences.php b/blog/preferences.php index c950f3792552a..4c16a56883e07 100644 --- a/blog/preferences.php +++ b/blog/preferences.php @@ -95,7 +95,7 @@ $strpreferences = get_string('preferences'); $strblogs = get_string('blogs', 'blog'); -$title = "$site->shortname: $strblogs : $strpreferences"; +$title = "$strblogs : $strpreferences"; $PAGE->set_title($title); $PAGE->set_heading(fullname($USER)); diff --git a/cache/classes/loaders.php b/cache/classes/loaders.php index 888f29c315238..5651aca947844 100644 --- a/cache/classes/loaders.php +++ b/cache/classes/loaders.php @@ -608,7 +608,8 @@ protected function get_implementation($key, int $requiredversion, int $strictnes // store; parent method will have set it to all stores if needed. if ($setaftervalidation) { $lock = null; - if (!empty($this->requirelockingbeforewrite)) { + // Only try to acquire a lock for this cache if we do not already have one. + if (!empty($this->requirelockingbeforewrite) && !$this->check_lock_state($key)) { $lock = $this->acquire_lock($key); } if ($requiredversion === self::VERSION_NONE) { @@ -1675,24 +1676,34 @@ public function __clone() { * @return bool Returns true if the lock could be acquired, false otherwise. */ public function acquire_lock($key) { - global $CFG; + $releaseparent = false; if ($this->get_loader() !== false) { - $this->get_loader()->acquire_lock($key); + if (!$this->get_loader()->acquire_lock($key)) { + return false; + } + // We need to release this lock later if the lock is not successful. + $releaseparent = true; } - $key = cache_helper::hash_key($key, $this->get_definition()); + $hashedkey = cache_helper::hash_key($key, $this->get_definition()); $before = microtime(true); if ($this->nativelocking) { - $lock = $this->get_store()->acquire_lock($key, $this->get_identifier()); + $lock = $this->get_store()->acquire_lock($hashedkey, $this->get_identifier()); } else { $this->ensure_cachelock_available(); - $lock = $this->cachelockinstance->lock($key, $this->get_identifier()); + $lock = $this->cachelockinstance->lock($hashedkey, $this->get_identifier()); } $after = microtime(true); if ($lock) { - $this->locks[$key] = $lock; + $this->locks[$hashedkey] = $lock; if ((defined('MDL_PERF') && MDL_PERF) || $this->perfdebug) { \core\lock\timing_wrapper_lock_factory::record_lock_data($after, $before, - $this->get_definition()->get_id(), $key, $lock, $this->get_identifier() . $key); + $this->get_definition()->get_id(), $hashedkey, $lock, $this->get_identifier() . $hashedkey); + } + } else { + // If we successfully got the parent lock, but are now failing to get this lock, then we should release + // the parent one. + if ($releaseparent) { + $this->get_loader()->release_lock($key); } } return $lock; diff --git a/cache/tests/cache_test.php b/cache/tests/cache_test.php index 0c6cff0412cdb..bcd99cc191637 100644 --- a/cache/tests/cache_test.php +++ b/cache/tests/cache_test.php @@ -2274,6 +2274,93 @@ public function test_application_locking_multiple_layers() { $this->assertEquals(['x' => 'X', 'y' => 'Y', 'z' => 'Z'], $cache->get_many(['x', 'y', 'z'])); } + /** + * Tests that locking fails correctly when either layer of a 2-layer cache has a lock already. + * + * @covers \cache_loader + */ + public function test_application_locking_multiple_layers_failures(): void { + + $instance = cache_config_testing::instance(true); + $instance->phpunit_add_definition('phpunit/test_application_locking', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'test_application_locking', + 'staticacceleration' => true, + 'staticaccelerationsize' => 1, + 'requirelockingbeforewrite' => true + ), false); + $instance->phpunit_add_file_store('phpunittest1'); + $instance->phpunit_add_file_store('phpunittest2'); + $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest1', 1); + $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest2', 2); + + $cache = cache::make('phpunit', 'test_application_locking'); + + // We need to get the individual stores so as to set up the right behaviour here. + $ref = new \ReflectionClass('\cache'); + $definitionprop = $ref->getProperty('definition'); + $definitionprop->setAccessible(true); + $storeprop = $ref->getProperty('store'); + $storeprop->setAccessible(true); + $loaderprop = $ref->getProperty('loader'); + $loaderprop->setAccessible(true); + + $definition = $definitionprop->getValue($cache); + $localstore = $storeprop->getValue($cache); + $sharedcache = $loaderprop->getValue($cache); + $sharedstore = $storeprop->getValue($sharedcache); + + // Set the lock waiting time to 1 second so it doesn't take forever to run the test. + $ref = new \ReflectionClass('\cachestore_file'); + $lockwaitprop = $ref->getProperty('lockwait'); + $lockwaitprop->setAccessible(true); + + $lockwaitprop->setValue($localstore, 1); + $lockwaitprop->setValue($sharedstore, 1); + + // Get key details and the cache identifier. + $hashedkey = cache_helper::hash_key('apple', $definition); + $localidentifier = $cache->get_identifier(); + $sharedidentifier = $sharedcache->get_identifier(); + + // 1. Local cache is not locked but parent cache is locked. + $sharedstore->acquire_lock($hashedkey, 'somebodyelse'); + try { + $this->assertFalse($cache->acquire_lock('apple')); + + // Neither store is locked by us, shared store still locked. + $this->assertFalse((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); + $this->assertFalse((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); + $this->assertTrue((bool)$sharedstore->check_lock_state($hashedkey, 'somebodyelse')); + + } finally { + $sharedstore->release_lock($hashedkey, 'somebodyelse'); + } + + // 2. Local cache is locked, parent cache is not locked. + $localstore->acquire_lock($hashedkey, 'somebodyelse'); + try { + $this->assertFalse($cache->acquire_lock('apple')); + + // Neither store is locked by us, local store still locked. + $this->assertFalse((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); + $this->assertFalse((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); + $this->assertTrue((bool)$localstore->check_lock_state($hashedkey, 'somebodyelse')); + } finally { + $localstore->release_lock($hashedkey, 'somebodyelse'); + } + + // 3. Just for completion, test what happens if we do lock it. + $this->assertTrue($cache->acquire_lock('apple')); + try { + $this->assertTrue((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); + $this->assertTrue((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); + } finally { + $cache->release_lock('apple'); + } + } + /** * Test the static cache_helper method purge_stores_used_by_definition. */ diff --git a/calendar/classes/type_base.php b/calendar/classes/type_base.php index 75e4bad89c280..223c59f068e88 100644 --- a/calendar/classes/type_base.php +++ b/calendar/classes/type_base.php @@ -165,7 +165,7 @@ public abstract function get_next_month($year, $month); * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use - * {@link http://docs.moodle.org/dev/Time_API#Timezone} + * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, @@ -180,7 +180,7 @@ public abstract function timestamp_to_date_string($time, $format, $timezone, $fi * * @param int $time timestamp in GMT * @param float|int|string $timezone the timezone to use to calculate the time - * {@link http://docs.moodle.org/dev/Time_API#Timezone} + * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ public abstract function timestamp_to_date_array($time, $timezone = 99); diff --git a/calendar/delete.php b/calendar/delete.php index 28af3809c997a..b4536e3a542a0 100644 --- a/calendar/delete.php +++ b/calendar/delete.php @@ -92,7 +92,7 @@ $PAGE->navbar->add($strcalendar, $viewcalendarurl); $PAGE->navbar->add($title); -$PAGE->set_title($site->shortname.': '.$strcalendar.': '.$title); +$PAGE->set_title($strcalendar.': '.$title); $PAGE->set_heading($COURSE->fullname); if ($course) { $PAGE->set_secondary_navigation(false); diff --git a/calendar/templates/event_icon.mustache b/calendar/templates/event_icon.mustache index ab58936a82e3e..133d071f502a3 100644 --- a/calendar/templates/event_icon.mustache +++ b/calendar/templates/event_icon.mustache @@ -34,7 +34,17 @@ } }} {{#modulename}} - {{#pix}} monologo, {{modulename}} {{/pix}} + {{#icon}} + {{#iconurl}} + {{alttext}} + {{/iconurl}} + {{^iconurl}} + {{#pix}} monologo, {{modulename}} {{/pix}} + {{/iconurl}} + {{/icon}} + {{^icon}} + {{#pix}} monologo, {{modulename}} {{/pix}} + {{/icon}} {{/modulename}} {{^modulename}} {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} diff --git a/calendar/templates/event_item.mustache b/calendar/templates/event_item.mustache index 2ac7c811f4e0f..abb2054e36c5d 100644 --- a/calendar/templates/event_item.mustache +++ b/calendar/templates/event_item.mustache @@ -75,7 +75,16 @@ {{/isactionevent}} {{/canedit}} - {{#icon}}
      {{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}
      {{/icon}} + {{#icon}} +
      + {{#iconurl}} + {{alttext}} + {{/iconurl}} + {{^iconurl}} + {{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}} + {{/iconurl}} +
      + {{/icon}}

      {{{name}}}

      diff --git a/calendar/tests/calendartype_test_example.php b/calendar/tests/calendartype_test_example.php index ab725d1d03576..21e9149ddb2ea 100644 --- a/calendar/tests/calendartype_test_example.php +++ b/calendar/tests/calendartype_test_example.php @@ -222,7 +222,7 @@ public function get_next_month($year, $month) { * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use - * {@link http://docs.moodle.org/dev/Time_API#Timezone} + * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, @@ -239,7 +239,7 @@ public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fi * * @param int $time timestamp in GMT * @param float|int|string $timezone the timezone to use to calculate the time - * {@link http://docs.moodle.org/dev/Time_API#Timezone} + * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ public function timestamp_to_date_array($time, $timezone = 99) { diff --git a/calendar/type/gregorian/classes/structure.php b/calendar/type/gregorian/classes/structure.php index bb0012666b225..a01e84b1c1dbb 100644 --- a/calendar/type/gregorian/classes/structure.php +++ b/calendar/type/gregorian/classes/structure.php @@ -289,7 +289,7 @@ public function get_next_month($year, $month) { * @param int $time the timestamp in UTC, as obtained from the database * @param string $format strftime format * @param int|float|string $timezone the timezone to use - * {@link http://docs.moodle.org/dev/Time_API#Timezone} + * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @param bool $fixday if true then the leading zero from %d is removed, * if false then the leading zero is maintained * @param bool $fixhour if true then the leading zero from %I is removed, @@ -348,7 +348,7 @@ public function timestamp_to_date_string($time, $format, $timezone, $fixday, $fi * * @param int $time Timestamp in GMT * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no - * dst offset is applied {@link http://docs.moodle.org/dev/Time_API#Timezone} + * dst offset is applied {@link https://moodledev.io/docs/apis/subsystems/time#timezone} * @return array an array that represents the date in user time */ public function timestamp_to_date_array($time, $timezone = 99) { diff --git a/calendar/upgrade.txt b/calendar/upgrade.txt index 79c554796e86d..376d0e67c5861 100644 --- a/calendar/upgrade.txt +++ b/calendar/upgrade.txt @@ -30,7 +30,7 @@ information provided here is intended especially for developers. === 3.9 === * Plugins can now create their own calendar events, both standard and action ones. To do it they need to specify $event->component when creating an event. Component events can not be edited or deleted manually. - See https://docs.moodle.org/dev/Calendar_API#Component_events + See https://moodledev.io/docs/apis/core/calendar#component-events * The following functions have been deprecated because they were no longer used: - calendar_add_event_metadata() - core_calendar_renderer::event() diff --git a/calendar/view.php b/calendar/view.php index 057f2254f2aba..59a4cbda0d6ab 100644 --- a/calendar/view.php +++ b/calendar/view.php @@ -114,7 +114,9 @@ $PAGE->set_context(context_system::instance()); } -require_login($course, false); +// Auto log in guests on frontpage. +$autologinguest = !$iscoursecalendar; +require_login($course, $autologinguest); $calendar = calendar_information::create($time, $courseid, $categoryid); diff --git a/contentbank/tests/behat/delete_content.feature b/contentbank/tests/behat/delete_content.feature index 2e639898f4304..5a2a801c697ca 100644 --- a/contentbank/tests/behat/delete_content.feature +++ b/contentbank/tests/behat/delete_content.feature @@ -43,7 +43,7 @@ Feature: Delete H5P file from the content bank And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element" And I click on "Delete" "button" in the "Delete content" "dialogue" And I wait until the page is ready - And I should see "The content has been deleted." + And I should see "Content deleted." And I should not see "content2delete.h5p" Scenario: Users without the required capability can only delete their own content @@ -94,5 +94,5 @@ Feature: Delete H5P file from the content bank Then I should see "Are you sure you want to delete the content 'content2delete.h5p'" And I should see "The content will only be deleted from the content bank" And I click on "Delete" "button" in the "Delete content" "dialogue" - And I should see "The content has been deleted." + And I should see "Content deleted." And I should not see "content2delete.h5p" diff --git a/course/delete.php b/course/delete.php index c4a942ffe9c5d..c6fa725368d1f 100644 --- a/course/delete.php +++ b/course/delete.php @@ -57,7 +57,7 @@ $strdeletingcourse = get_string("deletingcourse", "", $courseshortname); $PAGE->navbar->add($strdeletingcourse); - $PAGE->set_title("$SITE->shortname: $strdeletingcourse"); + $PAGE->set_title($strdeletingcourse); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); @@ -77,7 +77,7 @@ $strdeletecheck = get_string("deletecheck", "", $courseshortname); $PAGE->navbar->add($strdeletecheck); -$PAGE->set_title("$SITE->shortname: $strdeletecheck"); +$PAGE->set_title($strdeletecheck); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); diff --git a/course/edit.php b/course/edit.php index 93acabafc58d0..2d357bd1218e5 100644 --- a/course/edit.php +++ b/course/edit.php @@ -235,7 +235,7 @@ $PAGE->navbar->add(get_string('coursemgmt', 'admin'), $managementurl); $pagedesc = $straddnewcourse; - $title = "$site->shortname: $straddnewcourse"; + $title = $straddnewcourse; $fullname = format_string($category->name); $PAGE->navbar->add($pagedesc); } diff --git a/course/editcategory.php b/course/editcategory.php index af6d13c8f0fbf..c50adb2339ede 100644 --- a/course/editcategory.php +++ b/course/editcategory.php @@ -72,7 +72,7 @@ } else { $context = context_system::instance(); $fullname = $SITE->fullname; - $title = "$SITE->shortname: $strtitle"; + $title = $strtitle; $PAGE->set_secondary_active_tab('courses'); } diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 30e7f30085838..291072cfcabe2 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -371,7 +371,7 @@ public function get_modinfo(): course_modinfo { * This method ensures that 3rd party course format plugins that still use 'numsections' continue to * work but at the same time we no longer expect formats to have 'numsections' property. * - * @return int + * @return int The last section number, or -1 if sections are entirely missing */ public function get_last_section_number() { $course = $this->get_course(); @@ -380,6 +380,12 @@ public function get_last_section_number() { } $modinfo = get_fast_modinfo($course); $sections = $modinfo->get_section_info_all(); + + // Sections seem to be missing entirely. Avoid subsequent errors and return early. + if (count($sections) === 0) { + return -1; + } + return (int)max(array_keys($sections)); } diff --git a/course/format/tests/base_test.php b/course/format/tests/base_test.php index 55ca4d7c6022c..c6a1f6d1ece63 100644 --- a/course/format/tests/base_test.php +++ b/course/format/tests/base_test.php @@ -14,14 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -defined('MOODLE_INTERNAL') || die(); - /** * Course related unit tests * * @package core_course * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_courseformat\base */ class base_test extends advanced_testcase { @@ -348,6 +347,32 @@ public function test_set_sections_preference() { ); } + /** + * Test that retrieving last section number for a course + * + * @covers ::get_last_section_number + */ + public function test_get_last_section_number(): void { + global $DB; + + $this->resetAfterTest(); + + // Course with two additional sections. + $courseone = $this->getDataGenerator()->create_course(['numsections' => 2]); + $this->assertEquals(2, course_get_format($courseone)->get_last_section_number()); + + // Course without additional sections, section zero is the "default" section that always exists. + $coursetwo = $this->getDataGenerator()->create_course(['numsections' => 0]); + $this->assertEquals(0, course_get_format($coursetwo)->get_last_section_number()); + + // Course without additional sections, manually remove section zero, as "course_delete_section" prevents that. This + // simulates course data integrity issues that previously triggered errors. + $coursethree = $this->getDataGenerator()->create_course(['numsections' => 0]); + $DB->delete_records('course_sections', ['course' => $coursethree->id, 'section' => 0]); + + $this->assertEquals(-1, course_get_format($coursethree)->get_last_section_number()); + } + /** * Test for the default delete format data behaviour. * diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 38220c87823bd..68aed30b32169 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -1,6 +1,6 @@ This files describes API changes for course formats -Overview of this plugin type at http://docs.moodle.org/dev/Course_formats +Overview of this plugin type at https://moodledev.io/docs/apis/plugintypes/format === 4.1 === * New \core_courseformat\stateupdates methods add_section_remove() and add_cm_remove() have been added to replace diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index 3bd726b4fc35a..79b0b119fb41b 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -406,14 +406,25 @@ public function get_section_dates($section, $startdate = false) { } else { $sectionnum = $section; } - $oneweekseconds = 604800; - // Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight - // savings and the date changes. - $startdate = $startdate + 7200; + + // Create a DateTime object for the start date. + $startdateobj = new DateTime("@$startdate"); + + // Calculate the interval for one week. + $oneweekinterval = new DateInterval('P7D'); + + // Calculate the interval for the specified number of sections. + for ($i = 1; $i < $sectionnum; $i++) { + $startdateobj->add($oneweekinterval); + } + + // Calculate the end date. + $enddateobj = clone $startdateobj; + $enddateobj->add($oneweekinterval); $dates = new stdClass(); - $dates->start = $startdate + ($oneweekseconds * ($sectionnum - 1)); - $dates->end = $dates->start + $oneweekseconds; + $dates->start = $startdateobj->getTimestamp(); + $dates->end = $enddateobj->getTimestamp(); return $dates; } diff --git a/course/format/weeks/tests/format_weeks_test.php b/course/format/weeks/tests/format_weeks_test.php index dd5eb990a366c..8d6414a2a0c53 100644 --- a/course/format/weeks/tests/format_weeks_test.php +++ b/course/format/weeks/tests/format_weeks_test.php @@ -219,8 +219,8 @@ public function test_default_course_enddate() { $courseform = new \testable_course_edit_form(null, $args); $courseform->definition_after_data(); - // format_weeks::get_section_dates is adding 2h to avoid DST problems, we need to replicate it here. - $enddate = $params['startdate'] + (WEEKSECS * $params['numsections']) + 7200; + // Calculate the expected end date. + $enddate = $params['startdate'] + (WEEKSECS * $params['numsections']); $weeksformat = course_get_format($course->id); $this->assertEquals($enddate, $weeksformat->get_default_course_enddate($courseform->get_quick_form())); diff --git a/course/renderer.php b/course/renderer.php index 2db11d1d89ddc..ed3eefee1345d 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -97,8 +97,6 @@ public function course_info_box(stdClass $course) { * * @deprecated since 2.5 * - * Please see http://docs.moodle.org/dev/Courses_lists_upgrade_to_2.5 - * * @param array $ignored argument ignored * @return string */ @@ -112,8 +110,6 @@ public final function course_category_tree(array $ignored) { * * @deprecated since 2.5 * - * Please see http://docs.moodle.org/dev/Courses_lists_upgrade_to_2.5 - * * @param array $category * @param int $depth * @return string @@ -1662,13 +1658,13 @@ public function course_category($category) { if (core_course_category::is_simple_site()) { // There is only one category in the system, do not display link to it. $strfulllistofcourses = get_string('fulllistofcourses'); - $this->page->set_title("$site->shortname: $strfulllistofcourses"); + $this->page->set_title($strfulllistofcourses); } else if (!$coursecat->id || !$coursecat->is_uservisible()) { $strcategories = get_string('categories'); - $this->page->set_title("$site->shortname: $strcategories"); + $this->page->set_title($strcategories); } else { $strfulllistofcourses = get_string('fulllistofcourses'); - $this->page->set_title("$site->shortname: $strfulllistofcourses"); + $this->page->set_title($strfulllistofcourses); } // Print current category description diff --git a/course/search.php b/course/search.php index ccaf4b302a994..f0d9d62561403 100644 --- a/course/search.php +++ b/course/search.php @@ -86,10 +86,10 @@ if (empty($searchcriteria)) { // no search criteria specified, print page with just search form - $PAGE->set_title("$site->fullname : $strsearch"); + $PAGE->set_title($strsearch); } else { // this is search results page - $PAGE->set_title("$site->fullname : $strsearchresults"); + $PAGE->set_title($strsearchresults); // Link to manage search results should be visible if user have system or category level capability if ((can_edit_in_category() || !empty($usercatlist))) { $aurl = new moodle_url('/course/management.php', $searchcriteria); diff --git a/course/tests/behat/activity_resource_description_display.feature b/course/tests/behat/activity_resource_description_display.feature new file mode 100644 index 0000000000000..8a1ac00e34baf --- /dev/null +++ b/course/tests/behat/activity_resource_description_display.feature @@ -0,0 +1,71 @@ +@core @core_course +Feature: Display activity and resource description + In order to display activity and resource description + As teacher + I should be able to enable "Display description on course page" + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario Outline: Display activity and resource descriptions + # Generate activity/resource with description + Given the following "activities" exist: + | activity | course | name | intro | showdescription | + | | C1 | | intro | 1 | + When I am on the "Course 1" course page logged in as teacher1 + # Confirm that activity name and description are displayed + Then I should see "" in the "region-main" "region" + And I should see " intro" in the "region-main" "region" + + Examples: + | acttype | actname | + | assign | Assign 1 | + | book | Book 1 | + | chat | Chat 1 | + | data | Database 1 | + | feedback | Feedback 1 | + | forum | Forum 1 | + | label | Label 1 | + | lti | LTI 1 | + | page | Page 1 | + | quiz | Quiz 1 | + | resource | Resource 1 | + | imscp | IMSCP 1 | + | folder | Folder 1 | + | glossary | Glossary 1 | + | scorm | Scorm 1 | + | lesson | Lesson 1 | + | survey | Survey 1 | + | url | URL 1 | + | wiki | Wiki 1 | + | workshop | Workshop 1 | + + Scenario: Display url activity description with pop-up display + # Generate url activity with description and popup appearance + Given the following "activities" exist: + | activity | course | name | intro | showdescription | display | popupwidth | popupheight | + | url | C1 | URL 1 | URL 1 intro | 1 | 6 | 620 | 450 | + When I am on the "Course 1" course page logged in as teacher1 + # Confirm that activity name and description are displayed + Then I should see "URL 1" in the "region-main" "region" + And I should see "URL 1 intro" in the "region-main" "region" + + Scenario: Display activity with image description + # Generate page activity with image embedded in description + Given the following "activities" exist: + | activity | course | name | intro | showdescription | + | page | C1 | Page 1 | Page 1 intro with image: | 1 | + When I am on the "Course 1" course page logged in as teacher1 + # Confirm that activity name and description are displayed + Then I should see "Page 1" in the "region-main" "region" + And I should see "Page 1 intro with image:" in the "region-main" "region" + # Confirm that image element exists + And "//img[contains(@src, 'http://download.moodle.org/unittest/test.jpg')]" "xpath_element" should exist in the "region-main" "region" diff --git a/course/view.php b/course/view.php index 659834c565ad4..4ecf21dd46c49 100644 --- a/course/view.php +++ b/course/view.php @@ -227,13 +227,21 @@ $PAGE->set_button($buttons); } + $editingtitle = ''; + if ($PAGE->user_is_editing()) { + // Append this to the page title's lang string to get its equivalent when editing mode is turned on. + $editingtitle = 'editing'; + } + // If viewing a section, make the title more specific if ($section and $section > 0 and course_format_uses_sections($course->format)) { $sectionname = get_string('sectionname', "format_$course->format"); $sectiontitle = get_section_name($course, $section); - $PAGE->set_title(get_string('coursesectiontitle', 'moodle', array('course' => $course->fullname, 'sectiontitle' => $sectiontitle, 'sectionname' => $sectionname))); + $PAGE->set_title(get_string('coursesectiontitle' . $editingtitle, 'moodle', array( + 'course' => $course->fullname, 'sectiontitle' => $sectiontitle, 'sectionname' => $sectionname) + )); } else { - $PAGE->set_title(get_string('coursetitle', 'moodle', array('course' => $course->fullname))); + $PAGE->set_title(get_string('coursetitle' . $editingtitle, 'moodle', array('course' => $course->fullname))); } $PAGE->set_heading($course->fullname); diff --git a/customfield/field/textarea/lib.php b/customfield/field/textarea/lib.php index 83389f37dad36..92fb5523b7587 100644 --- a/customfield/field/textarea/lib.php +++ b/customfield/field/textarea/lib.php @@ -72,5 +72,5 @@ function customfield_textarea_pluginfile($course, $cm, $context, $filearea, $arg } // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering. - send_file($file, 86400, 0, $forcedownload, $options); + send_stored_file($file, DAYSECS, 0, $forcedownload, $options); } diff --git a/customfield/lib.php b/customfield/lib.php index 2f9b97eeb1599..c91a8892898e2 100644 --- a/customfield/lib.php +++ b/customfield/lib.php @@ -80,6 +80,5 @@ function core_customfield_pluginfile($course, $cm, $context, $filearea, $args, $ } // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering. - // From Moodle 2.3, use send_stored_file instead. - send_file($file, 86400, 0, $forcedownload, $options); + send_stored_file($file, DAYSECS, 0, $forcedownload, $options); } diff --git a/enrol/self/lang/en/enrol_self.php b/enrol/self/lang/en/enrol_self.php index 4479ae1c3c111..45e09ccec1c7c 100644 --- a/enrol/self/lang/en/enrol_self.php +++ b/enrol/self/lang/en/enrol_self.php @@ -79,7 +79,7 @@ $string['maxenrolled_help'] = 'Specifies the maximum number of users that can self enrol. 0 means no limit.'; $string['maxenrolledreached'] = 'Maximum number of users allowed to self-enrol was already reached.'; $string['messageprovider:expiry_notification'] = 'Self enrolment expiry notifications'; -$string['newenrols'] = 'Allow new enrolments'; +$string['newenrols'] = 'Allow new self enrolments'; $string['newenrols_desc'] = 'Allow users to self enrol into new courses by default.'; $string['newenrols_help'] = 'This setting determines whether a user can enrol into this course.'; $string['nopassword'] = 'No enrolment key required.'; @@ -92,6 +92,7 @@ $string['passwordinvalid'] = 'Incorrect enrolment key, please try again'; $string['passwordinvalidhint'] = 'That enrolment key was incorrect, please try again
      (Here\'s a hint - it starts with \'{$a}\')'; +$string['passwordmatchesgroupkey'] = 'Enrolment key matches an existing group enrolment key'; $string['pluginname'] = 'Self enrolment'; $string['pluginname_desc'] = 'The self enrolment plugin allows users to choose which courses they want to participate in. The courses may be protected by an enrolment key. Internally the enrolment is done via the manual enrolment plugin which has to be enabled in the same course.'; $string['requirepassword'] = 'Require enrolment key'; @@ -108,9 +109,9 @@ $string['sendexpirynotificationstask'] = "Self enrolment send expiry notifications task"; $string['showhint'] = 'Show hint'; $string['showhint_desc'] = 'Show first letter of the guest access key.'; -$string['status'] = 'Allow existing enrolments'; +$string['status'] = 'Keep current self enrolments active'; $string['status_desc'] = 'Enable self enrolment method in new courses.'; -$string['status_help'] = 'If enabled together with \'Allow new enrolments\' disabled, only users who self enrolled previously can access the course. If disabled, this self enrolment method is effectively disabled, since all existing self enrolments are suspended and new users cannot self enrol.'; +$string['status_help'] = 'If set to No, any existing participants who enrolled themselves into the course will no longer have access.'; $string['syncenrolmentstask'] = 'Synchronise self enrolments task'; $string['unenrol'] = 'Unenrol user'; $string['unenrolselfconfirm'] = 'Do you really want to unenrol yourself from course "{$a}"?'; diff --git a/enrol/self/lib.php b/enrol/self/lib.php index 1c931762e7f09..8bf5182502b15 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -159,8 +159,8 @@ public function enrol_self(stdClass $instance, $data = null) { \core\notification::success(get_string('youenrolledincourse', 'enrol')); - if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) { - // It must be a group enrolment, let's assign group too. + // Test whether the password is also used as a group key. + if ($instance->password && $instance->customint1) { $groups = $DB->get_records('groups', array('courseid'=>$instance->courseid), 'id', 'id, enrolmentkey'); foreach ($groups as $group) { if (empty($group->enrolmentkey)) { @@ -876,6 +876,9 @@ public function use_standard_editing_ui() { * @return void */ public function edit_instance_validation($data, $files, $instance, $context) { + global $CFG; + require_once("{$CFG->dirroot}/enrol/self/locallib.php"); + $errors = array(); $checkpassword = false; @@ -890,6 +893,11 @@ public function edit_instance_validation($data, $files, $instance, $context) { if (($data['status'] == ENROL_INSTANCE_ENABLED) && ($instance->password !== $data['password'])) { $checkpassword = true; } + + // Check the password if we are enabling group enrolment keys. + if (!$instance->customint1 && $data['customint1']) { + $checkpassword = true; + } } else { $checkpassword = true; } @@ -904,6 +912,10 @@ public function edit_instance_validation($data, $files, $instance, $context) { if (!check_password_policy($data['password'], $errmsg)) { $errors['password'] = $errmsg; } + } else if (!empty($data['password']) && $data['customint1'] && + enrol_self_check_group_enrolment_key($data['courseid'], $data['password'])) { + + $errors['password'] = get_string('passwordmatchesgroupkey', 'enrol_self'); } } diff --git a/enrol/self/tests/self_test.php b/enrol/self/tests/self_test.php index 600c1ba10a3d5..351fdfeec348c 100644 --- a/enrol/self/tests/self_test.php +++ b/enrol/self/tests/self_test.php @@ -16,6 +16,9 @@ namespace enrol_self; +use context_course; +use enrol_self_plugin; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -29,6 +32,7 @@ * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_self_plugin */ class self_test extends \advanced_testcase { @@ -761,6 +765,44 @@ public function test_is_self_enrol_available() { $this->assertEquals($canntenrolerror, $selfplugin->is_self_enrol_available($instance)); } + /** + * Test custom validation of instance data for group enrolment key + * + * @covers ::edit_instance_validation + */ + public function test_edit_instance_validation_group_enrolment_key(): void { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $context = context_course::instance($course->id); + + /** @var enrol_self_plugin $plugin */ + $plugin = enrol_get_plugin('self'); + + $instance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => $plugin->get_name()], '*', MUST_EXIST); + + // Enable group enrolment keys. + $errors = $plugin->edit_instance_validation([ + 'customint1' => 1, + 'password' => 'cat', + ] + (array) $instance, [], $instance, $context); + + $this->assertEmpty($errors); + + // Now create a group with the same enrolment key we want to use. + $this->getDataGenerator()->create_group(['courseid' => $course->id, 'enrolmentkey' => 'cat']); + + $errors = $plugin->edit_instance_validation([ + 'customint1' => 1, + 'password' => 'cat', + ] + (array) $instance, [], $instance, $context); + + $this->assertArrayHasKey('password', $errors); + $this->assertEquals('Enrolment key matches an existing group enrolment key', $errors['password']); + } + /** * Test enrol_self_check_group_enrolment_key */ diff --git a/filter/urltolink/tests/filter_test.php b/filter/urltolink/tests/filter_test.php index 800453419014e..48e8766819225 100644 --- a/filter/urltolink/tests/filter_test.php +++ b/filter/urltolink/tests/filter_test.php @@ -176,7 +176,9 @@ function get_convert_urls_into_links_test_cases() { '

      text www.moodle.org</p> text' => '

      text www.moodle.org</p> text', // Some more urls. '' => '', - 'www.google.com' => 'www.google.com', + 'www.google.com' => + '' . + 'www.google.com', 'http://nolandforzombies.com Zombies FTW http://aliens.org' => 'http://nolandforzombies.com Zombies FTW http://aliens.org', // Test 'nolink' class. 'URL: http://moodle.org' => 'URL: http://moodle.org', diff --git a/filter/wiris/classes/pluginwrapper.php b/filter/wiris/classes/pluginwrapper.php index 3336a2fc3a62d..e99b89294f935 100644 --- a/filter/wiris/classes/pluginwrapper.php +++ b/filter/wiris/classes/pluginwrapper.php @@ -143,7 +143,10 @@ public static function get_wiris_plugin() { if (!in_array('atto', $editors)) { $editors[] = 'atto'; } - if ($CFG->branch < 402) { + // BEGIN LSU if branch doesn't exist then installation fails. + // if ($CFG->branch < 402) { + if (isset($CFG->branch) && $CFG->branch < 402) { + // LSU END if (!in_array('tinymce', $editors)) { $editors[] = 'tinymce'; } diff --git a/githash.php b/githash.php index 5c4ec58e8400e..9a7d865b6b593 100644 --- a/githash.php +++ b/githash.php @@ -23,4 +23,4 @@ * and administration at http://docs.moodle.org/en/Git . */ -$githash = '3fddb90'; +$githash = 'b3ea1a9'; diff --git a/grade/edit/tree/category_form.php b/grade/edit/tree/category_form.php index 3e1af00d58ed2..d6be809c56752 100644 --- a/grade/edit/tree/category_form.php +++ b/grade/edit/tree/category_form.php @@ -241,7 +241,7 @@ function definition() { /// parent category related settings $mform->addElement('header', 'headerparent', get_string('parentcategory', 'grades')); - $mform->addElement('checkbox', 'grade_item_weightoverride', get_string('adjustedweight', 'grades')); + $mform->addElement('advcheckbox', 'grade_item_weightoverride', get_string('adjustedweight', 'grades')); $mform->addHelpButton('grade_item_weightoverride', 'weightoverride', 'grades'); $mform->addElement('float', 'grade_item_aggregationcoef2', get_string('weight', 'grades')); diff --git a/grade/edit/tree/item_form.php b/grade/edit/tree/item_form.php index 0a344a555b187..7042e73b16030 100644 --- a/grade/edit/tree/item_form.php +++ b/grade/edit/tree/item_form.php @@ -197,14 +197,14 @@ function definition() { /// parent category related settings $mform->addElement('header', 'headerparent', get_string('parentcategory', 'grades')); - $mform->addElement('checkbox', 'weightoverride', get_string('adjustedweight', 'grades')); + $mform->addElement('advcheckbox', 'weightoverride', get_string('adjustedweight', 'grades')); $mform->addHelpButton('weightoverride', 'weightoverride', 'grades'); $mform->disabledIf('weightoverride', 'gradetype', 'eq', GRADE_TYPE_NONE); $mform->disabledIf('weightoverride', 'gradetype', 'eq', GRADE_TYPE_TEXT); $mform->addElement('float', 'aggregationcoef2', get_string('weight', 'grades')); $mform->addHelpButton('aggregationcoef2', 'weight', 'grades'); - $mform->disabledIf('aggregationcoef2', 'weightoverride', 'not_checked'); + $mform->disabledIf('aggregationcoef2', 'weightoverride'); $mform->disabledIf('aggregationcoef2', 'gradetype', 'eq', GRADE_TYPE_NONE); $mform->disabledIf('aggregationcoef2', 'gradetype', 'eq', GRADE_TYPE_TEXT); diff --git a/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php b/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php index 6f23158b75acc..09c76679dd1e5 100644 --- a/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php +++ b/grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php @@ -27,9 +27,9 @@ require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php'); -use Behat\Gherkin\Node\TableNode as TableNode, - Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, - Behat\Mink\Exception\ExpectationException as ExpectationException; +use Behat\Gherkin\Node\TableNode; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Exception\ExpectationException; /** * Steps definitions to help with rubrics. @@ -66,7 +66,6 @@ class behat_gradingform_rubric extends behat_base { * @param TableNode $rubric */ public function i_define_the_following_rubric(TableNode $rubric) { - // Being a smart method is nothing good when we talk about step definitions, in // this case we didn't have any other options as there are no labels no elements // id we can point to without having to "calculate" them. @@ -80,7 +79,6 @@ public function i_define_the_following_rubric(TableNode $rubric) { // Cleaning the current ones. $deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']"); if ($deletebuttons) { - // We should reverse the deletebuttons because otherwise once we delete // the first one the DOM will change and the [X] one will not exist anymore. $deletebuttons = array_reverse($deletebuttons, true); @@ -132,7 +130,10 @@ public function i_define_the_following_rubric(TableNode $rubric) { } // Add new criterion. - $addcriterionbutton->click(); + $this->execute('behat_general::i_click_on', [ + $addcriterionbutton, + 'NodeElement', + ]); $criterionroot = 'rubric[criteria][NEWID' . ($criterionit + 1) . ']'; @@ -158,12 +159,14 @@ public function i_define_the_following_rubric(TableNode $rubric) { if ($this->running_javascript()) { $deletelevel = $this->find_button($criterionroot . '[levels][NEWID' . $i . '][delete]'); $this->click_and_confirm($deletelevel); - } else { // Only if the level exists. $buttonname = $criterionroot . '[levels][NEWID' . $i . '][delete]'; if ($deletelevel = $this->getSession()->getPage()->findButton($buttonname)) { - $deletelevel->click(); + $this->execute('behat_general::i_click_on', [ + $deletelevel, + 'NodeElement', + ]); } } } @@ -171,7 +174,10 @@ public function i_define_the_following_rubric(TableNode $rubric) { // Adding levels if we don't have enough. $addlevel = $this->find_button($criterionroot . '[levels][addlevel]'); for ($i = ($defaultnumberoflevels + 1); $i <= $nlevels; $i++) { - $addlevel->click(); + $this->execute('behat_general::i_click_on', [ + $addlevel, + 'NodeElement', + ]); } } @@ -226,7 +232,6 @@ public function i_define_the_following_rubric(TableNode $rubric) { * @param string $criterionname */ public function i_replace_rubric_level_with($currentvalue, $value, $criterionname) { - $currentvalueliteral = behat_context_helper::escape($currentvalue); $criterionliteral = behat_context_helper::escape($criterionname); @@ -247,14 +252,15 @@ public function i_replace_rubric_level_with($currentvalue, $value, $criterionnam "/descendant::textarea[text()=$currentvalueliteral]"; if ($this->running_javascript()) { - $spansufix = "/ancestor::div[@class='level-wrapper']" . "/descendant::div[@class='definition']" . "/descendant::span[@class='textvalue']"; // Expanding the level input boxes. - $spannode = $this->find('xpath', $inputxpath . $spansufix . '|' . $textareaxpath . $spansufix); - $spannode->click(); + $this->execute('behat_general::i_click_on', [ + $inputxpath . $spansufix . '|' . $textareaxpath . $spansufix, + 'xpath', + ]); $inputfield = $this->find('xpath', $inputxpath . '|' . $textareaxpath); $inputfield->setValue($value); @@ -263,7 +269,6 @@ public function i_replace_rubric_level_with($currentvalue, $value, $criterionnam $fieldnode = $this->find('xpath', $inputxpath . '|' . $textareaxpath); $this->set_rubric_field_value($fieldnode->getAttribute('name'), $value); } - } /** @@ -275,7 +280,6 @@ public function i_replace_rubric_level_with($currentvalue, $value, $criterionnam * @param TableNode $rubric */ public function i_grade_by_filling_the_rubric_with(TableNode $rubric) { - $criteria = $rubric->getRowsHash(); $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' . @@ -288,7 +292,6 @@ public function i_grade_by_filling_the_rubric_with(TableNode $rubric) { // First element -> name, second -> points, third -> Remark. foreach ($criteria as $name => $criterion) { - // We only expect the points and the remark, as the criterion name is $name. if (count($criterion) !== 2) { throw new ExpectationException($stepusage, $this->getSession()); @@ -349,7 +352,6 @@ public function i_grade_by_filling_the_rubric_with(TableNode $rubric) { * @return void */ public function the_level_with_points_was_previously_selected_for_the_rubric_criterion($points, $criterionname) { - $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points) . "[contains(concat(' ', normalize-space(@class), ' '), ' currentchecked ')]"; @@ -378,7 +380,6 @@ public function the_level_with_points_was_previously_selected_for_the_rubric_cri * @return void */ public function the_level_with_points_is_selected_for_the_rubric_criterion($points, $criterionname) { - $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points); @@ -409,7 +410,6 @@ public function the_level_with_points_is_selected_for_the_rubric_criterion($poin * @return void */ public function the_level_with_points_is_not_selected_for_the_rubric_criterion($points, $criterionname) { - $levelxpath = $this->get_criterion_xpath($criterionname) . $this->get_level_xpath($points); @@ -437,12 +437,13 @@ public function the_level_with_points_is_not_selected_for_the_rubric_criterion($ * @return void */ protected function set_rubric_field_value($name, $value, $visible = false) { - // Fields are hidden by default. if ($this->running_javascript() == true && $visible === false) { $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]"; - $textnode = $this->find('xpath', $xpath); - $textnode->click(); + $this->execute('behat_general::i_click_on', [ + $xpath, + 'xpath', + ]); } // Set the value now. @@ -457,19 +458,20 @@ protected function set_rubric_field_value($name, $value, $visible = false) { * @return void */ protected function click_and_confirm($node) { - // Clicks to perform the action. - $node->click(); + $this->execute('behat_general::i_click_on', [ + $node, + 'NodeElement', + ]); // Confirms the delete. if ($this->running_javascript()) { - $confirmbutton = $this->get_node_in_container( - 'button', + $this->execute('behat_general::i_click_on_in_the', [ get_string('yes'), + 'button', + get_string('confirmation', 'admin'), 'dialogue', - get_string('confirmation', 'admin') - ); - $confirmbutton->click(); + ]); } } diff --git a/grade/lib.php b/grade/lib.php index 04eb90eb54d02..1aa9e9e5b9eb6 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -770,8 +770,8 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) { break; } foreach ($plugins as $plugin) { - if (is_a($plugin, 'grade_plugin_info')) { - if ($active_plugin == $plugin->id) { + if (is_a($plugin, grade_plugin_info::class)) { + if ($plugin_type === $active_type && $active_plugin == $plugin->id) { $plugin_info['strings']['active_plugin_str'] = $plugin->string; } } @@ -880,7 +880,7 @@ public function __construct($id, $link, $string, $parent=null) { * @param string|null $headerhelpidentifier The help string identifier if required. * @param string|null $headerhelpcomponent The component for the help string. * @param stdClass|null $user The user object for use with the user context header. - * @param actionbar|null $actionbar The actions bar which will be displayed on the page if $shownavigation is set + * @param action_bar|null $actionbar The actions bar which will be displayed on the page if $shownavigation is set * to true. If $actionbar is not explicitly defined, the general action bar * (\core_grades\output\general_action_bar) will be used by default. * @param boolean $showtitle If set to false just show course full name as a title. @@ -922,7 +922,19 @@ function print_grade_page_head(int $courseid, string $active_type, ?string $acti } else { $PAGE->set_pagelayout('admin'); } - $PAGE->set_title(get_string('grades') . ': ' . $stractive_type); + $coursecontext = context_course::instance($courseid); + // Title will be constituted by information starting from the unique identifying information for the page. + if (in_array($active_type, ['report', 'settings'])) { + $uniquetitle = $stractive_plugin; + } else { + $uniquetitle = $stractive_type . ': ' . $stractive_plugin; + } + $titlecomponents = [ + $uniquetitle, + get_string('grades'), + $coursecontext->get_context_name(false), + ]; + $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titlecomponents)); $PAGE->set_heading($title); $PAGE->set_secondary_active_tab('grades'); @@ -988,8 +1000,7 @@ function print_grade_page_head(int $courseid, string $active_type, ?string $acti echo $output; } - $returnval .= print_natural_aggregation_upgrade_notice($courseid, context_course::instance($courseid), $PAGE->url, - $return); + $returnval .= print_natural_aggregation_upgrade_notice($courseid, $coursecontext, $PAGE->url, $return); if ($return) { return $returnval; diff --git a/grade/report/grader/styles.css b/grade/report/grader/styles.css index 37ffc5407a5fe..142056058b1ac 100644 --- a/grade/report/grader/styles.css +++ b/grade/report/grader/styles.css @@ -201,3 +201,9 @@ border: 1px solid #ccc; border-radius: 4px; } + +@media only screen and (min-width: 768px) { + .path-grade-report-grader .gradeparent table { + padding-right: 6em; + } +} diff --git a/grade/report/singleview/classes/local/screen/tablelike.php b/grade/report/singleview/classes/local/screen/tablelike.php index 6d06f36acf013..38b2d95eabcf4 100644 --- a/grade/report/singleview/classes/local/screen/tablelike.php +++ b/grade/report/singleview/classes/local/screen/tablelike.php @@ -225,7 +225,7 @@ public function bulk_insert() { return html_writer::tag( 'div', (new bulk_insert($this->item))->html(), - ['class' => 'singleview_bulk', 'hidden' => true] + ['class' => 'singleview_bulk', 'hidden' => 'hidden'] ); } diff --git a/grade/report/singleview/classes/local/ui/dropdown_attribute.php b/grade/report/singleview/classes/local/ui/dropdown_attribute.php index d2726304b6d70..1c30a8e2d35b7 100644 --- a/grade/report/singleview/classes/local/ui/dropdown_attribute.php +++ b/grade/report/singleview/classes/local/ui/dropdown_attribute.php @@ -105,7 +105,6 @@ public function html(): string { 'name' => $this->name, 'value' => $this->selected, 'text' => $options[$selected], - 'tabindex' => 1, 'disabled' => !empty($this->isdisabled), 'readonly' => $this->isreadonly, 'options' => array_map(function($option) use ($options, $selected) { diff --git a/grade/report/singleview/classes/local/ui/text_attribute.php b/grade/report/singleview/classes/local/ui/text_attribute.php index ead123e950089..164f1ec4e94f0 100644 --- a/grade/report/singleview/classes/local/ui/text_attribute.php +++ b/grade/report/singleview/classes/local/ui/text_attribute.php @@ -85,10 +85,8 @@ public function html(): string { $context->label = ''; if (preg_match("/^feedback/", $this->name)) { $context->label = get_string('feedbackfor', 'gradereport_singleview', $this->label); - $context->tabindex = '2'; } else if (preg_match("/^finalgrade/", $this->name)) { $context->label = get_string('gradefor', 'gradereport_singleview', $this->label); - $context->tabindex = '1'; } return $OUTPUT->render_from_template('gradereport_singleview/text_attribute', $context); diff --git a/grade/report/singleview/templates/dropdown_attribute.mustache b/grade/report/singleview/templates/dropdown_attribute.mustache index e2d225ab65fc4..d04a631708aae 100644 --- a/grade/report/singleview/templates/dropdown_attribute.mustache +++ b/grade/report/singleview/templates/dropdown_attribute.mustache @@ -33,7 +33,7 @@ {{/readonly}} {{^readonly}} - {{#options}} {{/options}} diff --git a/grade/report/singleview/templates/grade_item_selector.mustache b/grade/report/singleview/templates/grade_item_selector.mustache index da9fc322ad90e..1963b3a7669be 100644 --- a/grade/report/singleview/templates/grade_item_selector.mustache +++ b/grade/report/singleview/templates/grade_item_selector.mustache @@ -31,23 +31,23 @@ }}