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/presets/classes/local/setting/adminpresets_admin_setting_configmultiselect.php b/admin/presets/classes/local/setting/adminpresets_admin_setting_configmultiselect.php index 7ae72675a3285..025de6b1a11e0 100644 --- a/admin/presets/classes/local/setting/adminpresets_admin_setting_configmultiselect.php +++ b/admin/presets/classes/local/setting/adminpresets_admin_setting_configmultiselect.php @@ -26,42 +26,14 @@ */ class adminpresets_admin_setting_configmultiselect extends adminpresets_setting { - /** - * Ensure that the $value values are setting choices. - * - * @param mixed $value Setting value - * @return mixed Returns false if wrong param value - */ - protected function set_value($value) { - if ($value) { - $options = explode(',', $value); - foreach ($options as $option) { - - foreach ($this->settingdata->choices as $key => $choice) { - - if ($key == $option) { - $this->value = $option; - $this->set_visiblevalue(); - return true; - } - } - } - - $value = implode(',', $options); - } - $this->value = $value; - $this->set_visiblevalue(); - - return true; - } - protected function set_visiblevalue() { $values = explode(',', $this->value); $visiblevalues = []; foreach ($values as $value) { - if (!empty($this->settingdata->choices[$value])) { + // Ensure that each value exists as a setting choice. + if (array_key_exists($value, $this->settingdata->choices)) { $visiblevalues[] = $this->settingdata->choices[$value]; } } diff --git a/admin/renderer.php b/admin/renderer.php index 0c66010ed7305..30365a1774106 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -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); diff --git a/admin/searchreindex.php b/admin/searchreindex.php index b149e75f3a0c9..158e3fc520df9 100644 --- a/admin/searchreindex.php +++ b/admin/searchreindex.php @@ -32,7 +32,7 @@ admin_externalpage_setup('searchareas', '', null, (new moodle_url('/admin/searchreindex.php'))->out(false)); // Get area parameter and check it exists. -$areaid = required_param('areaid', PARAM_ALPHAEXT); +$areaid = required_param('areaid', PARAM_ALPHANUMEXT); $area = \core_search\manager::get_search_area($areaid); if ($area === false) { throw new moodle_exception('invalidrequest'); @@ -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/settings/security.php b/admin/settings/security.php index 72fac4a77d9e2..a703f9e0155ee 100644 --- a/admin/settings/security.php +++ b/admin/settings/security.php @@ -145,7 +145,7 @@ $temp->add(new admin_setting_heading('adminpresets', new lang_string('siteadminpresetspluginname', 'core_adminpresets'), '')); $sensiblesettingsdefault = 'recaptchapublickey@@none, recaptchaprivatekey@@none, googlemapkey3@@none, '; $sensiblesettingsdefault .= 'secretphrase@@url, cronremotepassword@@none, smtpuser@@none, '; - $sensiblesettingsdefault .= 'smtppass@none, proxypassword@@none, quizpassword@@quiz, allowedip@@none, blockedip@@none, '; + $sensiblesettingsdefault .= 'smtppass@@none, proxypassword@@none, quizpassword@@quiz, allowedip@@none, blockedip@@none, '; $sensiblesettingsdefault .= 'dbpass@@logstore_database, messageinbound_hostpass@@none, '; $sensiblesettingsdefault .= 'bind_pw@@auth_cas, pass@@auth_db, bind_pw@@auth_ldap, '; $sensiblesettingsdefault .= 'dbpass@@enrol_database, bind_pw@@enrol_ldap, '; diff --git a/admin/tool/admin_presets/classes/form/import_form.php b/admin/tool/admin_presets/classes/form/import_form.php index c7ee781ff206c..6738a06f5e492 100644 --- a/admin/tool/admin_presets/classes/form/import_form.php +++ b/admin/tool/admin_presets/classes/form/import_form.php @@ -42,8 +42,8 @@ public function definition(): void { $mform->setType('name', PARAM_TEXT); // File upload. - $mform->addElement('filepicker', 'xmlfile', - get_string('selectfile', 'tool_admin_presets')); + $mform->addElement('filepicker', 'xmlfile', get_string('selectfile', 'tool_admin_presets'), null, + ['accepted_types' => ['.xml']]); $mform->addRule('xmlfile', null, 'required'); $this->add_action_buttons(true, get_string('import', 'tool_admin_presets')); diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature index 19bf84211d0ab..0a9cfd133566c 100644 --- a/admin/tool/behat/tests/behat/data_generators.feature +++ b/admin/tool/behat/tests/behat/data_generators.feature @@ -100,8 +100,7 @@ Feature: Set up contextual data for tests And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | - When I log in as "student1" - And I am on "Course 1" course homepage + When I am on the "Course 1" course page logged in as student1 Then I should see "Topic 1" Scenario: Add role assigns @@ -135,17 +134,11 @@ Feature: Set up contextual data for tests When I log in as "user1" And I am on site homepage Then edit mode should be available on the current page - And I log out - And I log in as "user2" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as user2 Then edit mode should be available on the current page - And I log out - And I log in as "user3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as user3 Then edit mode should be available on the current page - And I log out - And I log in as "user4" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as user4 Then edit mode should be available on the current page And I log out And I log in as "user5" @@ -187,8 +180,7 @@ Feature: Set up contextual data for tests And the following "activities" exist: | activity | name | intro | course | idnumber | grade | | assign | Test assignment name with scale | Test assignment description | C1 | assign1 | Test Scale 1 | - When I log in as "admin" - And I am on "Course 1" course homepage + When I am on the "Course 1" course page logged in as admin Then I should see "Test assignment name" # Assignment 2.2 module type is disabled by default # And I should see "Test assignment22 name" @@ -304,9 +296,7 @@ Feature: Set up contextual data for tests And the following "grade categories" exist: | fullname | course | gradecategory | | Grade sub category 2 | C1 | Grade category 1 | - When I log in as "admin" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + When I am on the "Course 1" "grades > Grader report > View" page logged in as "admin" Then I should see "Grade category 1" And I should see "Grade sub category 2" @@ -327,9 +317,7 @@ Feature: Set up contextual data for tests | itemname | course | gradecategory | | Test Grade Item 2 | C1 | Grade category 1 | | Test Grade Item 3 | C1 | Grade sub category 2 | - When I log in as "admin" - And I am on "Course 1" course homepage - And I navigate to "Setup > Gradebook setup" in the course gradebook + When I am on the "Course 1" "grades > gradebook setup" page logged in as "admin" Then I should see "Test Grade Item 1" And I open the action menu in "Test Grade Item 1" "table_row" And I choose "Edit" in the open action menu @@ -358,9 +346,7 @@ Feature: Set up contextual data for tests And the following "scales" exist: | name | scale | | Test Scale 1 | Disappointing, Good, Very good, Excellent | - When I log in as "admin" - And I am on "Course 1" course homepage - And I navigate to "Scales" in the course gradebook + When I am on the "Course 1" "grades > scales" page logged in as admin Then I should see "Test Scale 1" And I should see "Disappointing, Good, Very good, Excellent" @@ -379,9 +365,7 @@ Feature: Set up contextual data for tests | Grade outcome 2 | OT2 | C1 | Test Scale 1 | And the following config values are set as admin: | enableoutcomes | 1 | - When I log in as "admin" - And I am on "Course 1" course homepage - And I navigate to "More > Outcomes" in the course gradebook + When I am on the "Course 1" "grades > outcomes" page logged in as admin Then I should see "Grade outcome 1" in the "#addoutcomes" "css_element" And I should see "Grade outcome 2" in the "#removeoutcomes" "css_element" And I press "Manage outcomes" @@ -407,9 +391,7 @@ Feature: Set up contextual data for tests | Test Outcome Grade Item 1 | C1 | OT1 | Grade category 1 | And the following config values are set as admin: | enableoutcomes | 1 | - When I log in as "admin" - And I am on "Course 1" course homepage - And I navigate to "Setup > Gradebook setup" in the course gradebook + When I am on the "Course 1" "grades > gradebook setup" page logged in as "admin" Then I should see "Test Outcome Grade Item 1" And I open the action menu in "Test Outcome Grade Item 1" "table_row" And I choose "Edit" in the open action menu @@ -425,6 +407,5 @@ Feature: Set up contextual data for tests And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | online_users | Course | C1 | course-view-* | site-pre | - When I log in as "admin" - And I am on "Course 1" course homepage + When I am on the "Course 1" course page logged in as admin Then I should see "Online users" diff --git a/admin/tool/dataprivacy/tests/behat/protecteddelete.feature b/admin/tool/dataprivacy/tests/behat/protecteddelete.feature index b4e36dde72d80..978da609881fe 100644 --- a/admin/tool/dataprivacy/tests/behat/protecteddelete.feature +++ b/admin/tool/dataprivacy/tests/behat/protecteddelete.feature @@ -30,6 +30,12 @@ Feature: Protected data should not be deleted | Site purpose | PT1H | 0 | | prot | P1D | 1 | | unprot | P1D | 0 | + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | u1 | forump1 | Discussion subject | Test post in forump1 | + | u1 | forumu1 | Discussion subject | Test post in forumu1 | + | u1 | forump2 | Discussion subject | Test post in forump2 | + | u1 | forumu2 | Discussion subject | Test post in forumu2 | And I set the category and purpose for the "forump1" "forum" in course "C1" to "CAT" and "prot" And I set the category and purpose for the "forump2" "forum" in course "C2" to "CAT" and "prot" And I set the category and purpose for the "forumu1" "forum" in course "C1" to "CAT" and "unprot" @@ -38,47 +44,25 @@ Feature: Protected data should not be deleted @javascripta Scenario: Unexpired and protected data is not removed - Given I log in as "u1" - And I am on "C1" course homepage - And I add a new discussion to "forump1" forum with: - | Subject | Discussion subject | - | Message | Test post in forump1 | - And I am on "C1" course homepage - And I add a new discussion to "forumu1" forum with: - | Subject | Discussion subject | - | Message | Test post in forumu1 | - And I am on "C2" course homepage - And I add a new discussion to "forump2" forum with: - | Subject | Discussion subject | - | Message | Test post in forump2 | - And I am on "C2" course homepage - And I add a new discussion to "forumu2" forum with: - | Subject | Discussion subject | - | Message | Test post in forumu2 | - And I log out - And I log in as "admin" + Given I log in as "admin" And I create a dataprivacy "delete" request for "u1" And I approve a dataprivacy "delete" request for "u1" And I run all adhoc tasks And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Deleted" in the "u1" "table_row" - And I am on "C1" course homepage - And I follow "forump1" + And I am on the "forump1" "forum activity" page And I follow "Discussion subject" Then I should not see "Test post in forump1" - When I am on "C1" course homepage - And I follow "forumu1" + When I am on the "forumu1" "forum activity" page And I follow "Discussion subject" Then I should not see "Test post in forumu1" - And I am on "C2" course homepage - And I follow "forump2" + And I am on the "forump2" "forum activity" page And I follow "Discussion subject" Then I should see "Test post in forump2" - When I am on "C2" course homepage - And I follow "forumu2" + When I am on the "forumu2" "forum activity" page And I follow "Discussion subject" Then I should not see "Test post in forumu2" 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/lpimportcsv/export.php b/admin/tool/lpimportcsv/export.php index a2d4b390a2c06..0dc0a03d86ee6 100644 --- a/admin/tool/lpimportcsv/export.php +++ b/admin/tool/lpimportcsv/export.php @@ -25,6 +25,8 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir.'/adminlib.php'); +admin_externalpage_setup('toollpexportcsv'); + $pagetitle = get_string('exportnavlink', 'tool_lpimportcsv'); $context = context_system::instance(); diff --git a/admin/tool/lpimportcsv/index.php b/admin/tool/lpimportcsv/index.php index 2c541d378a74b..8c25e02debb50 100644 --- a/admin/tool/lpimportcsv/index.php +++ b/admin/tool/lpimportcsv/index.php @@ -25,6 +25,8 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir.'/adminlib.php'); +admin_externalpage_setup('toollpimportcsv'); + $pagetitle = get_string('pluginname', 'tool_lpimportcsv'); $context = context_system::instance(); 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/classes/external.php b/admin/tool/mobile/classes/external.php index f9e56a57b1c34..ed079f36bc057 100644 --- a/admin/tool/mobile/classes/external.php +++ b/admin/tool/mobile/classes/external.php @@ -319,7 +319,7 @@ public static function get_autologin_key($privatetoken) { $timenow = time(); if ($timenow - $last < $mintimereq) { $minutes = $mintimereq / MINSECS; - throw new moodle_exception('autologinkeygenerationlockout', 'tool_mobile', $minutes); + throw new moodle_exception('autologinkeygenerationlockout', 'tool_mobile', '', $minutes); } set_user_preference('tool_mobile_autologin_request_last', $timenow, $USER); 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/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index 9e7a02e44e224..1e7bd0a387c3e 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -387,18 +387,18 @@ public function test_get_autologin_key_missing_locked() { $result = external::get_autologin_key($token->privatetoken); $result = \external_api::clean_returnvalue(external::get_autologin_key_returns(), $result); - // Change min time between requests to 30 seconds. - set_config('autologinmintimebetweenreq', 30, 'tool_mobile'); + // Change min time between requests to 3 minutes. + set_config('autologinmintimebetweenreq', 3 * MINSECS, 'tool_mobile'); - // Mock a previous request, 60 seconds ago. - $mocktime = time() - MINSECS; + // Mock a previous request, 4 minutes ago. + $mocktime = time() - (4 * MINSECS); set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER); - $result = external::get_autologin_key($token->privatetoken); // All good, we were expecint 30 seconds or more. + $result = external::get_autologin_key($token->privatetoken); $result = \external_api::clean_returnvalue(external::get_autologin_key_returns(), $result); // We just requested one token, we must wait. $this->expectException('moodle_exception'); - $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile')); + $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile', 3)); $result = external::get_autologin_key($token->privatetoken); } diff --git a/admin/tool/policy/classes/output/page_viewalldoc.php b/admin/tool/policy/classes/output/page_viewalldoc.php index 801aac0c51641..3ffde42730cf9 100644 --- a/admin/tool/policy/classes/output/page_viewalldoc.php +++ b/admin/tool/policy/classes/output/page_viewalldoc.php @@ -48,15 +48,18 @@ */ class page_viewalldoc implements renderable, templatable { - /** @var string Return url */ - private $returnurl; + /** @var ?moodle_url Return url */ + private $returnurl = null; /** * Prepare the page for rendering. * */ public function __construct($returnurl) { - $this->returnurl = $returnurl; + if (!empty($returnurl)) { + $this->returnurl = new moodle_url($returnurl); + } + $this->prepare_global_page_access(); $this->prepare_policies(); } diff --git a/admin/tool/recyclebin/tests/behat/backup_user_data.feature b/admin/tool/recyclebin/tests/behat/backup_user_data.feature index b6012e84eefc9..0eccc5944df8e 100644 --- a/admin/tool/recyclebin/tests/behat/backup_user_data.feature +++ b/admin/tool/recyclebin/tests/behat/backup_user_data.feature @@ -54,10 +54,7 @@ Feature: Backup user data And I navigate to "Recycle bin" in current page administration And I should see "Quiz 1" And I click on "Restore" "link" in the "region-main" "region" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage - When I navigate to "User report" in the course gradebook + When I am on the "Course 1" "grades > User report > View" page logged in as "student1" Then "Quiz 1" row "Grade" column of "user-grade" table should contain "50" And "Quiz 1" row "Percentage" column of "user-grade" table should contain "50" diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index dc648df82160d..ac1e78bc45d26 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -101,7 +101,7 @@ class tool_uploadcourse_course { /** @var array fields allowed as course data. */ static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate', 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes', - 'groupmode', 'groupmodeforce', 'enablecompletion', 'downloadcontent'); + 'groupmode', 'groupmodeforce', 'enablecompletion', 'downloadcontent', 'showactivitydates'); /** @var array fields required on course creation. */ static protected $mandatoryfields = array('fullname', 'category'); diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php index 4cd4f27c2bea3..ffd6ba06f7996 100644 --- a/admin/tool/uploadcourse/classes/step2_form.php +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -192,6 +192,10 @@ public function definition () { $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion'); } + $mform->addElement('selectyesno', 'defaults[showactivitydates]', get_string('showactivitydates')); + $mform->addHelpButton('defaults[showactivitydates]', 'showactivitydates'); + $mform->setDefault('defaults[showactivitydates]', $courseconfig->showactivitydates); + // Add custom fields to the form. $handler = \core_course\customfield\course_handler::create(); $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse'); diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index 5068670d8b2c9..2b50730ca50ef 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -166,6 +166,7 @@ $defaults['visible'] = $courseconfig->visible; $defaults['lang'] = $courseconfig->lang; $defaults['enablecompletion'] = $courseconfig->enablecompletion; +$defaults['showactivitydates'] = $courseconfig->showactivitydates; // Course template. if (isset($options['templatecourse'])) { diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index d664621904c01..fbd749de352dc 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -425,6 +425,7 @@ public function test_data_saved() { 'groupmode' => '2', 'groupmodeforce' => '1', 'enablecompletion' => '1', + 'showactivitydates' => '1', 'tags' => 'Cat, Dog', 'role_teacher' => 'Knight', @@ -478,6 +479,7 @@ public function test_data_saved() { $this->assertEquals($data['groupmode'], $course->groupmode); $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce); $this->assertEquals($data['enablecompletion'], $course->enablecompletion); + $this->assertEquals($data['showactivitydates'], $course->showactivitydates); $this->assertEquals($data['tags'], join(', ', \core_tag_tag::get_item_tags_array('core', 'course', $course->id))); // Roles. @@ -530,6 +532,7 @@ public function test_data_saved() { 'groupmode' => '1', 'groupmodeforce' => '0', 'enablecompletion' => '0', + 'showactivitydates' => '0', 'role_teacher' => 'Teacher', 'role_manager' => 'Manager', @@ -583,6 +586,7 @@ public function test_data_saved() { $this->assertEquals($data['groupmode'], $course->groupmode); $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce); $this->assertEquals($data['enablecompletion'], $course->enablecompletion); + $this->assertEquals($data['showactivitydates'], $course->showactivitydates); // Roles. $roleids = array(); @@ -644,6 +648,7 @@ public function test_default_data_saved() { 'groupmode' => '2', 'groupmodeforce' => '1', 'enablecompletion' => '1', + 'showactivitydates' => '1', ); $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); @@ -673,6 +678,7 @@ public function test_default_data_saved() { $this->assertEquals($defaultdata['groupmode'], $course->groupmode); $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce); $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion); + $this->assertEquals($defaultdata['showactivitydates'], $course->showactivitydates); // Update. $cat = $this->getDataGenerator()->create_category(); @@ -701,6 +707,7 @@ public function test_default_data_saved() { 'groupmode' => '1', 'groupmodeforce' => '0', 'enablecompletion' => '0', + 'showactivitydates' => '0', ); $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); @@ -730,6 +737,7 @@ public function test_default_data_saved() { $this->assertEquals($defaultdata['groupmode'], $course->groupmode); $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce); $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion); + $this->assertEquals($defaultdata['showactivitydates'], $course->showactivitydates); } public function test_rename() { 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/analytics/classes/analysable.php b/analytics/classes/analysable.php index 97faf596e8ecf..968de63f68374 100644 --- a/analytics/classes/analysable.php +++ b/analytics/classes/analysable.php @@ -37,8 +37,9 @@ interface analysable { /** * Max timestamp. + * We are limited by both PHP's max int value and DB (cross-db) max int allowed. Use the smallest one. */ - const MAX_TIME = 9999999999; + const MAX_TIME = PHP_INT_MAX < 9999999999 ? PHP_MAX_INT : 9999999999; /** * The analysable unique identifier in the site. 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/oauth2/confirm-linkedlogin.php b/auth/oauth2/confirm-linkedlogin.php index 879655ae92bbf..cf031be5ba230 100644 --- a/auth/oauth2/confirm-linkedlogin.php +++ b/auth/oauth2/confirm-linkedlogin.php @@ -48,11 +48,7 @@ throw new \moodle_exception('cannotfinduser', '', '', $userid); } - if (!$user->suspended) { - complete_user_login($user); - - \core\session\manager::apply_concurrent_login_limit($user->id, session_id()); - + if ($user->id == $USER->id) { // Check where to go, $redirect has a higher preference. if (empty($redirect) and !empty($SESSION->wantsurl) ) { $redirect = $SESSION->wantsurl; @@ -69,14 +65,24 @@ $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); - echo "

".get_string("thanks").", ". fullname($USER) . "

\n"; + echo "

".get_string("thanks").", ". fullname($user) . "

\n"; echo "

".get_string("confirmed")."

\n"; - echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); + // If $wantsurl and $redirect are empty, then the button will navigate the identical user to the dashboard. + if ($user->id == $USER->id) { + echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); + } else if (!isloggedin() || isguestuser()) { + echo $OUTPUT->single_button(get_login_url(), get_string('login')); + } else { + echo $OUTPUT->single_button("$CFG->wwwroot/login/logout.php", get_string('logout')); + } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else { - \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); + // Avoid error if logged-in user visiting the page. + if (!isloggedin()) { + \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); + } } redirect("$CFG->wwwroot/"); 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/backup/util/dbops/backup_structure_dbops.class.php b/backup/util/dbops/backup_structure_dbops.class.php index 8ffc63e2efd5a..249a55d27da3b 100644 --- a/backup/util/dbops/backup_structure_dbops.class.php +++ b/backup/util/dbops/backup_structure_dbops.class.php @@ -72,7 +72,7 @@ public static function convert_params_to_values($params, $processor) { throw new base_element_struct_exception('valueofparamelementnotset', $param->get_name()); } - } else if ($param < 0) { // Possibly one processor variable, let's process it + } else if (is_int($param) && $param < 0) { // Possibly one processor variable, let's process it // See @backup class for all the VAR_XXX variables available. // Note1: backup::VAR_PARENTID is handled by nested elements themselves // Note2: trying to use one non-existing var will throw exception diff --git a/backup/util/dbops/tests/backup_structure_dbops_test.php b/backup/util/dbops/tests/backup_structure_dbops_test.php new file mode 100644 index 0000000000000..69b38514202a0 --- /dev/null +++ b/backup/util/dbops/tests/backup_structure_dbops_test.php @@ -0,0 +1,117 @@ +. + +namespace core_backup; + +use backup_structure_dbops; + +/** + * Tests for backup_structure_dbops + * + * @package core_backup + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \backup_structure_dbops + */ +class backup_structure_dbops_test extends \advanced_testcase { + public static function setUpBeforeClass(): void { + global $CFG; + parent::setUpBeforeClass(); + require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php"); + } + + /** + * Tests for convert_params_to_values. + * + * @dataProvider convert_params_to_values_provider + * @param array $params + * @param mixed $processor + * @param array $expected + */ + public function test_convert_params_to_values( + array $params, + $processor, + array $expected + ): void { + if (is_callable($processor)) { + $newprocessor = $this->createMock(\backup_structure_processor::class); + $newprocessor->method('get_var')->willReturnCallback($processor); + $processor = $newprocessor; + } + + $result = backup_structure_dbops::convert_params_to_values($params, $processor); + + $this->assertEqualsCanonicalizing($expected, $result); + } + + /** + * Data provider for convert_params_to_values_provider. + */ + public static function convert_params_to_values_provider(): array { + return [ + 'String value is not processed' => [ + ['/0/1/2/345'], + null, + ['/0/1/2/345'], + ], + 'Positive integer' => [ + [123, 456], + null, + [123, 456], + ], + 'Negative integer' => [ + [-42], + function () { + return 'Life, the Universe, and Everything'; + }, + ['Life, the Universe, and Everything'], + ], + 'Mix of strings, and ints with a processor' => [ + ['foo', 123, 'bar', -42], + function () { + return 'Life, the Universe, and Everything'; + }, + ['foo', 123, 'bar', 'Life, the Universe, and Everything'], + ], + ]; + } + + /** + * Tests for convert_params_to_values with an atom. + */ + public function test_convert_params_to_values_with_atom(): void { + $atom = $this->createMock(\base_atom::class); + $atom->method('is_set')->willReturn(true); + $atom->method('get_value')->willReturn('Some atomised value'); + + $result = backup_structure_dbops::convert_params_to_values([$atom], null); + + $this->assertEqualsCanonicalizing(['Some atomised value'], $result); + } + + /** + * Tests for convert_params_to_values with an atom without any value. + */ + public function test_convert_params_to_values_with_atom_no_value(): void { + $atom = $this->createMock(\base_atom::class); + $atom->method('is_set')->willReturn(false); + $atom->method('get_name')->willReturn('Atomisd name'); + + $this->expectException(\base_element_struct_exception::class); + backup_structure_dbops::convert_params_to_values([$atom], null); + } +} diff --git a/badges/classes/backpack_api.php b/badges/classes/backpack_api.php index e0db0035400e1..c7c49cec06eaa 100644 --- a/badges/classes/backpack_api.php +++ b/badges/classes/backpack_api.php @@ -639,6 +639,8 @@ public function disconnect_backpack($userid, $backpackid) { $DB->delete_records('badge_external', array('backpackid' => $backpackid)); $DB->delete_records('badge_backpack', array('userid' => $userid)); $badgescache->delete($userid); + $this->clear_system_user_session(); + return true; } diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index 96e51b0184d8a..31bb51c82004f 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -229,12 +229,19 @@ Feature: Award badges @javascript Scenario: Award badge on course completion - Given I log in as "teacher1" + Given the following "activity" exists: + | activity | chat | + | course | C1 | + | name | Music history | + | section | 1 | + | completion | 2 | + | completionview | 1 | + And I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Course completion" in current page administration And I set the field "id_overall_aggregation" to "2" And I click on "Condition: Activity completion" "link" - And I set the field "Assignment - Test assignment name" to "1" + And I set the field "Chat - Music history" to "1" And I press "Save changes" And I am on "Course 1" course homepage And I navigate to "Badges > Add a new badge" in current page administration @@ -253,8 +260,7 @@ Feature: Award badges And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" Then I should not see "badges" - And I am on "Course 1" course homepage - And I toggle the manual completion state of "Test assignment name" + When I am on the "Music history" "chat activity" page And I log out # Completion cron won't mark the whole course completed unless the # individual criteria was marked completed more than a second ago. So @@ -265,6 +271,7 @@ Feature: Award badges # The student should now see their badge. And I log in as "student1" And I follow "Profile" in the user menu + And I click on "Course 1" "link" in the "region-main" "region" Then I should see "Course Badge" @javascript diff --git a/blocks/activity_results/tests/behat/addblockinactivity.feature b/blocks/activity_results/tests/behat/addblockinactivity.feature index c9d32db7c278f..e80658cbe5878 100644 --- a/blocks/activity_results/tests/behat/addblockinactivity.feature +++ b/blocks/activity_results/tests/behat/addblockinactivity.feature @@ -32,18 +32,15 @@ Feature: The activity results block displays student scores And the following "activities" exist: | activity | name | content | course | section | idnumber | | page | Test page name | This is a page | C1 | 1 | page1 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment 1 | student1 | 90.00 | + | Test assignment 1 | student2 | 80.00 | + | Test assignment 1 | student3 | 70.00 | + | Test assignment 1 | student4 | 60.00 | + | Test assignment 1 | student5 | 50.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I should see "Test page name" - And I navigate to "View > Grader report" in the course gradebook - And I change window size to "large" - And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment 1" - And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment 1" - And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment 1" - And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment 1" - And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment 1" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on a non-graded activity to show 3 high scores Given I am on the "Test page name" "page activity" page diff --git a/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature b/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature index aba0c34c46e13..97b6fe1c2f3dd 100644 --- a/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature @@ -27,16 +27,15 @@ Feature: The activity results block displays student high scores And the following "activities" exist: | activity | name | intro | course | section | idnumber | assignsubmission_file_enabled | | assign | Test assignment | Offline text | C1 | 1 | assign1 | 0 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment | student1 | 90.00 | + | Test assignment | student2 | 80.00 | + | Test assignment | student3 | 70.00 | + | Test assignment | student4 | 60.00 | + | Test assignment | student5 | 50.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook - And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment" - And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment" - And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" - And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 0 high scores Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/highscoreswithscales.feature b/blocks/activity_results/tests/behat/highscoreswithscales.feature index 0af28c4aee5e2..3e9caa8294fb6 100644 --- a/blocks/activity_results/tests/behat/highscoreswithscales.feature +++ b/blocks/activity_results/tests/behat/highscoreswithscales.feature @@ -31,23 +31,15 @@ Feature: The activity results block displays students high scores in group as sc | name | Test assignment | | intro | Offline text | | assignsubmission_file_enabled | 0 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "Scales" in the course gradebook - And I press "Add a new scale" - And I set the following fields to these values: - | Name | My Scale | - | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | - And I press "Save changes" - And I am on "Course 1" course homepage - And I follow "Test assignment" - And I navigate to "Settings" in current page administration + And the following "scales" exist: + | name | scale | + | My Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | And I press "Save and return to course" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" diff --git a/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature b/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature index d3b3b68634183..fbada7aecd7ae 100644 --- a/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature @@ -44,25 +44,19 @@ Feature: The activity results block displays student in group high scores as sca And the following "activities" exist: | activity | name | intro | course | section | idnumber | | assign | Test assignment | Offline text | C1 | 1 | assign1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I navigate to "Scales" in the course gradebook - And I press "Add a new scale" - And I set the following fields to these values: - | Name | My Scale | - | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | - And I press "Save changes" - And I am on "Course 1" course homepage - And I am on the "Test assignment" "assign activity" page - And I navigate to "Settings" in current page administration + And the following "scales" exist: + | name | scale | + | My Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I change window size to "large" + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | assignsubmission_file_enabled | 0 | | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | | Group mode | Separate groups | And I press "Save and return to course" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I am on the "Course 1" "grades > Grader report > View" page And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" And I give the grade "Very good" to the user "Student 3" for the grade item "Test assignment" @@ -83,9 +77,7 @@ Feature: The activity results block displays student in group high scores as sca And I press "Save changes" Then I should see "Group 1" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "Student 1" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" @@ -104,9 +96,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Very good" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out - And I log in as "student3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student3 And I should see "Student 3" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -127,10 +117,8 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Excellent!" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out # Students cannot see user identity fields. - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should not see "User S1" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" @@ -150,9 +138,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Excellent!" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature b/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature index daa5d10a11d0d..52dab9dbb78af 100644 --- a/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature @@ -46,21 +46,19 @@ Feature: The activity results block displays student in separate groups scores | course | C1 | | idnumber | 0001 | | name | Test assignment | - | intro | Offline text | | section | 1 | | assignsubmission_file_enabled | 0 | | groupmode | 1 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment | student1 | 100.00 | + | Test assignment | student2 | 90.00 | + | Test assignment | student3 | 90.00 | + | Test assignment | student4 | 80.00 | + | Test assignment | student5 | 80.00 | + | Test assignment | student6 | 70.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook - And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" - And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 high score Given I add the "Activity results" block @@ -88,9 +86,7 @@ Feature: The activity results block displays student in separate groups scores And I press "Save changes" Then I should see "Group 1" in the "Activity results" "block" And I should see "95.00/100.00" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "Student 1" in the "Activity results" "block" And I should see "100.00/100.00" in the "Activity results" "block" @@ -106,9 +102,7 @@ Feature: The activity results block displays student in separate groups scores And I press "Save changes" Then I should see "Group 1" in the "Activity results" "block" And I should see "95.00" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "Student 1" in the "Activity results" "block" And I should see "100.00" in the "Activity results" "block" @@ -129,9 +123,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "85%" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75%" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "Student 1" in the "Activity results" "block" And I should see "100%" in the "Activity results" "block" And I should see "Student 2" in the "Activity results" "block" @@ -153,9 +145,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "85.00/100.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75.00/100.00" in the "Activity results" "block" - And I log out - And I log in as "student3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student3 And I should see "Student 3" in the "Activity results" "block" And I should see "90.00/100.00" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -177,9 +167,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "85.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75.00" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "Student 1" in the "Activity results" "block" And I should see "100.00" in the "Activity results" "block" And I should see "Student 2" in the "Activity results" "block" @@ -201,10 +189,8 @@ Feature: The activity results block displays student in separate groups scores And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" - And I log out # Students cannot see user identity fields. - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should not see "User S1" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" @@ -225,9 +211,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "90.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature b/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature index 6b3103b0af3d3..32f247bcf2b30 100644 --- a/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature @@ -42,17 +42,13 @@ Feature: The activity results block displays student in visible groups scores | student5 | G3 | | student6 | G3 | And the following "activities" exist: - | activity | name | intro | course | idnumber | section | assignsubmission_file_enabled | - | assign | Test assignment | Test assignment | C1 | assign1 | 1 | 0 | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I am on the "Test assignment" "assign activity" page - And I navigate to "Settings" in current page administration + | activity | name | course | idnumber | section | assignsubmission_file_enabled | + | assign | Test assignment | C1 | assign1 | 1 | 0 | + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | Group mode | Visible groups | And I press "Save and return to course" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode on And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" @@ -61,7 +57,7 @@ Feature: The activity results block displays student in visible groups scores And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I am on "Course 1" course homepage + And I am on "Course 1" course homepage with editing mode on Scenario: Configure the block on the course page to show 1 high score Given I add the "Activity results" block @@ -87,9 +83,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 1" in the "Activity results" "block" And I should see "95.00/100.00" in the "Activity results" "block" @@ -103,9 +97,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 1" in the "Activity results" "block" And I should see "95.00" in the "Activity results" "block" @@ -120,9 +112,7 @@ Feature: The activity results block displays student in visible groups scores | config_decimalpoints | 0 | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 1" in the "Activity results" "block" And I should see "95%" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -140,9 +130,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 1" in the "Activity results" "block" And I should see "95.00/100.00" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -160,9 +148,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 1" in the "Activity results" "block" And I should see "95.00" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -182,9 +168,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Display only ID numbers | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group" in the "Activity results" "block" And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" @@ -200,9 +184,7 @@ Feature: The activity results block displays student in visible groups scores | config_nameformat | Anonymous results | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group" in the "Activity results" "block" And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature b/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature index 4ed74d718d67e..e3b890580d916 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature @@ -29,19 +29,16 @@ Feature: The activity results block displays student low scores | course | C1 | | idnumber | 0001 | | name | Test assignment | - | intro | Offline text | - | section | 1 | | assignsubmission_file_enabled | 0 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment | student1 | 90.00 | + | Test assignment | student2 | 80.00 | + | Test assignment | student3 | 70.00 | + | Test assignment | student4 | 60.00 | + | Test assignment | student5 | 50.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook - And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 2" for the grade item "Test assignment" - And I give the grade "70.00" to the user "Student 3" for the grade item "Test assignment" - And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" - And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/lowscoreswithscales.feature b/blocks/activity_results/tests/behat/lowscoreswithscales.feature index 54b5654b23203..0a615227f16a1 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithscales.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithscales.feature @@ -32,23 +32,16 @@ Feature: The activity results block displays student low scores as scales | idnumber | 0001 | | section | 1 | | assignsubmission_file_enabled | 0 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "Scales" in the course gradebook - And I press "Add a new scale" - And I set the following fields to these values: - | Name | My Scale | - | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | - And I press "Save changes" - And I am on "Course 1" course homepage with editing mode on - And I am on the "Test assignment" "assign activity" page - And I navigate to "Settings" in current page administration + And the following "scales" exist: + | name | scale | + | My Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | And I press "Save and return to course" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I turn editing mode on + And I am on the "Course 1" "grades > Grader report > View" page And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" And I give the grade "Good" to the user "Student 3" for the grade item "Test assignment" diff --git a/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature b/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature index f0f20b47f2cb9..52df073fa539a 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature @@ -49,23 +49,16 @@ Feature: The activity results block displays students in groups low scores as sc | description | Offline text | | assignsubmission_file_enabled | 0 | | groupmode | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "Scales" in the course gradebook - And I press "Add a new scale" - And I set the following fields to these values: - | Name | My Scale | - | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | - And I press "Save changes" - And I am on "Course 1" course homepage - And I follow "Test assignment" - And I navigate to "Settings" in current page administration + And the following "scales" exist: + | name | scale | + | My Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | + And I change window size to "large" + And I am on the "Test assignment" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | And I press "Save and return to course" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" And I give the grade "Very good" to the user "Student 2" for the grade item "Test assignment" @@ -87,9 +80,7 @@ Feature: The activity results block displays students in groups low scores as sc And I press "Save changes" Then I should see "Group 3" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "Student 6" in the "Activity results" "block" And I should see "Average" in the "Activity results" "block" @@ -106,9 +97,7 @@ Feature: The activity results block displays students in groups low scores as sc And I should see "Very good" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out - And I log in as "student3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student3 And I should see "Student 3" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -128,10 +117,8 @@ Feature: The activity results block displays students in groups low scores as sc Then I should see "Group" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out # Students cannot see user identity fields. - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "User" in the "Activity results" "block" And I should not see "User S5" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" @@ -150,9 +137,7 @@ Feature: The activity results block displays students in groups low scores as sc Then I should see "Group" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "User" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" And I should see "Average" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature b/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature index 2214a8704aaff..589cdc8016a32 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature @@ -42,19 +42,18 @@ Feature: The activity results block displays students in separate groups scores | student5 | G3 | | student6 | G3 | And the following "activities" exist: - | activity | course | idnumber | name | intro | assignsubmission_file_enabled | groupmode | - | assign | C1 | a1 | Test assignment | Offline text | 0 | 1 | + | activity | course | idnumber | name | assignsubmission_file_enabled | groupmode | + | assign | C1 | a1 | Test assignment | 0 | 1 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment | student1 | 100.00 | + | Test assignment | student2 | 90.00 | + | Test assignment | student3 | 90.00 | + | Test assignment | student4 | 80.00 | + | Test assignment | student5 | 80.00 | + | Test assignment | student6 | 70.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook - And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" - And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block @@ -82,9 +81,7 @@ Feature: The activity results block displays students in separate groups scores And I press "Save changes" Then I should see "Group 3" in the "Activity results" "block" And I should see "75.00/100.00" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "Student 6" in the "Activity results" "block" And I should see "70.00/100.00" in the "Activity results" "block" @@ -100,9 +97,7 @@ Feature: The activity results block displays students in separate groups scores And I press "Save changes" Then I should see "Group 3" in the "Activity results" "block" And I should see "75.00" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "Student 6" in the "Activity results" "block" And I should see "70.00" in the "Activity results" "block" @@ -121,9 +116,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "85%" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75%" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "Student 6" in the "Activity results" "block" And I should see "70%" in the "Activity results" "block" And I should see "Student 5" in the "Activity results" "block" @@ -143,9 +136,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "85.00/100.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75.00/100.00" in the "Activity results" "block" - And I log out - And I log in as "student3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student3 And I should see "Student 3" in the "Activity results" "block" And I should see "90.00/100.00" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -165,9 +156,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "85.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75.00" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 And I should see "Student 5" in the "Activity results" "block" And I should see "80.00" in the "Activity results" "block" And I should see "Student 6" in the "Activity results" "block" @@ -188,10 +177,8 @@ Feature: The activity results block displays students in separate groups scores Then I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" - And I log out # Students cannot see user identity fields. - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should not see "User S1" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" @@ -211,9 +198,7 @@ Feature: The activity results block displays students in separate groups scores Then I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" - And I log out - And I log in as "student1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student1 And I should see "User" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "90.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature b/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature index 253f1c5b3d96d..a7f42c6980474 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature @@ -46,21 +46,18 @@ Feature: The activity results block displays student in visible groups low score | course | C1 | | idnumber | 0001 | | name | Test assignment | - | intro | Offline text | - | section | 1 | | assignsubmission_file_enabled | 0 | | groupmode | 2 | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment | student1 | 100.00 | + | Test assignment | student2 | 90.00 | + | Test assignment | student3 | 90.00 | + | Test assignment | student4 | 80.00 | + | Test assignment | student5 | 80.00 | + | Test assignment | student6 | 70.00 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook - And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 2" for the grade item "Test assignment" - And I give the grade "90.00" to the user "Student 3" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 4" for the grade item "Test assignment" - And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" - And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" - And I press "Save changes" - And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block @@ -86,9 +83,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 3" in the "Activity results" "block" And I should see "75.00/100.00" in the "Activity results" "block" @@ -102,9 +97,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 3" in the "Activity results" "block" And I should see "75.00" in the "Activity results" "block" @@ -123,9 +116,7 @@ Feature: The activity results block displays student in visible groups low score And I should see "85%" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" And I should see "75%" in the "Activity results" "block" - And I log out - And I log in as "student5" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student5 Then I should see "Group 2" in the "Activity results" "block" And I should see "85%" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -141,9 +132,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 2" in the "Activity results" "block" And I should see "85.00/100.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -159,9 +148,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Display full names | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group 2" in the "Activity results" "block" And I should see "85.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -179,9 +166,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Display only ID numbers | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" @@ -196,9 +181,7 @@ Feature: The activity results block displays student in visible groups low score | config_nameformat | Anonymous results | | config_usegroups | Yes | And I press "Save changes" - And I log out - Then I log in as "student1" - And I am on "Course 1" course homepage + Then I am on the "Course 1" course page logged in as student1 And I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" diff --git a/blocks/myoverview/templates/view-list.mustache b/blocks/myoverview/templates/view-list.mustache index 7e89779b152dc..bfb1a539c58b1 100644 --- a/blocks/myoverview/templates/view-list.mustache +++ b/blocks/myoverview/templates/view-list.mustache @@ -45,7 +45,7 @@
- {{#str}}aria:courseimage, core_course{{/str}} + {{fullname}}
diff --git a/blocks/myoverview/templates/view-summary.mustache b/blocks/myoverview/templates/view-summary.mustache index 32ce2824256eb..50c6d2fc8eac8 100644 --- a/blocks/myoverview/templates/view-summary.mustache +++ b/blocks/myoverview/templates/view-summary.mustache @@ -45,7 +45,7 @@
- {{#str}}aria:courseimage, core_course{{/str}} + {{fullname}}
diff --git a/blocks/news_items/tests/behat/display_news.feature b/blocks/news_items/tests/behat/display_news.feature index e976ed829f8d7..806dc2de2beff 100644 --- a/blocks/news_items/tests/behat/display_news.feature +++ b/blocks/news_items/tests/behat/display_news.feature @@ -18,17 +18,12 @@ Feature: Latest announcements block displays the course latest news And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | news_items | Course | C1 | course-view-* | side-pre | - And I am on the "Course 1" Course page logged in as teacher1 - When I add a new topic to "Announcements" forum with: - | Subject | Discussion One | - | Message | Not important | - And I add a new topic to "Announcements" forum with: - | Subject | Discussion Two | - | Message | Not important | - And I add a new topic to "Announcements" forum with: - | Subject | Discussion Three | - | Message | Not important | - And I am on "Course 1" course homepage + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | teacher1 | Announcements | Discussion One | Not important | + | teacher1 | Announcements | Discussion Two | Not important | + | teacher1 | Announcements | Discussion Three | Not important | + When I am on the "Course 1" Course page logged in as teacher1 Then I should see "Discussion One" in the "Latest announcements" "block" And I should see "Discussion Two" in the "Latest announcements" "block" And I should see "Discussion Three" in the "Latest announcements" "block" diff --git a/blocks/search_forums/tests/behat/block_search_forums_course.feature b/blocks/search_forums/tests/behat/block_search_forums_course.feature index 0cc4f02b87551..00866fe4888bf 100644 --- a/blocks/search_forums/tests/behat/block_search_forums_course.feature +++ b/blocks/search_forums/tests/behat/block_search_forums_course.feature @@ -23,6 +23,9 @@ Feature: The search forums block allows users to search for forum posts on cours And I am on the "Course 1" "course editing" page logged in as teacher1 And I set the field "id_newsitems" to "1" And I press "Save and display" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | teacher1 | Announcements | My subject | My message | Scenario: Use the search forum block in a course without any forum posts Given I am on the "Course 1" course page logged in as student1 @@ -31,12 +34,7 @@ Feature: The search forums block allows users to search for forum posts on cours Then I should see "No posts" Scenario: Use the search forum block in a course with a hidden forum and search for posts - Given I add a new topic to "Announcements" forum with: - | Subject | My subject | - | Message | My message | - And I am on "Course 1" course homepage with editing mode on - And I follow "Announcements" - And I navigate to "Settings" in current page administration + Given I am on the "Announcements" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "id_visible" to "0" And I press "Save and return to course" @@ -47,10 +45,7 @@ Feature: The search forums block allows users to search for forum posts on cours Then I should see "No posts" Scenario: Use the search forum block in a course and search for posts - Given I add a new topic to "Announcements" forum with: - | Subject | My subject | - | Message | My message | - When I am on the "Course 1" course page logged in as student1 + Given I am on the "Course 1" course page logged in as student1 And "Search forums" "block" should exist And I set the field "Search" to "message" And I press "Search" diff --git a/blocks/starredcourses/classes/external.php b/blocks/starredcourses/classes/external.php index d1ace2cd0f3fd..d83b32f8d9cdb 100644 --- a/blocks/starredcourses/classes/external.php +++ b/blocks/starredcourses/classes/external.php @@ -82,14 +82,41 @@ public static function get_starred_courses($limit, $offset) { // Get the favourites, by type, for the user. $favourites = $userservice->find_favourites_by_type('core_course', 'courses', $offset, $limit); + $favouritecourseids = []; + if ($favourites) { + $favouritecourseids = array_map( + function($favourite) { + return $favourite->itemid; + }, $favourites); + } + + // Get all courses that the current user is enroled in, restricted down to favourites. + $filteredcourses = []; + if ($favouritecourseids) { + $courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, null, + COURSE_DB_QUERY_LIMIT, $favouritecourseids); + list($filteredcourses, $processedcount) = course_filter_courses_by_favourites( + $courses, + $favouritecourseids, + 0 + ); + } + // Grab the course ids. + $filteredcourseids = array_column($filteredcourses, 'id'); + + // Filter out any favourites that are not in the list of enroled courses. + $filteredfavourites = array_filter($favourites, function($favourite) use ($filteredcourseids) { + return in_array($favourite->itemid, $filteredcourseids); + }); + // Sort the favourites getting last added first. - usort($favourites, function($a, $b) { + usort($filteredfavourites, function($a, $b) { if ($a->timemodified == $b->timemodified) return 0; return ($a->timemodified > $b->timemodified) ? -1 : 1; }); $formattedcourses = array(); - foreach ($favourites as $favourite) { + foreach ($filteredfavourites as $favourite) { $course = get_course($favourite->itemid); $context = \context_course::instance($favourite->itemid); $canviewhiddencourses = has_capability('moodle/course:viewhiddencourses', $context); 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..3d650feb56488 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) { diff --git a/cache/classes/local/administration_display_helper.php b/cache/classes/local/administration_display_helper.php index 22d8e79f4a79f..cadf8e028c04a 100644 --- a/cache/classes/local/administration_display_helper.php +++ b/cache/classes/local/administration_display_helper.php @@ -30,8 +30,8 @@ namespace core_cache\local; -defined('MOODLE_INTERNAL') || die(); use cache_store, cache_factory, cache_config_writer, cache_helper; +use core\output\notification; /** * A cache helper for administration tasks @@ -505,9 +505,8 @@ public function action_editstore(): array { * Performs the deletestore action. * * @param string $action the action calling to this function. - * @return void */ - public function action_deletestore(string $action) { + public function action_deletestore(string $action): void { global $OUTPUT, $PAGE, $SITE; $notifysuccess = true; $storeinstancesummaries = $this->get_store_instance_summaries(); @@ -517,10 +516,10 @@ public function action_deletestore(string $action) { if (!array_key_exists($store, $storeinstancesummaries)) { $notifysuccess = false; - $notifications[] = array(get_string('invalidstore', 'cache'), false); + $notification = get_string('invalidstore', 'cache'); } else if ($storeinstancesummaries[$store]['mappings'] > 0) { $notifysuccess = false; - $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false); + $notification = get_string('deletestorehasmappings', 'cache'); } if ($notifysuccess) { @@ -544,6 +543,8 @@ public function action_deletestore(string $action) { $writer->delete_store_instance($store); redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5); } + } else { + redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR); } } @@ -731,9 +732,8 @@ public function action_newlockinstance(): array { * Performs the delete lock action. * * @param string $action the action calling this function. - * @return void */ - public function action_deletelock(string $action) { + public function action_deletelock(string $action): void { global $OUTPUT, $PAGE, $SITE; $notifysuccess = true; $locks = $this->get_lock_summaries(); @@ -742,10 +742,10 @@ public function action_deletelock(string $action) { $confirm = optional_param('confirm', false, PARAM_BOOL); if (!array_key_exists($lock, $locks)) { $notifysuccess = false; - $notifications[] = array(get_string('invalidlock', 'cache'), false); + $notification = get_string('invalidlock', 'cache'); } else if ($locks[$lock]['uses'] > 0) { $notifysuccess = false; - $notifications[] = array(get_string('deletelockhasuses', 'cache'), false); + $notification = get_string('deletelockhasuses', 'cache'); } if ($notifysuccess) { if (!$confirm) { @@ -768,6 +768,8 @@ public function action_deletelock(string $action) { $writer->delete_lock_instance($lock); redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5); } + } else { + redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR); } } diff --git a/calendar/classes/local/api.php b/calendar/classes/local/api.php index e59f34e95ec6c..a29a429b099c1 100644 --- a/calendar/classes/local/api.php +++ b/calendar/classes/local/api.php @@ -325,6 +325,12 @@ public static function update_event_start_day( 'core_calendar_event_timestart_updated', [$legacyevent, $moduleinstance] ); + + // Rebuild the course cache to make sure the updated dates are reflected. + $courseid = $event->get_course()->get('id'); + $cmid = $event->get_course_module()->get('id'); + \course_modinfo::purge_course_module_cache($courseid, $cmid); + rebuild_course_cache($courseid, true, true); } return $mapper->from_legacy_event_to_event($legacyevent); 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/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/completion/tests/behat/activity_completion_criteria.feature b/completion/tests/behat/activity_completion_criteria.feature index beb6ce8eaf858..50084de7c0be4 100644 --- a/completion/tests/behat/activity_completion_criteria.feature +++ b/completion/tests/behat/activity_completion_criteria.feature @@ -22,7 +22,6 @@ Feature: Allow to mark course as completed without cron for activity completion | course | CC1 | | name | Test assignment name | | idnumber | assign1 | - | description | Test assignment description | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | CC1 | course-view-* | side-pre | @@ -87,7 +86,6 @@ Feature: Allow to mark course as completed without cron for activity completion And I click on "Grade" "link" in the "student1@example.com" "table_row" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" - And I am on "Completion course" course homepage And I am on the "Completion course" course page logged in as student1 And I should see "Status: In progress" And I am on the "Test assignment name2" "assign activity" page logged in as teacher1 @@ -95,14 +93,12 @@ Feature: Allow to mark course as completed without cron for activity completion And I click on "Grade" "link" in the "student1@example.com" "table_row" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" - And I am on "Completion course" course homepage When I am on the "Completion course" course page logged in as student1 Then I should see "Status: Complete" @javascript Scenario: Course completion should not be updated when teacher grades assignment on course grader report page - Given I am on the "Completion course" course page logged in as teacher1 - And I navigate to "View > Grader report" in the course gradebook + Given I am on the "Completion course" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "57" to the user "Student First" for the grade item "Test assignment name" And I press "Save changes" @@ -116,8 +112,7 @@ Feature: Allow to mark course as completed without cron for activity completion @javascript Scenario: Course completion should not be updated when teacher grades assignment on activity grader report page - Given I am on the "Completion course" course page logged in as teacher1 - And I navigate to "View > Single view" in the course gradebook + Given I am on the "Completion course" "grades > Single View > View" page logged in as "teacher1" And I click on "Users" "link" in the ".page-toggler" "css_element" And I turn editing mode on And I click on "Student First" in the "user" search widget diff --git a/completion/tests/behat/enable_completion_on_pass_grade.feature b/completion/tests/behat/enable_completion_on_pass_grade.feature index 10087f72ec140..cf9846663c48e 100644 --- a/completion/tests/behat/enable_completion_on_pass_grade.feature +++ b/completion/tests/behat/enable_completion_on_pass_grade.feature @@ -27,26 +27,18 @@ Feature: Students will be marked as completed if they have achieved a passing gr | completionpassgrade | 1 | | completionusegrade | 1 | | gradepass | 50 | - And I log in as "teacher1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity - And I log out Scenario: Passing grade completion - Given I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + Given I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" And I press "Save changes" - And I log out - When I log in as "student1" - And I am on "Course 1" course homepage - And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" + When I am on the "Course 1" course page logged in as student1 + Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" - And I log out - And I log in as "student2" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" diff --git a/completion/tests/behat/enable_completion_on_view_and_grade.feature b/completion/tests/behat/enable_completion_on_view_and_grade.feature index f762c49dd915b..69e06a3f8c0a2 100644 --- a/completion/tests/behat/enable_completion_on_view_and_grade.feature +++ b/completion/tests/behat/enable_completion_on_view_and_grade.feature @@ -23,7 +23,6 @@ Feature: Students will be marked as completed and pass/fail | course | C1 | | idnumber | a1 | | name | Test assignment name | - | intro | Submit your online text | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | @@ -31,61 +30,43 @@ Feature: Students will be marked as completed and pass/fail | completionusegrade | 1 | | gradepass | 50 | | completionpassgrade | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity - And I log out And I am on the "Test assignment name" "assign activity" page logged in as student2 - And I log out And I am on the "Test assignment name" "assign activity" page logged in as student1 - And I log out Scenario: Confirm completion (incomplete/pass/fail) are set correctly - Given I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook - And I turn editing mode on - And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" - And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" - And I give the grade "30" to the user "Student Third" for the grade item "Test assignment name" - And I press "Save changes" - And I log out - When I log in as "student1" - And I am on "Course 1" course homepage - And the "View" completion condition of "Test assignment name" is displayed as "done" + Given the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment name | student1 | 21.00 | + | Test assignment name | student2 | 50.00 | + | Test assignment name | student3 | 30.00 | + When I am on "Course 1" course homepage + Then the "View" completion condition of "Test assignment name" is displayed as "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" - And I log out - And I log in as "student2" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student2 And the "View" completion condition of "Test assignment name" is displayed as "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" - And I log out - And I log in as "student3" - And I am on "Course 1" course homepage + And I am on the "Course 1" course page logged in as student3 And the "View" completion condition of "Test assignment name" is displayed as "todo" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" @javascript Scenario: Keep current view completion condition when the teacher does the action 'Unlock completion settings'. - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "View > Grader report" in the course gradebook - And I turn editing mode on - And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" - And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" - And I press "Save changes" - And I am on the "Test assignment name" "assign activity" page logged in as teacher1 - And I navigate to "Settings" in current page administration + Given the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment name | student1 | 21.00 | + | Test assignment name | student2 | 50.00 | + And I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I expand all fieldsets And I press "Unlock completion settings" And I expand all fieldsets And I should see "Completion options unlocked" And I click on "Save and display" "button" - And I log out When I am on the "Course 1" course page logged in as student1 Then the "View" completion condition of "Test assignment name" is displayed as "done" - And I log out - When I am on the "Course 1" course page logged in as student2 - Then the "View" completion condition of "Test assignment name" is displayed as "done" + And I am on the "Course 1" course page logged in as student2 + And the "View" completion condition of "Test assignment name" is displayed as "done" diff --git a/completion/tests/behat/passgrade_completion_criteria_gradeitem_visibility.feature b/completion/tests/behat/passgrade_completion_criteria_gradeitem_visibility.feature index 0e948611732b5..6fa91a783268b 100644 --- a/completion/tests/behat/passgrade_completion_criteria_gradeitem_visibility.feature +++ b/completion/tests/behat/passgrade_completion_criteria_gradeitem_visibility.feature @@ -22,7 +22,6 @@ Feature: Students will be shown relevant completion state based on grade item vi | activity | assign | | course | C1 | | name | Test assignment name | - | intro | Submit your online text | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | @@ -31,34 +30,27 @@ Feature: Students will be shown relevant completion state based on grade item vi | gradepass | 50 | And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity - And I log out And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo" - And I log out Scenario: Passing grade and receive a grade completions for visible grade item (passgrade completion enabled) - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "View > Grader report" in the course gradebook - And I turn editing mode on - And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" - And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" - And I press "Save changes" - And I am on "Course 1" course homepage + Given the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment name | student1 | 21.00 | + | Test assignment name | student2 | 50.00 | + And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity - And I log out When I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" - And I log out And I am on the "Course 1" course page logged in as student2 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Passing grade and receive a grade completions for hidden grade item (passgrade completion enabled) - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "Setup > Gradebook setup" in the course gradebook + Given I am on the "Course 1" "grades > gradebook setup" page logged in as "teacher1" And I hide the grade item "Test assignment name" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on @@ -68,28 +60,22 @@ Feature: Students will be shown relevant completion state based on grade item vi And I am on "Course 1" course homepage And "Student First" user has not completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity - And I log out And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo" - And I log out And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Receive a grade completion for visible grade item (passgrade completion disabled) - Given I am on the "Test assignment name" Activity page logged in as teacher1 - When I navigate to "Settings" in current page administration + Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | completionpassgrade | 0 | And I press "Save and display" - And I log out And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And I should not see "Receive a passing grade" - And I log out - And I am on the "Course 1" course page logged in as teacher1 - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" @@ -97,28 +83,22 @@ Feature: Students will be shown relevant completion state based on grade item vi And I am on "Course 1" course homepage And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity - And I log out When I am on the "Course 1" course page logged in as student1 # Once MDL-75582 is fixed "failed" should be changed to "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "failed" And I should not see "Receive a passing grade" - And I log out And I am on the "Course 1" course page logged in as student2 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Receive a grade completion for hidden grade item (passgrade completion disabled) - Given I am on the "Test assignment name" Activity page logged in as teacher1 - When I navigate to "Settings" in current page administration + Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | completionpassgrade | 0 | And I press "Save and display" - And I log out And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And I should not see "Receive a passing grade" - And I log out - And I am on the "Course 1" course page logged in as teacher1 - And I navigate to "Setup > Gradebook setup" in the course gradebook + And I am on the "Course 1" "grades > gradebook setup" page logged in as "teacher1" And I hide the grade item "Test assignment name" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on @@ -128,10 +108,8 @@ Feature: Students will be shown relevant completion state based on grade item vi And I am on "Course 1" course homepage And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity - And I log out When I am on the "Course 1" course page logged in as student1 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And I should not see "Receive a passing grade" - And I log out And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" diff --git a/composer.json b/composer.json index 071dfe30dc5bd..9b69ce18be776 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,9 @@ "behat/mink": "^1.10.0", "friends-of-behat/mink-extension": "^2.7.2", "behat/mink-goutte-driver": "^2.0", - "symfony/process": "^4.4 || ^5.0", - "behat/behat": "3.12.*", - "oleg-andreyev/mink-phpwebdriver": "^1.2.1" + "symfony/process": "^4.4 || ^5.0 || ^6.0", + "behat/behat": "3.13.*", + "oleg-andreyev/mink-phpwebdriver": "1.2.*" }, "autoload-dev": { "psr-0": { diff --git a/composer.lock b/composer.lock index 7192796e2f780..fd0452661a143 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "71522fc9834426e64d51273f01714eaa", + "content-hash": "00ebd18e25df51908b15e70c2b2e9098", "packages": [], "packages-dev": [ { "name": "behat/behat", - "version": "v3.12.0", + "version": "v3.13.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "2f059c9172764ba1f1759b3679aca499b665330a" + "reference": "9dd7cdb309e464ddeab095cd1a5151c2dccba4ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/2f059c9172764ba1f1759b3679aca499b665330a", - "reference": "2f059c9172764ba1f1759b3679aca499b665330a", + "url": "https://api.github.com/repos/Behat/Behat/zipball/9dd7cdb309e464ddeab095cd1a5151c2dccba4ab", + "reference": "9dd7cdb309e464ddeab095cd1a5151c2dccba4ab", "shasum": "" }, "require": { @@ -90,9 +90,9 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.12.0" + "source": "https://github.com/Behat/Behat/tree/v3.13.0" }, - "time": "2022-11-29T15:30:11+00:00" + "time": "2023-04-18T15:40:53+00:00" }, { "name": "behat/gherkin", @@ -343,6 +343,7 @@ "issues": "https://github.com/minkphp/MinkGoutteDriver/issues", "source": "https://github.com/minkphp/MinkGoutteDriver/tree/v2.0.0" }, + "abandoned": "behat/mink-browserkit-driver", "time": "2021-12-29T10:56:50+00:00" }, { @@ -466,28 +467,29 @@ }, { "name": "fabpot/goutte", - "version": "v4.0.2", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/Goutte.git", - "reference": "f51940fbe0db060bc4fc0b3f1d19bc4ff3054b17" + "reference": "e3f28671c87a48a0f13ada1baea0d95acc2138c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/f51940fbe0db060bc4fc0b3f1d19bc4ff3054b17", - "reference": "f51940fbe0db060bc4fc0b3f1d19bc4ff3054b17", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/e3f28671c87a48a0f13ada1baea0d95acc2138c3", + "reference": "e3f28671c87a48a0f13ada1baea0d95acc2138c3", "shasum": "" }, "require": { "php": ">=7.1.3", "symfony/browser-kit": "^4.4|^5.0|^6.0", "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/dom-crawler": "^4.4|^5.0|^6.0", "symfony/http-client": "^4.4|^5.0|^6.0", "symfony/mime": "^4.4|^5.0|^6.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.0|^6.0" + "symfony/phpunit-bridge": "^6.0" }, "type": "application", "autoload": { @@ -515,9 +517,10 @@ ], "support": { "issues": "https://github.com/FriendsOfPHP/Goutte/issues", - "source": "https://github.com/FriendsOfPHP/Goutte/tree/v4.0.2" + "source": "https://github.com/FriendsOfPHP/Goutte/tree/v4.0.3" }, - "time": "2021-12-17T17:15:01+00:00" + "abandoned": "symfony/browser-kit", + "time": "2023-04-01T09:05:33+00:00" }, { "name": "friends-of-behat/mink-extension", @@ -637,16 +640,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -684,7 +687,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -692,20 +695,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.2", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -746,9 +749,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "oleg-andreyev/mink-phpwebdriver", @@ -928,37 +931,38 @@ }, { "name": "php-webdriver/webdriver", - "version": "1.13.1", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c" + "reference": "3ea4f924afb43056bf9c630509e657d951608563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/6dfe5f814b796c1b5748850aa19f781b9274c36c", - "reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563", + "reference": "3ea4f924afb43056bf9c630509e657d951608563", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "ext-zip": "*", - "php": "^5.6 || ~7.0 || ^8.0", + "php": "^7.3 || ^8.0", "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0 || ^6.0" + "symfony/process": "^5.0 || ^6.0" }, "replace": { "facebook/webdriver": "*" }, "require-dev": { - "ondram/ci-detector": "^2.1 || ^3.5 || ^4.0", + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^1.1 || ^2.0", + "php-mock/php-mock-phpunit": "^2.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9", + "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0 || ^6.0" + "symfony/var-dumper": "^5.0 || ^6.0" }, "suggest": { "ext-SimpleXML": "For Firefox profile creation" @@ -987,29 +991,29 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.13.1" + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0" }, - "time": "2022-10-11T11:49:44+00:00" + "time": "2023-02-09T12:12:19+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.23", + "version": "9.2.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1024,8 +1028,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -1058,7 +1062,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" }, "funding": [ { @@ -1066,7 +1071,7 @@ "type": "github" } ], - "time": "2022-12-28T12:41:10+00:00" + "time": "2023-07-26T13:44:30+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1311,20 +1316,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.27", + "version": "9.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38" + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1393,7 +1398,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" }, "funding": [ { @@ -1409,7 +1414,7 @@ "type": "tidelift" } ], - "time": "2022-12-09T07:31:23+00:00" + "time": "2023-01-14T12:32:24+00:00" }, { "name": "psr/container", @@ -1859,16 +1864,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -1913,7 +1918,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -1921,20 +1926,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -1976,7 +1981,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -1984,7 +1989,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -2065,16 +2070,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -2117,7 +2122,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -2125,7 +2130,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -2298,16 +2303,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -2346,10 +2351,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2357,7 +2362,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2416,16 +2421,16 @@ }, { "name": "sebastian/type", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -2460,7 +2465,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2468,7 +2473,7 @@ "type": "github" } ], - "time": "2022-09-12T14:47:03+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -2525,16 +2530,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.4.11", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027" + "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/081fe28a26b6bd671dea85ef3a4b5003f3c88027", - "reference": "081fe28a26b6bd671dea85ef3a4b5003f3c88027", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a866ca7e396f15d7efb6d74a8a7d364d4e05b704", + "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704", "shasum": "" }, "require": { @@ -2577,7 +2582,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.11" + "source": "https://github.com/symfony/browser-kit/tree/v5.4.21" }, "funding": [ { @@ -2593,20 +2598,20 @@ "type": "tidelift" } ], - "time": "2022-07-27T15:50:05+00:00" + "time": "2023-02-14T08:03:56+00:00" }, { "name": "symfony/config", - "version": "v5.4.11", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "ec79e03125c1d2477e43dde8528535d90cc78379" + "reference": "8109892f27beed9252bd1f1c1880aeb4ad842650" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/ec79e03125c1d2477e43dde8528535d90cc78379", - "reference": "ec79e03125c1d2477e43dde8528535d90cc78379", + "url": "https://api.github.com/repos/symfony/config/zipball/8109892f27beed9252bd1f1c1880aeb4ad842650", + "reference": "8109892f27beed9252bd1f1c1880aeb4ad842650", "shasum": "" }, "require": { @@ -2656,7 +2661,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.4.11" + "source": "https://github.com/symfony/config/tree/v5.4.26" }, "funding": [ { @@ -2672,20 +2677,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2023-07-19T20:21:11+00:00" }, { "name": "symfony/console", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f" + "reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", + "url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273", + "reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273", "shasum": "" }, "require": { @@ -2750,12 +2755,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.17" + "source": "https://github.com/symfony/console/tree/v5.4.26" }, "funding": [ { @@ -2771,20 +2776,20 @@ "type": "tidelift" } ], - "time": "2022-12-28T14:15:31+00:00" + "time": "2023-07-19T20:11:33+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4" + "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/052ef49b660f9ad2a3adb311c555c9bc11ba61f4", - "reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", + "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", "shasum": "" }, "require": { @@ -2821,7 +2826,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.17" + "source": "https://github.com/symfony/css-selector/tree/v5.4.26" }, "funding": [ { @@ -2837,20 +2842,20 @@ "type": "tidelift" } ], - "time": "2022-12-23T11:40:44+00:00" + "time": "2023-07-07T06:10:25+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "58f2988128d2d278280781db037677a32cf720db" + "reference": "6736a10dcf724725a3b1c3b53e63a9ee03b27db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/58f2988128d2d278280781db037677a32cf720db", - "reference": "58f2988128d2d278280781db037677a32cf720db", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6736a10dcf724725a3b1c3b53e63a9ee03b27db9", + "reference": "6736a10dcf724725a3b1c3b53e63a9ee03b27db9", "shasum": "" }, "require": { @@ -2910,7 +2915,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.17" + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.26" }, "funding": [ { @@ -2926,7 +2931,7 @@ "type": "tidelift" } ], - "time": "2022-12-28T13:55:51+00:00" + "time": "2023-07-19T20:11:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2997,16 +3002,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.17", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "32a07d910edc138a1dd5508c17c6b9bc1eb27a1b" + "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/32a07d910edc138a1dd5508c17c6b9bc1eb27a1b", - "reference": "32a07d910edc138a1dd5508c17c6b9bc1eb27a1b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d2aefa5a7acc5511422792931d14d1be96fe9fea", + "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea", "shasum": "" }, "require": { @@ -3052,7 +3057,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.17" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.25" }, "funding": [ { @@ -3068,20 +3073,20 @@ "type": "tidelift" } ], - "time": "2022-12-22T10:31:03+00:00" + "time": "2023-06-05T08:05:41+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9" + "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", - "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac", + "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac", "shasum": "" }, "require": { @@ -3137,7 +3142,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.17" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26" }, "funding": [ { @@ -3153,7 +3158,7 @@ "type": "tidelift" } ], - "time": "2022-12-12T15:54:21+00:00" + "time": "2023-07-06T06:34:20+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3236,16 +3241,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.13", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51" + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", "shasum": "" }, "require": { @@ -3280,7 +3285,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.13" + "source": "https://github.com/symfony/filesystem/tree/v5.4.25" }, "funding": [ { @@ -3296,20 +3301,20 @@ "type": "tidelift" } ], - "time": "2022-09-21T19:53:16+00:00" + "time": "2023-05-31T13:04:02+00:00" }, { "name": "symfony/http-client", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "772129f800fc0bfaa6bd40c40934d544f0957d30" + "reference": "19d48ef7f38e5057ed1789a503cd3eccef039bce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/772129f800fc0bfaa6bd40c40934d544f0957d30", - "reference": "772129f800fc0bfaa6bd40c40934d544f0957d30", + "url": "https://api.github.com/repos/symfony/http-client/zipball/19d48ef7f38e5057ed1789a503cd3eccef039bce", + "reference": "19d48ef7f38e5057ed1789a503cd3eccef039bce", "shasum": "" }, "require": { @@ -3335,6 +3340,7 @@ "guzzlehttp/promises": "^1.4", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", + "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", "symfony/http-kernel": "^4.4.13|^5.1.5|^6.0", @@ -3366,8 +3372,11 @@ ], "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", + "keywords": [ + "http" + ], "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.17" + "source": "https://github.com/symfony/http-client/tree/v5.4.26" }, "funding": [ { @@ -3383,7 +3392,7 @@ "type": "tidelift" } ], - "time": "2022-12-13T11:07:37+00:00" + "time": "2023-07-03T12:14:50+00:00" }, { "name": "symfony/http-client-contracts", @@ -3465,16 +3474,16 @@ }, { "name": "symfony/mime", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2a83d82efc91c3f03a23c8b47a896df168aa5c63" + "reference": "2ea06dfeee20000a319d8407cea1d47533d5a9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2a83d82efc91c3f03a23c8b47a896df168aa5c63", - "reference": "2a83d82efc91c3f03a23c8b47a896df168aa5c63", + "url": "https://api.github.com/repos/symfony/mime/zipball/2ea06dfeee20000a319d8407cea1d47533d5a9d2", + "reference": "2ea06dfeee20000a319d8407cea1d47533d5a9d2", "shasum": "" }, "require": { @@ -3489,15 +3498,15 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<4.4", - "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + "symfony/serializer": "<5.4.26|>=6,<6.2.13|>=6.3,<6.3.2" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1", + "egulias/email-validator": "^2.1.10|^3.1|^4", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", "symfony/property-access": "^4.4|^5.1|^6.0", "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + "symfony/serializer": "^5.4.26|~6.2.13|^6.3.2" }, "type": "library", "autoload": { @@ -3529,7 +3538,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.17" + "source": "https://github.com/symfony/mime/tree/v5.4.26" }, "funding": [ { @@ -3545,7 +3554,7 @@ "type": "tidelift" } ], - "time": "2022-12-13T09:59:55+00:00" + "time": "2023-07-27T06:29:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4283,16 +4292,16 @@ }, { "name": "symfony/process", - "version": "v5.4.11", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" + "reference": "1a44dc377ec86a50fab40d066cd061e28a6b482f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", + "url": "https://api.github.com/repos/symfony/process/zipball/1a44dc377ec86a50fab40d066cd061e28a6b482f", + "reference": "1a44dc377ec86a50fab40d066cd061e28a6b482f", "shasum": "" }, "require": { @@ -4325,7 +4334,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.11" + "source": "https://github.com/symfony/process/tree/v5.4.26" }, "funding": [ { @@ -4341,7 +4350,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2023-07-12T15:44:31+00:00" }, { "name": "symfony/service-contracts", @@ -4428,16 +4437,16 @@ }, { "name": "symfony/string", - "version": "v5.4.17", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b" + "reference": "1181fe9270e373537475e826873b5867b863883c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/55733a8664b8853b003e70251c58bc8cb2d82a6b", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b", + "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", + "reference": "1181fe9270e373537475e826873b5867b863883c", "shasum": "" }, "require": { @@ -4494,7 +4503,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.17" + "source": "https://github.com/symfony/string/tree/v5.4.26" }, "funding": [ { @@ -4510,20 +4519,20 @@ "type": "tidelift" } ], - "time": "2022-12-12T15:54:21+00:00" + "time": "2023-06-28T12:46:07+00:00" }, { "name": "symfony/translation", - "version": "v5.4.14", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f0ed07675863aa6e3939df8b1bc879450b585cab" + "reference": "de237e59c5833422342be67402d487fbf50334ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f0ed07675863aa6e3939df8b1bc879450b585cab", - "reference": "f0ed07675863aa6e3939df8b1bc879450b585cab", + "url": "https://api.github.com/repos/symfony/translation/zipball/de237e59c5833422342be67402d487fbf50334ff", + "reference": "de237e59c5833422342be67402d487fbf50334ff", "shasum": "" }, "require": { @@ -4591,7 +4600,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.14" + "source": "https://github.com/symfony/translation/tree/v5.4.24" }, "funding": [ { @@ -4607,7 +4616,7 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:01:20+00:00" + "time": "2023-05-19T12:34:17+00:00" }, { "name": "symfony/translation-contracts", @@ -4689,16 +4698,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.17", + "version": "v5.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "edcdc11498108f8967fe95118a7ec8624b94760e" + "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/edcdc11498108f8967fe95118a7ec8624b94760e", - "reference": "edcdc11498108f8967fe95118a7ec8624b94760e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4cd2e3ea301aadd76a4172756296fe552fb45b0b", + "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b", "shasum": "" }, "require": { @@ -4744,7 +4753,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.17" + "source": "https://github.com/symfony/yaml/tree/v5.4.23" }, "funding": [ { @@ -4760,7 +4769,7 @@ "type": "tidelift" } ], - "time": "2022-12-13T09:57:04+00:00" + "time": "2023-04-23T19:33:36+00:00" }, { "name": "theseer/tokenizer", diff --git a/course/amd/build/manual_completion_toggle.min.js b/course/amd/build/manual_completion_toggle.min.js index 97ae32ada059b..18c7cdd69a97b 100644 --- a/course/amd/build/manual_completion_toggle.min.js +++ b/course/amd/build/manual_completion_toggle.min.js @@ -1,4 +1,4 @@ -define("core_course/manual_completion_toggle",["exports","core/templates","core/notification","core_course/repository","core_course/events"],(function(_exports,_templates,_notification,_repository,CourseEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_course/manual_completion_toggle",["exports","core/templates","core/notification","core_course/repository","core_course/events","core/pending"],(function(_exports,_templates,_notification,_repository,CourseEvents,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Provides the functionality for toggling the manual completion state of a course module through * the manual completion button. @@ -6,6 +6,6 @@ define("core_course/manual_completion_toggle",["exports","core/templates","core/ * @module core_course/manual_completion_toggle * @copyright 2021 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE,loadingHtml=await _templates.default.render("core/loading",{});await _templates.default.replaceNodeContents(toggleButton,loadingHtml,"");try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const pendingPromise=new _pending.default("core_course:toggleManualCompletionState"),originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE;_templates.default.renderForPromise("core/loading",{}).then((loadingHtml=>{_templates.default.replaceNodeContents(toggleButton,loadingHtml,"")})).catch((()=>{}));try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}pendingPromise.resolve()}})); //# sourceMappingURL=manual_completion_toggle.min.js.map \ No newline at end of file diff --git a/course/amd/build/manual_completion_toggle.min.js.map b/course/amd/build/manual_completion_toggle.min.js.map index 4ddc12428ccf4..7b22322106c0c 100644 --- a/course/amd/build/manual_completion_toggle.min.js.map +++ b/course/amd/build/manual_completion_toggle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"manual_completion_toggle.min.js","sources":["../src/manual_completion_toggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Provides the functionality for toggling the manual completion state of a course module through\n * the manual completion button.\n *\n * @module core_course/manual_completion_toggle\n * @copyright 2021 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport {toggleManualCompletion} from 'core_course/repository';\nimport * as CourseEvents from 'core_course/events';\n\n/**\n * Selectors in the manual completion template.\n *\n * @type {{MANUAL_TOGGLE: string}}\n */\nconst SELECTORS = {\n MANUAL_TOGGLE: 'button[data-action=toggle-manual-completion]',\n};\n\n/**\n * Toggle type values for the data-toggletype attribute in the core_course/completion_manual template.\n *\n * @type {{TOGGLE_UNDO: string, TOGGLE_MARK_DONE: string}}\n */\nconst TOGGLE_TYPES = {\n TOGGLE_MARK_DONE: 'manual:mark-done',\n TOGGLE_UNDO: 'manual:undo',\n};\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n/**\n * Registers the click event listener for the manual completion toggle button.\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n document.addEventListener('click', (e) => {\n const toggleButton = e.target.closest(SELECTORS.MANUAL_TOGGLE);\n if (toggleButton) {\n e.preventDefault();\n toggleManualCompletionState(toggleButton).catch(Notification.exception);\n }\n });\n registered = true;\n};\n\n/**\n * Toggles the manual completion state of the module for the given user.\n *\n * @param {HTMLElement} toggleButton\n * @returns {Promise}\n */\nconst toggleManualCompletionState = async(toggleButton) => {\n // Make a copy of the original content of the button.\n const originalInnerHtml = toggleButton.innerHTML;\n\n // Disable the button to prevent double clicks.\n toggleButton.setAttribute('disabled', 'disabled');\n\n // Get button data.\n const toggleType = toggleButton.getAttribute('data-toggletype');\n const cmid = toggleButton.getAttribute('data-cmid');\n const activityname = toggleButton.getAttribute('data-activityname');\n // Get the target completion state.\n const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE;\n\n // Replace the button contents with the loading icon.\n const loadingHtml = await Templates.render('core/loading', {});\n await Templates.replaceNodeContents(toggleButton, loadingHtml, '');\n\n try {\n // Call the webservice to update the manual completion status.\n await toggleManualCompletion(cmid, completed);\n\n // All good so far. Refresh the manual completion button to reflect its new state by re-rendering the template.\n const templateContext = {\n cmid: cmid,\n activityname: activityname,\n overallcomplete: completed,\n overallincomplete: !completed,\n istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.\n };\n const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);\n\n // Replace the toggle button with the newly loaded template.\n const replacedNode = await Templates.replaceNode(toggleButton, renderObject.html, renderObject.js);\n const newToggleButton = replacedNode.pop();\n\n // Build manualCompletionToggled custom event.\n const withAvailability = toggleButton.getAttribute('data-withavailability');\n const toggledEvent = new CustomEvent(CourseEvents.manualCompletionToggled, {\n bubbles: true,\n detail: {\n cmid,\n activityname,\n completed,\n withAvailability,\n }\n });\n // Dispatch the manualCompletionToggled custom event.\n newToggleButton.dispatchEvent(toggledEvent);\n\n } catch (exception) {\n // In case of an error, revert the original state and appearance of the button.\n toggleButton.removeAttribute('disabled');\n toggleButton.innerHTML = originalInnerHtml;\n\n // Show the exception.\n Notification.exception(exception);\n }\n};\n"],"names":["SELECTORS","TOGGLE_TYPES","registered","document","addEventListener","e","toggleButton","target","closest","preventDefault","toggleManualCompletionState","catch","Notification","exception","async","originalInnerHtml","innerHTML","setAttribute","toggleType","getAttribute","cmid","activityname","completed","loadingHtml","Templates","render","replaceNodeContents","templateContext","overallcomplete","overallincomplete","istrackeduser","renderObject","renderForPromise","newToggleButton","replaceNode","html","js","pop","withAvailability","toggledEvent","CustomEvent","CourseEvents","manualCompletionToggled","bubbles","detail","dispatchEvent","removeAttribute"],"mappings":";;;;;;;;k2BAkCMA,wBACa,+CAQbC,8BACgB,uBASlBC,YAAa,gBAKG,KACZA,aAGJC,SAASC,iBAAiB,SAAUC,UAC1BC,aAAeD,EAAEE,OAAOC,QAAQR,yBAClCM,eACAD,EAAEI,iBACFC,4BAA4BJ,cAAcK,MAAMC,sBAAaC,eAGrEX,YAAa,UASXQ,4BAA8BI,MAAAA,qBAE1BC,kBAAoBT,aAAaU,UAGvCV,aAAaW,aAAa,WAAY,kBAGhCC,WAAaZ,aAAaa,aAAa,mBACvCC,KAAOd,aAAaa,aAAa,aACjCE,aAAef,aAAaa,aAAa,qBAEzCG,UAAYJ,aAAejB,8BAG3BsB,kBAAoBC,mBAAUC,OAAO,eAAgB,UACrDD,mBAAUE,oBAAoBpB,aAAciB,YAAa,cAIrD,sCAAuBH,KAAME,iBAG7BK,gBAAkB,CACpBP,KAAMA,KACNC,aAAcA,aACdO,gBAAiBN,UACjBO,mBAAoBP,UACpBQ,eAAe,GAEbC,mBAAqBP,mBAAUQ,iBAAiB,gCAAiCL,iBAIjFM,uBADqBT,mBAAUU,YAAY5B,aAAcyB,aAAaI,KAAMJ,aAAaK,KAC1DC,MAG/BC,iBAAmBhC,aAAaa,aAAa,yBAC7CoB,aAAe,IAAIC,YAAYC,aAAaC,wBAAyB,CACvEC,SAAS,EACTC,OAAQ,CACJxB,KAAAA,KACAC,aAAAA,aACAC,UAAAA,UACAgB,iBAAAA,oBAIRL,gBAAgBY,cAAcN,cAEhC,MAAO1B,WAELP,aAAawC,gBAAgB,YAC7BxC,aAAaU,UAAYD,wCAGZF,UAAUA"} \ No newline at end of file +{"version":3,"file":"manual_completion_toggle.min.js","sources":["../src/manual_completion_toggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Provides the functionality for toggling the manual completion state of a course module through\n * the manual completion button.\n *\n * @module core_course/manual_completion_toggle\n * @copyright 2021 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport {toggleManualCompletion} from 'core_course/repository';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\n\n/**\n * Selectors in the manual completion template.\n *\n * @type {{MANUAL_TOGGLE: string}}\n */\nconst SELECTORS = {\n MANUAL_TOGGLE: 'button[data-action=toggle-manual-completion]',\n};\n\n/**\n * Toggle type values for the data-toggletype attribute in the core_course/completion_manual template.\n *\n * @type {{TOGGLE_UNDO: string, TOGGLE_MARK_DONE: string}}\n */\nconst TOGGLE_TYPES = {\n TOGGLE_MARK_DONE: 'manual:mark-done',\n TOGGLE_UNDO: 'manual:undo',\n};\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n/**\n * Registers the click event listener for the manual completion toggle button.\n */\nexport const init = () => {\n if (registered) {\n return;\n }\n document.addEventListener('click', (e) => {\n const toggleButton = e.target.closest(SELECTORS.MANUAL_TOGGLE);\n if (toggleButton) {\n e.preventDefault();\n toggleManualCompletionState(toggleButton).catch(Notification.exception);\n }\n });\n registered = true;\n};\n\n/**\n * Toggles the manual completion state of the module for the given user.\n *\n * @param {HTMLElement} toggleButton\n * @returns {Promise}\n */\nconst toggleManualCompletionState = async(toggleButton) => {\n const pendingPromise = new Pending('core_course:toggleManualCompletionState');\n // Make a copy of the original content of the button.\n const originalInnerHtml = toggleButton.innerHTML;\n\n // Disable the button to prevent double clicks.\n toggleButton.setAttribute('disabled', 'disabled');\n\n // Get button data.\n const toggleType = toggleButton.getAttribute('data-toggletype');\n const cmid = toggleButton.getAttribute('data-cmid');\n const activityname = toggleButton.getAttribute('data-activityname');\n // Get the target completion state.\n const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE;\n\n // Replace the button contents with the loading icon.\n Templates.renderForPromise('core/loading', {})\n .then((loadingHtml) => {\n Templates.replaceNodeContents(toggleButton, loadingHtml, '');\n return;\n }).catch(() => {});\n\n try {\n // Call the webservice to update the manual completion status.\n await toggleManualCompletion(cmid, completed);\n\n // All good so far. Refresh the manual completion button to reflect its new state by re-rendering the template.\n const templateContext = {\n cmid: cmid,\n activityname: activityname,\n overallcomplete: completed,\n overallincomplete: !completed,\n istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.\n };\n const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);\n\n // Replace the toggle button with the newly loaded template.\n const replacedNode = await Templates.replaceNode(toggleButton, renderObject.html, renderObject.js);\n const newToggleButton = replacedNode.pop();\n\n // Build manualCompletionToggled custom event.\n const withAvailability = toggleButton.getAttribute('data-withavailability');\n const toggledEvent = new CustomEvent(CourseEvents.manualCompletionToggled, {\n bubbles: true,\n detail: {\n cmid,\n activityname,\n completed,\n withAvailability,\n }\n });\n // Dispatch the manualCompletionToggled custom event.\n newToggleButton.dispatchEvent(toggledEvent);\n\n } catch (exception) {\n // In case of an error, revert the original state and appearance of the button.\n toggleButton.removeAttribute('disabled');\n toggleButton.innerHTML = originalInnerHtml;\n\n // Show the exception.\n Notification.exception(exception);\n }\n pendingPromise.resolve();\n};\n"],"names":["SELECTORS","TOGGLE_TYPES","registered","document","addEventListener","e","toggleButton","target","closest","preventDefault","toggleManualCompletionState","catch","Notification","exception","async","pendingPromise","Pending","originalInnerHtml","innerHTML","setAttribute","toggleType","getAttribute","cmid","activityname","completed","renderForPromise","then","loadingHtml","replaceNodeContents","templateContext","overallcomplete","overallincomplete","istrackeduser","renderObject","Templates","newToggleButton","replaceNode","html","js","pop","withAvailability","toggledEvent","CustomEvent","CourseEvents","manualCompletionToggled","bubbles","detail","dispatchEvent","removeAttribute","resolve"],"mappings":";;;;;;;;44BAmCMA,wBACa,+CAQbC,8BACgB,uBASlBC,YAAa,gBAKG,KACZA,aAGJC,SAASC,iBAAiB,SAAUC,UAC1BC,aAAeD,EAAEE,OAAOC,QAAQR,yBAClCM,eACAD,EAAEI,iBACFC,4BAA4BJ,cAAcK,MAAMC,sBAAaC,eAGrEX,YAAa,UASXQ,4BAA8BI,MAAAA,qBAC1BC,eAAiB,IAAIC,iBAAQ,2CAE7BC,kBAAoBX,aAAaY,UAGvCZ,aAAaa,aAAa,WAAY,kBAGhCC,WAAad,aAAae,aAAa,mBACvCC,KAAOhB,aAAae,aAAa,aACjCE,aAAejB,aAAae,aAAa,qBAEzCG,UAAYJ,aAAenB,iDAGvBwB,iBAAiB,eAAgB,IAC1CC,MAAMC,iCACOC,oBAAoBtB,aAAcqB,YAAa,OAE1DhB,OAAM,mBAIC,sCAAuBW,KAAME,iBAG7BK,gBAAkB,CACpBP,KAAMA,KACNC,aAAcA,aACdO,gBAAiBN,UACjBO,mBAAoBP,UACpBQ,eAAe,GAEbC,mBAAqBC,mBAAUT,iBAAiB,gCAAiCI,iBAIjFM,uBADqBD,mBAAUE,YAAY9B,aAAc2B,aAAaI,KAAMJ,aAAaK,KAC1DC,MAG/BC,iBAAmBlC,aAAae,aAAa,yBAC7CoB,aAAe,IAAIC,YAAYC,aAAaC,wBAAyB,CACvEC,SAAS,EACTC,OAAQ,CACJxB,KAAAA,KACAC,aAAAA,aACAC,UAAAA,UACAgB,iBAAAA,oBAIRL,gBAAgBY,cAAcN,cAEhC,MAAO5B,WAELP,aAAa0C,gBAAgB,YAC7B1C,aAAaY,UAAYD,wCAGZJ,UAAUA,WAE3BE,eAAekC"} \ No newline at end of file diff --git a/course/amd/src/manual_completion_toggle.js b/course/amd/src/manual_completion_toggle.js index b0acec0542fc7..12200f7a878e7 100644 --- a/course/amd/src/manual_completion_toggle.js +++ b/course/amd/src/manual_completion_toggle.js @@ -26,6 +26,7 @@ import Templates from 'core/templates'; import Notification from 'core/notification'; import {toggleManualCompletion} from 'core_course/repository'; import * as CourseEvents from 'core_course/events'; +import Pending from 'core/pending'; /** * Selectors in the manual completion template. @@ -77,6 +78,7 @@ export const init = () => { * @returns {Promise} */ const toggleManualCompletionState = async(toggleButton) => { + const pendingPromise = new Pending('core_course:toggleManualCompletionState'); // Make a copy of the original content of the button. const originalInnerHtml = toggleButton.innerHTML; @@ -91,8 +93,11 @@ const toggleManualCompletionState = async(toggleButton) => { const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE; // Replace the button contents with the loading icon. - const loadingHtml = await Templates.render('core/loading', {}); - await Templates.replaceNodeContents(toggleButton, loadingHtml, ''); + Templates.renderForPromise('core/loading', {}) + .then((loadingHtml) => { + Templates.replaceNodeContents(toggleButton, loadingHtml, ''); + return; + }).catch(() => {}); try { // Call the webservice to update the manual completion status. @@ -134,4 +139,5 @@ const toggleManualCompletionState = async(toggleButton) => { // Show the exception. Notification.exception(exception); } + pendingPromise.resolve(); }; diff --git a/course/classes/reportbuilder/local/entities/completion.php b/course/classes/reportbuilder/local/entities/completion.php index efcad9248bbe4..d8201a2fd54e5 100644 --- a/course/classes/reportbuilder/local/entities/completion.php +++ b/course/classes/reportbuilder/local/entities/completion.php @@ -226,11 +226,11 @@ protected function get_all_columns(): array { LEFT JOIN {grade_grades} {$grade} ON ({$user}.id = {$grade}.userid AND {$gradeitem}.id = {$grade}.itemid) ") - ->set_type(column::TYPE_INTEGER) + ->set_type(column::TYPE_FLOAT) ->add_fields("{$grade}.finalgrade") ->set_is_sortable(true) - ->add_callback(function ($value) { - if (!$value) { + ->add_callback(function(?float $value): string { + if ($value === null) { return ''; } return format_float($value, 2); diff --git a/course/classes/reportbuilder/local/entities/enrolment.php b/course/classes/reportbuilder/local/entities/enrolment.php index c55e3141770e7..9eafb2417c2e3 100644 --- a/course/classes/reportbuilder/local/entities/enrolment.php +++ b/course/classes/reportbuilder/local/entities/enrolment.php @@ -149,7 +149,6 @@ protected function get_all_columns(): array { ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field($this->get_status_field_sql(), 'status') - ->add_field("{$userenrolments}.userid") ->set_is_sortable(true) ->add_callback([enrolment_formatter::class, 'enrolment_status']); @@ -200,7 +199,7 @@ private function get_status_field_sql(): string { THEN " . status_field::STATUS_NOT_CURRENT . " ELSE " . status_field::STATUS_ACTIVE . " END - ELSE " . status_field::STATUS_SUSPENDED . " + ELSE {$userenrolments}.status END"; } diff --git a/course/classes/reportbuilder/local/formatters/enrolment.php b/course/classes/reportbuilder/local/formatters/enrolment.php index fb97e74f93533..b502c903885fb 100644 --- a/course/classes/reportbuilder/local/formatters/enrolment.php +++ b/course/classes/reportbuilder/local/formatters/enrolment.php @@ -67,14 +67,14 @@ public static function enrolment_values(): array { /** * Return enrolment status for user * - * @param string $value - * @param stdClass $row + * @param string|null $value * @return string|null */ - public static function enrolment_status(string $value, stdClass $row): ?string { - if (!$row->userid) { + public static function enrolment_status(?string $value): ?string { + if ($value === null) { return null; } + $statusvalues = self::enrolment_values(); $value = (int) $value; diff --git a/course/delete.php b/course/delete.php index 570e5a1e09c3a..b682523b5b1da 100644 --- a/course/delete.php +++ b/course/delete.php @@ -65,7 +65,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(); @@ -85,7 +85,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 dc7b233d20364..fc3819100a1be 100644 --- a/course/edit.php +++ b/course/edit.php @@ -251,7 +251,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/amd/build/local/content/actions.min.js b/course/format/amd/build/local/content/actions.min.js index 1836bdffc4d66..051bd6e1520e7 100644 --- a/course/format/amd/build/local/content/actions.min.js +++ b/course/format/amd/build/local/content/actions.min.js @@ -9,6 +9,6 @@ define("core_courseformat/local/content/actions",["exports","core/reactive","cor * @class core_courseformat/local/content/actions * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_jquery=_interopRequireDefault(_jquery),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-toggle="dropdown"]'},this.classes={DISABLED:"disabled"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state})))}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}async _requestMoveSection(target,event){const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title;const modalParams={title:(0,_str.get_string)("movecoursesection","core"),body:_templates.default.render("core_courseformat/local/content/movesection",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0],currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement);const generalSection=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-number='0']"));this._disableLink(generalSection),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMove",[sectionId],target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){var _toggler$data;const cmId=target.dataset.id;if(!cmId)return;const cmInfo=this.reactive.get("cm",cmId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);data.cmid=cmInfo.id,data.cmname=cmInfo.name;const modalParams={title:(0,_str.get_string)("movecoursemodule","core"),body:_templates.default.render("core_courseformat/local/content/movecm",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0];let currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK});const sectionnode=currentElement.closest(this.selectors.SECTIONNODE),toggler=(0,_jquery.default)(sectionnode).find(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");collapsibleId&&(collapsibleId=collapsibleId.replace("#",""),(0,_jquery.default)("#".concat(collapsibleId)).collapse("toggle")),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;if(event.preventDefault(),"cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.dispatch("cmMove",[cmId],targetSectionId,targetCmId),this._destroyModal(modal,editTools)})),pendingModalReady.resolve()}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestDeleteSection(target,event){var _sectionInfo$cmlist;const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();if((null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle){const modalParams={title:(0,_str.get_string)("confirm","core"),body:(0,_str.get_string)("confirmdeletesection","moodle",sectionInfo.title),saveButtonText:(0,_str.get_string)("delete","core"),type:_modal_factory.default.types.SAVE_CANCEL},modal=await this._modalBodyRenderedPromise(modalParams);modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.destroy(),this.reactive.dispatch("sectionDelete",[sectionId])}))}else this.reactive.dispatch("sectionDelete",[sectionId])}async _requestMutationAction(target,event,mutationName){target.dataset.id&&(event.preventDefault(),this.reactive.dispatch(mutationName,[target.dataset.id]))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked),this.setElementLocked(element,locked)}))}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_factory.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_jquery=_interopRequireDefault(_jquery),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-toggle="dropdown"]'},this.classes={DISABLED:"text-body",ITALIC:"font-italic"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state})))}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}async _requestMoveSection(target,event){const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title;const modalParams={title:(0,_str.get_string)("movecoursesection","core"),body:_templates.default.render("core_courseformat/local/content/movesection",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0],currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement);const generalSection=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-number='0']"));this._disableLink(generalSection),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMove",[sectionId],target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){var _toggler$data;const cmId=target.dataset.id;if(!cmId)return;const cmInfo=this.reactive.get("cm",cmId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);data.cmid=cmInfo.id,data.cmname=cmInfo.name;const modalParams={title:(0,_str.get_string)("movecoursemodule","core"),body:_templates.default.render("core_courseformat/local/content/movecm",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0];let currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK});const sectionnode=currentElement.closest(this.selectors.SECTIONNODE),toggler=(0,_jquery.default)(sectionnode).find(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");collapsibleId&&(collapsibleId=collapsibleId.replace("#",""),(0,_jquery.default)("#".concat(collapsibleId)).collapse("toggle")),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;if(event.preventDefault(),"cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.dispatch("cmMove",[cmId],targetSectionId,targetCmId),this._destroyModal(modal,editTools)})),pendingModalReady.resolve()}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestDeleteSection(target,event){var _sectionInfo$cmlist;const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();if((null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle){const modalParams={title:(0,_str.get_string)("confirm","core"),body:(0,_str.get_string)("confirmdeletesection","moodle",sectionInfo.title),saveButtonText:(0,_str.get_string)("delete","core"),type:_modal_factory.default.types.SAVE_CANCEL},modal=await this._modalBodyRenderedPromise(modalParams);modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.destroy(),this.reactive.dispatch("sectionDelete",[sectionId])}))}else this.reactive.dispatch("sectionDelete",[sectionId])}async _requestMutationAction(target,event,mutationName){target.dataset.id&&(event.preventDefault(),this.reactive.dispatch(mutationName,[target.dataset.id]))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked),element.classList.toggle(this.classes.ITALIC,locked),this.setElementLocked(element,locked)}))}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.classList.add(this.classes.ITALIC),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_factory.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/actions.min.js.map b/course/format/amd/build/local/content/actions.min.js.map index e88679752e07a..6374a348d0375 100644 --- a/course/format/amd/build/local/content/actions.min.js.map +++ b/course/format/amd/build/local/content/actions.min.js.map @@ -1 +1 @@ -{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\nimport {getList} from 'core/normalise';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `disabled`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target section id and title.\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursesection', 'core'),\n body: Templates.render('core_courseformat/local/content/movesection', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element and section zero.\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);\n this._disableLink(generalSection);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmId = target.dataset.id;\n if (!cmId) {\n return;\n }\n const cmInfo = this.reactive.get('cm', cmId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target cm info.\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursemodule', 'core'),\n body: Templates.render('core_courseformat/local/content/movecm', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element.\n let currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n // All jQuery int this code can be replaced when MDL-71979 is integrated.\n const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);\n const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n jQuery(`#${collapsibleId}`).collapse('toggle');\n }\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n // Get draggable data from cm or section to dispatch.\n let targetSectionId;\n let targetCmId;\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n\n this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const cmList = sectionInfo.cmlist ?? [];\n if (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle) {\n // We need confirmation if the section has something.\n const modalParams = {\n title: getString('confirm', 'core'),\n body: getString('confirmdeletesection', 'moodle', sectionInfo.title),\n saveButtonText: getString('delete', 'core'),\n type: ModalFactory.types.SAVE_CANCEL,\n };\n\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n modal.getRoot().on(\n ModalEvents.save,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n );\n return;\n } else {\n // We don't need confirmation to delete empty sections.\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n this.setElementLocked(element, locked);\n });\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalFactory.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","classes","DISABLED","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","sectionId","id","sectionInfo","reactive","get","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","sectionid","sectiontitle","title","modalParams","body","Templates","render","modal","_modalBodyRenderedPromise","modalBody","getBody","currentElement","querySelector","_disableLink","generalSection","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmId","cmInfo","exporter","cmid","cmname","ENTER","sectionnode","toggler","find","collapsibleId","attr","replace","collapse","targetSectionId","targetCmId","dropData","cmDraggableData","nextcmid","section","cmlist","hassummary","rawtitle","saveButtonText","type","ModalFactory","types","SAVE_CANCEL","getRoot","on","ModalEvents","save","e","destroy","mutationName","locked","getElements","forEach","toggle","setElementLocked","style","pointerEvents","userSelect","add","setAttribute","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","show","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":";;;;;;;;;;;ujCAyCgB,OAAQ,CAAC,oBAAqB,mBAAoB,UAAW,iBAKvEA,gBAAkB,CACpBC,YAAa,cACbC,YAAa,cACbC,OAAQ,SACRC,OAAQ,SACRC,UAAW,YACXC,YAAa,cACbC,WAAY,qCAGaC,wBAKzBC,cAESC,KAAO,uBAEPC,UAAY,CACbC,2BAEAC,mCACAC,yBACAC,uCACAC,wCACAC,wCACAC,oCACAC,0BACAC,mDAGCC,QAAU,CACXC,uCASUC,aACT,MAAOC,OAAQC,qBAAsBC,OAAOC,QAAQJ,SAAU,IAC9B,mBAAtBE,mBAAiE,iBAAtBA,wBAC5C,IAAIG,gBAASJ,yDAEvBxB,gBAAgBwB,QAAUC,mBAUlCI,WAAWC,YAEFC,iBACDC,KAAKC,QACL,QACAD,KAAKE,qBAGJC,kBAAkB,CAACL,MAAAA,aAEnBC,iBACDC,KAAKC,QACLG,aAAaC,kBACb,IAAML,KAAKG,kBAAkB,CAACL,MAAAA,UAStCQ,oBACW,CAEH,CAACC,mCAAqCC,QAASR,KAAKG,oBAI5DD,eAAeO,aACLC,OAASD,MAAMC,OAAOC,QAAQX,KAAKrB,UAAUC,gBAC9C8B,iBAGDA,OAAOE,UAAUC,SAASb,KAAKX,QAAQC,sBACvCmB,MAAMK,uBAKJC,WAAaL,OAAOM,QAAQxB,OAC5ByB,WAAajB,KAAKkB,kBAAkBH,oBAEjBI,IAArBnB,KAAKiB,wBAM2BE,IAAhCnD,gBAAgB+C,YAC2B,mBAAhC/C,gBAAgB+C,iBACvB/C,gBAAgB+C,YAAYL,OAAQD,iBAGnCW,uBAAuBV,OAAQD,MAAOzC,gBAAgB+C,yBAVtDE,YAAYP,OAAQD,OAejCS,kBAAkBxC,YACR2C,YAAc3C,KAAK4C,OAAO,GAAGC,cAAgB7C,KAAK8C,MAAM,2BAC5CH,aAStBlB,4BAAkBL,MAACA,iBAEV2B,qBAAqB3B,MAAM4B,OAAOC,YAAYC,OAAS9B,MAAM4B,OAAOG,uCASnDnB,OAAQD,aAExBqB,UAAYpB,OAAOM,QAAQe,OAC5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,uBAEAqB,kBAAoB,IAAIC,iEAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAI9C6B,KADWvC,KAAKiC,SAASO,cACTd,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAKE,UAAYT,YAAYD,GAC7BQ,KAAKG,aAAeV,YAAYW,YAG1BC,YAAc,CAChBD,OAAO,mBAAU,oBAAqB,QACtCE,KAAMC,mBAAUC,OAAO,8CAA+CR,OAIpES,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,GAGrCC,eAAiBF,UAAUG,wBAAiBrD,KAAKrB,UAAUE,iCAAwBiD,sBACpFwB,aAAaF,sBACZG,eAAiBL,UAAUG,wBAAiBrD,KAAKrB,UAAUE,uCAC5DyE,aAAaC,oBAGdC,qBACAN,UAAUG,cAAcrD,KAAKrB,UAAUO,aACvC,CACIuE,QAASzD,KAAKrB,UAAUI,YACxB2E,QAAS1D,KAAKrB,UAAUK,aACxB2E,SAAU3D,KAAKrB,UAAUK,eAE7B,GAIJkE,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,OAChBA,OAAOkD,QAAQ,MAA8B,WAAtBlD,OAAOM,QAAQ6C,UAA0C1C,IAAtBT,OAAOM,QAAQe,KAG1ErB,OAAOoD,aAAa,mBAGxBrD,MAAMK,sBACDmB,SAAS8B,SAAS,cAAe,CAACjC,WAAYpB,OAAOM,QAAQe,SAC7DiC,cAAchB,MAAOX,gBAG9BF,kBAAkB8B,+BASDvD,OAAQD,+BAEnByD,KAAOxD,OAAOM,QAAQe,OACvBmC,kBAGCC,OAASnE,KAAKiC,SAASC,IAAI,KAAMgC,MAEvCzD,MAAMK,uBAEAqB,kBAAoB,IAAIC,4DAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAG9C0D,SAAWpE,KAAKiC,SAASO,cACzBD,KAAO6B,SAAS1C,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAK8B,KAAOF,OAAOpC,GACnBQ,KAAK+B,OAASH,OAAOzF,WAGfkE,YAAc,CAChBD,OAAO,mBAAU,mBAAoB,QACrCE,KAAMC,mBAAUC,OAAO,yCAA0CR,OAI/DS,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,OAGvCC,eAAiBF,UAAUG,wBAAiBrD,KAAKrB,UAAUG,4BAAmBoF,iBAC7EZ,aAAaF,oBAGdI,qBACAN,UAAUG,cAAcrD,KAAKrB,UAAUO,aACvC,CACIuE,QAASzD,KAAKrB,UAAUI,YACxB2E,QAAS1D,KAAKrB,UAAUK,aACxB2E,SAAU3D,KAAKrB,UAAUK,aACzBuF,MAAOvE,KAAKrB,UAAUE,oBAMxB2F,YAAcpB,eAAezC,QAAQX,KAAKrB,UAAUI,aACpD0F,SAAU,mBAAOD,aAAaE,KAAK1E,KAAKrB,UAAUK,kBACpD2F,oCAAgBF,QAAQlC,KAAK,iDAAakC,QAAQG,KAAK,QACvDD,gBAEAA,cAAgBA,cAAcE,QAAQ,IAAK,mCAChCF,gBAAiBG,SAAS,WAIzC5B,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,WAChBA,OAAOkD,QAAQ,WAA+BzC,IAAvBT,OAAOM,QAAQ6C,UAA2C1C,IAAtBT,OAAOM,QAAQe,aAG3ErB,OAAOoD,aAAa,4BAMpBiB,gBACAC,cAJJvE,MAAMK,iBAKoB,MAAtBJ,OAAOM,QAAQ6C,IAAa,OACtBoB,SAAWb,SAASc,gBAAgBlF,KAAKiC,SAASnC,MAAOY,OAAOM,QAAQe,IAC9EgD,gBAAkBE,SAASxC,UAC3BuC,WAAaC,SAASE,aACnB,OACGC,QAAUpF,KAAKiC,SAASC,IAAI,UAAWxB,OAAOM,QAAQe,IAC5DgD,gBAAkBrE,OAAOM,QAAQe,GACjCiD,WAAaI,MAAAA,eAAAA,QAASC,OAAO,QAG5BpD,SAAS8B,SAAS,SAAU,CAACG,MAAOa,gBAAiBC,iBACrDhB,cAAchB,MAAOX,cAG9BF,kBAAkB8B,mCASGvD,OAAQD,8BAC7BA,MAAMK,sBACDmB,SAAS8B,SAAS,wCAAcrD,OAAOM,QAAQe,oDAAM,+BASlCrB,OAAQD,qCAE1BqB,UAAYpB,OAAOM,QAAQe,OAE5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,iDAESkB,YAAYqD,0DAAU,IAC1BzD,QAAUI,YAAYsD,YAActD,YAAYuD,gBAEjD3C,YAAc,CAChBD,OAAO,mBAAU,UAAW,QAC5BE,MAAM,mBAAU,uBAAwB,SAAUb,YAAYW,OAC9D6C,gBAAgB,mBAAU,SAAU,QACpCC,KAAMC,uBAAaC,MAAMC,aAGvB5C,YAAchD,KAAKiD,0BAA0BL,aAEnDI,MAAM6C,UAAUC,GACZC,sBAAYC,MACZC,IAEIA,EAAEnF,iBACFkC,MAAMkD,eACDjE,SAAS8B,SAAS,gBAAiB,CAACjC,yBAM5CG,SAAS8B,SAAS,gBAAiB,CAACjC,yCAWpBpB,OAAQD,MAAO0F,cACnCzF,OAAOM,QAAQe,KAGpBtB,MAAMK,sBACDmB,SAAS8B,SAASoC,aAAc,CAACzF,OAAOM,QAAQe,MAQzDN,qBAAqB2E,QACDpG,KAAKqG,YAAYrG,KAAKrB,UAAUM,YACxCqH,SAAQrG,UACZA,QAAQW,UAAU2F,OAAOvG,KAAKX,QAAQC,SAAU8G,aAC3CI,iBAAiBvG,QAASmG,WASvC9C,aAAarD,SACLA,UACAA,QAAQwG,MAAMC,cAAgB,OAC9BzG,QAAQwG,MAAME,WAAa,OAC3B1G,QAAQW,UAAUgG,IAAI5G,KAAKX,QAAQC,UACnCW,QAAQ4G,aAAa,iBAAiB,GACtC5G,QAAQF,iBAAiB,SAASU,OAASA,MAAMK,oBAUzDmC,0BAA0BL,oBACf,IAAIkE,SAAQ,CAAC7C,QAAS8C,iCACZtI,OAAOmE,aAAaoE,MAAMhE,QACnCA,MAAMiE,kBAAiB,GAEvBjE,MAAM6C,UAAUC,GAAGC,sBAAYmB,cAAc,KACzCjD,QAAQjB,eAGuB7B,IAA/ByB,YAAY4C,gBACZxC,MAAMmE,kBAAkBvE,YAAY4C,gBAExCxC,MAAMoE,UAEPC,OAAM,KACLN,0CAaZ/C,cAAchB,MAAO/C,SACjB+C,MAAMsE,aACAC,eAAiB,IAAInF,sDACvBnC,SACAA,QAAQuH,QAEZC,YAAW,KACPzE,MAAMkD,UACNqB,eAAetD,YAChB,KASP3B,6BAA6BrC,eACnByH,WAAazH,QAAQU,QAAQX,KAAKrB,UAAUQ,eAC7CuI,kBAGEA,WAAWrE,cAAcrD,KAAKrB,UAAUS"} \ No newline at end of file +{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\nimport {getList} from 'core/normalise';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `text-body`,\n ITALIC: `font-italic`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target section id and title.\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursesection', 'core'),\n body: Templates.render('core_courseformat/local/content/movesection', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element and section zero.\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);\n this._disableLink(generalSection);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmId = target.dataset.id;\n if (!cmId) {\n return;\n }\n const cmInfo = this.reactive.get('cm', cmId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target cm info.\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursemodule', 'core'),\n body: Templates.render('core_courseformat/local/content/movecm', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element.\n let currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n // All jQuery int this code can be replaced when MDL-71979 is integrated.\n const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);\n const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n jQuery(`#${collapsibleId}`).collapse('toggle');\n }\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n // Get draggable data from cm or section to dispatch.\n let targetSectionId;\n let targetCmId;\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n\n this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const cmList = sectionInfo.cmlist ?? [];\n if (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle) {\n // We need confirmation if the section has something.\n const modalParams = {\n title: getString('confirm', 'core'),\n body: getString('confirmdeletesection', 'moodle', sectionInfo.title),\n saveButtonText: getString('delete', 'core'),\n type: ModalFactory.types.SAVE_CANCEL,\n };\n\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n modal.getRoot().on(\n ModalEvents.save,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n );\n return;\n } else {\n // We don't need confirmation to delete empty sections.\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n element.classList.toggle(this.classes.ITALIC, locked);\n this.setElementLocked(element, locked);\n });\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.classList.add(this.classes.ITALIC);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalFactory.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","classes","DISABLED","ITALIC","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","sectionId","id","sectionInfo","reactive","get","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","sectionid","sectiontitle","title","modalParams","body","Templates","render","modal","_modalBodyRenderedPromise","modalBody","getBody","currentElement","querySelector","_disableLink","generalSection","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmId","cmInfo","exporter","cmid","cmname","ENTER","sectionnode","toggler","find","collapsibleId","attr","replace","collapse","targetSectionId","targetCmId","dropData","cmDraggableData","nextcmid","section","cmlist","hassummary","rawtitle","saveButtonText","type","ModalFactory","types","SAVE_CANCEL","getRoot","on","ModalEvents","save","e","destroy","mutationName","locked","getElements","forEach","toggle","setElementLocked","style","pointerEvents","userSelect","add","setAttribute","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","show","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":";;;;;;;;;;;ujCAyCgB,OAAQ,CAAC,oBAAqB,mBAAoB,UAAW,iBAKvEA,gBAAkB,CACpBC,YAAa,cACbC,YAAa,cACbC,OAAQ,SACRC,OAAQ,SACRC,UAAW,YACXC,YAAa,cACbC,WAAY,qCAGaC,wBAKzBC,cAESC,KAAO,uBAEPC,UAAY,CACbC,2BAEAC,mCACAC,yBACAC,uCACAC,wCACAC,wCACAC,oCACAC,0BACAC,mDAGCC,QAAU,CACXC,qBACAC,wCASUC,aACT,MAAOC,OAAQC,qBAAsBC,OAAOC,QAAQJ,SAAU,IAC9B,mBAAtBE,mBAAiE,iBAAtBA,wBAC5C,IAAIG,gBAASJ,yDAEvBzB,gBAAgByB,QAAUC,mBAUlCI,WAAWC,YAEFC,iBACDC,KAAKC,QACL,QACAD,KAAKE,qBAGJC,kBAAkB,CAACL,MAAAA,aAEnBC,iBACDC,KAAKC,QACLG,aAAaC,kBACb,IAAML,KAAKG,kBAAkB,CAACL,MAAAA,UAStCQ,oBACW,CAEH,CAACC,mCAAqCC,QAASR,KAAKG,oBAI5DD,eAAeO,aACLC,OAASD,MAAMC,OAAOC,QAAQX,KAAKtB,UAAUC,gBAC9C+B,iBAGDA,OAAOE,UAAUC,SAASb,KAAKZ,QAAQC,sBACvCoB,MAAMK,uBAKJC,WAAaL,OAAOM,QAAQxB,OAC5ByB,WAAajB,KAAKkB,kBAAkBH,oBAEjBI,IAArBnB,KAAKiB,wBAM2BE,IAAhCpD,gBAAgBgD,YAC2B,mBAAhChD,gBAAgBgD,iBACvBhD,gBAAgBgD,YAAYL,OAAQD,iBAGnCW,uBAAuBV,OAAQD,MAAO1C,gBAAgBgD,yBAVtDE,YAAYP,OAAQD,OAejCS,kBAAkBzC,YACR4C,YAAc5C,KAAK6C,OAAO,GAAGC,cAAgB9C,KAAK+C,MAAM,2BAC5CH,aAStBlB,4BAAkBL,MAACA,iBAEV2B,qBAAqB3B,MAAM4B,OAAOC,YAAYC,OAAS9B,MAAM4B,OAAOG,uCASnDnB,OAAQD,aAExBqB,UAAYpB,OAAOM,QAAQe,OAC5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,uBAEAqB,kBAAoB,IAAIC,iEAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAI9C6B,KADWvC,KAAKiC,SAASO,cACTd,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAKE,UAAYT,YAAYD,GAC7BQ,KAAKG,aAAeV,YAAYW,YAG1BC,YAAc,CAChBD,OAAO,mBAAU,oBAAqB,QACtCE,KAAMC,mBAAUC,OAAO,8CAA+CR,OAIpES,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,GAGrCC,eAAiBF,UAAUG,wBAAiBrD,KAAKtB,UAAUE,iCAAwBkD,sBACpFwB,aAAaF,sBACZG,eAAiBL,UAAUG,wBAAiBrD,KAAKtB,UAAUE,uCAC5D0E,aAAaC,oBAGdC,qBACAN,UAAUG,cAAcrD,KAAKtB,UAAUO,aACvC,CACIwE,QAASzD,KAAKtB,UAAUI,YACxB4E,QAAS1D,KAAKtB,UAAUK,aACxB4E,SAAU3D,KAAKtB,UAAUK,eAE7B,GAIJmE,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,OAChBA,OAAOkD,QAAQ,MAA8B,WAAtBlD,OAAOM,QAAQ6C,UAA0C1C,IAAtBT,OAAOM,QAAQe,KAG1ErB,OAAOoD,aAAa,mBAGxBrD,MAAMK,sBACDmB,SAAS8B,SAAS,cAAe,CAACjC,WAAYpB,OAAOM,QAAQe,SAC7DiC,cAAchB,MAAOX,gBAG9BF,kBAAkB8B,+BASDvD,OAAQD,+BAEnByD,KAAOxD,OAAOM,QAAQe,OACvBmC,kBAGCC,OAASnE,KAAKiC,SAASC,IAAI,KAAMgC,MAEvCzD,MAAMK,uBAEAqB,kBAAoB,IAAIC,4DAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAG9C0D,SAAWpE,KAAKiC,SAASO,cACzBD,KAAO6B,SAAS1C,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAK8B,KAAOF,OAAOpC,GACnBQ,KAAK+B,OAASH,OAAO1F,WAGfmE,YAAc,CAChBD,OAAO,mBAAU,mBAAoB,QACrCE,KAAMC,mBAAUC,OAAO,yCAA0CR,OAI/DS,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,OAGvCC,eAAiBF,UAAUG,wBAAiBrD,KAAKtB,UAAUG,4BAAmBqF,iBAC7EZ,aAAaF,oBAGdI,qBACAN,UAAUG,cAAcrD,KAAKtB,UAAUO,aACvC,CACIwE,QAASzD,KAAKtB,UAAUI,YACxB4E,QAAS1D,KAAKtB,UAAUK,aACxB4E,SAAU3D,KAAKtB,UAAUK,aACzBwF,MAAOvE,KAAKtB,UAAUE,oBAMxB4F,YAAcpB,eAAezC,QAAQX,KAAKtB,UAAUI,aACpD2F,SAAU,mBAAOD,aAAaE,KAAK1E,KAAKtB,UAAUK,kBACpD4F,oCAAgBF,QAAQlC,KAAK,iDAAakC,QAAQG,KAAK,QACvDD,gBAEAA,cAAgBA,cAAcE,QAAQ,IAAK,mCAChCF,gBAAiBG,SAAS,WAIzC5B,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,WAChBA,OAAOkD,QAAQ,WAA+BzC,IAAvBT,OAAOM,QAAQ6C,UAA2C1C,IAAtBT,OAAOM,QAAQe,aAG3ErB,OAAOoD,aAAa,4BAMpBiB,gBACAC,cAJJvE,MAAMK,iBAKoB,MAAtBJ,OAAOM,QAAQ6C,IAAa,OACtBoB,SAAWb,SAASc,gBAAgBlF,KAAKiC,SAASnC,MAAOY,OAAOM,QAAQe,IAC9EgD,gBAAkBE,SAASxC,UAC3BuC,WAAaC,SAASE,aACnB,OACGC,QAAUpF,KAAKiC,SAASC,IAAI,UAAWxB,OAAOM,QAAQe,IAC5DgD,gBAAkBrE,OAAOM,QAAQe,GACjCiD,WAAaI,MAAAA,eAAAA,QAASC,OAAO,QAG5BpD,SAAS8B,SAAS,SAAU,CAACG,MAAOa,gBAAiBC,iBACrDhB,cAAchB,MAAOX,cAG9BF,kBAAkB8B,mCASGvD,OAAQD,8BAC7BA,MAAMK,sBACDmB,SAAS8B,SAAS,wCAAcrD,OAAOM,QAAQe,oDAAM,+BASlCrB,OAAQD,qCAE1BqB,UAAYpB,OAAOM,QAAQe,OAE5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,iDAESkB,YAAYqD,0DAAU,IAC1BzD,QAAUI,YAAYsD,YAActD,YAAYuD,gBAEjD3C,YAAc,CAChBD,OAAO,mBAAU,UAAW,QAC5BE,MAAM,mBAAU,uBAAwB,SAAUb,YAAYW,OAC9D6C,gBAAgB,mBAAU,SAAU,QACpCC,KAAMC,uBAAaC,MAAMC,aAGvB5C,YAAchD,KAAKiD,0BAA0BL,aAEnDI,MAAM6C,UAAUC,GACZC,sBAAYC,MACZC,IAEIA,EAAEnF,iBACFkC,MAAMkD,eACDjE,SAAS8B,SAAS,gBAAiB,CAACjC,yBAM5CG,SAAS8B,SAAS,gBAAiB,CAACjC,yCAWpBpB,OAAQD,MAAO0F,cACnCzF,OAAOM,QAAQe,KAGpBtB,MAAMK,sBACDmB,SAAS8B,SAASoC,aAAc,CAACzF,OAAOM,QAAQe,MAQzDN,qBAAqB2E,QACDpG,KAAKqG,YAAYrG,KAAKtB,UAAUM,YACxCsH,SAAQrG,UACZA,QAAQW,UAAU2F,OAAOvG,KAAKZ,QAAQC,SAAU+G,QAChDnG,QAAQW,UAAU2F,OAAOvG,KAAKZ,QAAQE,OAAQ8G,aACzCI,iBAAiBvG,QAASmG,WASvC9C,aAAarD,SACLA,UACAA,QAAQwG,MAAMC,cAAgB,OAC9BzG,QAAQwG,MAAME,WAAa,OAC3B1G,QAAQW,UAAUgG,IAAI5G,KAAKZ,QAAQC,UACnCY,QAAQW,UAAUgG,IAAI5G,KAAKZ,QAAQE,QACnCW,QAAQ4G,aAAa,iBAAiB,GACtC5G,QAAQF,iBAAiB,SAASU,OAASA,MAAMK,oBAUzDmC,0BAA0BL,oBACf,IAAIkE,SAAQ,CAAC7C,QAAS8C,iCACZvI,OAAOoE,aAAaoE,MAAMhE,QACnCA,MAAMiE,kBAAiB,GAEvBjE,MAAM6C,UAAUC,GAAGC,sBAAYmB,cAAc,KACzCjD,QAAQjB,eAGuB7B,IAA/ByB,YAAY4C,gBACZxC,MAAMmE,kBAAkBvE,YAAY4C,gBAExCxC,MAAMoE,UAEPC,OAAM,KACLN,0CAaZ/C,cAAchB,MAAO/C,SACjB+C,MAAMsE,aACAC,eAAiB,IAAInF,sDACvBnC,SACAA,QAAQuH,QAEZC,YAAW,KACPzE,MAAMkD,UACNqB,eAAetD,YAChB,KASP3B,6BAA6BrC,eACnByH,WAAazH,QAAQU,QAAQX,KAAKtB,UAAUQ,eAC7CwI,kBAGEA,WAAWrE,cAAcrD,KAAKtB,UAAUS"} \ No newline at end of file diff --git a/course/format/amd/src/local/content/actions.js b/course/format/amd/src/local/content/actions.js index e36af4ddd6bda..2682712c5be78 100644 --- a/course/format/amd/src/local/content/actions.js +++ b/course/format/amd/src/local/content/actions.js @@ -77,7 +77,8 @@ export default class extends BaseComponent { }; // Component css classes. this.classes = { - DISABLED: `disabled`, + DISABLED: `text-body`, + ITALIC: `font-italic`, }; } @@ -427,6 +428,7 @@ export default class extends BaseComponent { const targets = this.getElements(this.selectors.ADDSECTION); targets.forEach(element => { element.classList.toggle(this.classes.DISABLED, locked); + element.classList.toggle(this.classes.ITALIC, locked); this.setElementLocked(element, locked); }); } @@ -441,6 +443,7 @@ export default class extends BaseComponent { element.style.pointerEvents = 'none'; element.style.userSelect = 'none'; element.classList.add(this.classes.DISABLED); + element.classList.add(this.classes.ITALIC); element.setAttribute('aria-disabled', true); element.addEventListener('click', event => event.preventDefault()); } diff --git a/course/format/classes/output/local/content/cm/cmname.php b/course/format/classes/output/local/content/cm/cmname.php index 9c7c9ad321001..99825d10818e1 100644 --- a/course/format/classes/output/local/content/cm/cmname.php +++ b/course/format/classes/output/local/content/cm/cmname.php @@ -110,14 +110,12 @@ public function export_for_template(\renderer_base $output): array { 'iconclass' => $iconclass, 'modname' => $mod->modname, 'textclasses' => $displayoptions['textclasses'] ?? '', + 'pluginname' => get_string('pluginname', 'mod_' . $mod->modname), + 'showpluginname' => $this->format->show_editor(), 'purpose' => plugin_supports('mod', $mod->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER), 'activityname' => $this->get_title_data($output), ]; - if ($this->format->show_editor()) { - $data['pluginname'] = get_string('pluginname', 'mod_' . $mod->modname); - } - return $data; } diff --git a/course/format/social/tests/behat/social_adjust_discussion_count.feature b/course/format/social/tests/behat/social_adjust_discussion_count.feature index 2fec3514751ba..07d11ae0182e4 100644 --- a/course/format/social/tests/behat/social_adjust_discussion_count.feature +++ b/course/format/social/tests/behat/social_adjust_discussion_count.feature @@ -105,11 +105,10 @@ Feature: Change number of discussions displayed | Message | This is forum post one | And I press "Post to forum" And I wait to be redirected - And I am on "Course 1" course homepage + And I am on the "C1" "course editing" page logged in as teacher1 Scenario: When number of discussions is decreased fewer discussions appear - Given I navigate to "Settings" in current page administration - And I set the following fields to these values: + Given I set the following fields to these values: | numdiscussions | 5 | When I press "Save and display" Then I should see "This is forum post one" @@ -117,8 +116,7 @@ Feature: Change number of discussions displayed And I should not see "This is forum post six" Scenario: When number of discussions is decreased to less than 1 only 1 discussion should appear - Given I navigate to "Settings" in current page administration - And I set the following fields to these values: + Given I set the following fields to these values: | numdiscussions | -1 | When I press "Save and display" Then I should see "This is forum post one" @@ -126,8 +124,7 @@ Feature: Change number of discussions displayed And I should not see "This is forum post ten" Scenario: When number of discussions is increased more discussions appear - Given I navigate to "Settings" in current page administration - And I set the following fields to these values: + Given I set the following fields to these values: | numdiscussions | 9 | When I press "Save and display" Then I should see "This is forum post one" diff --git a/course/format/templates/local/content/cm/cmname.mustache b/course/format/templates/local/content/cm/cmname.mustache index 62389885190b8..c001b82477a0b 100644 --- a/course/format/templates/local/content/cm/cmname.mustache +++ b/course/format/templates/local/content/cm/cmname.mustache @@ -27,6 +27,7 @@ "icon": "../../../pix/help.svg", "iconclass": "", "pluginname": "File", + "showpluginname": 1, "textclasses": "", "purpose": "content", "modname": "resource", @@ -48,14 +49,16 @@
- {{{modname}}} icon + {{#cleanstr}} activityicon, moodle, {{{pluginname}}} {{/cleanstr}}
- {{#pluginname}} + {{#showpluginname}}
{{{pluginname}}}
- {{/pluginname}} + {{/showpluginname}}
{{#activityname}} {{$ core/inplace_editable }} diff --git a/course/lib.php b/course/lib.php index f01de15b6c752..505411acfacfa 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3444,10 +3444,9 @@ function duplicate_module($course, $cm) { // Proceed with activity renaming before everything else. We don't use APIs here to avoid // triggering a lot of create/update duplicated events. $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course); - // Add ' (copy)' to duplicates. Note we don't cleanup or validate lengths here. It comes - // from original name that was valid, so the copy should be too. + // Add ' (copy)' language string postfix to duplicated module. $newname = get_string('duplicatedmodule', 'moodle', $newcm->name); - $DB->set_field($cm->modname, 'name', $newname, ['id' => $newcm->instance]); + set_coursemodule_name($newcm->id, $newname); $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course)); $modarray = explode(",", trim($section->sequence)); diff --git a/course/renderer.php b/course/renderer.php index 4894ff901e145..70c2bc8ca9861 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -1210,7 +1210,7 @@ protected function course_overview_files(core_course_list_element $course): stri $file->get_filearea() . $file->get_filepath() . $file->get_filename(), !$isimage); if ($isimage) { $contentimages .= html_writer::tag('div', - html_writer::empty_tag('img', ['src' => $url]), + html_writer::empty_tag('img', ['src' => $url, 'alt' => '']), ['class' => 'courseimage']); } else { $image = $this->output->pix_icon(file_file_icon($file, 24), $file->get_filename(), 'moodle'); @@ -1662,13 +1662,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/templates/coursecard.mustache b/course/templates/coursecard.mustache index 0fc3e6083471a..e9c5fabdac216 100644 --- a/course/templates/coursecard.mustache +++ b/course/templates/coursecard.mustache @@ -39,7 +39,7 @@ data-course-id="{{{id}}}">
- {{#str}}aria:courseimage, core_course{{/str}} + {{fullname}}
@@ -58,8 +58,8 @@ {{> core_course/favouriteicon }} - {{#str}}aria:coursename, core_course{{/str}} - + {{#str}}aria:coursename, core_course{{/str}} + {{$coursename}}{{/coursename}}
diff --git a/course/tests/behat/activity_resource_delete.feature b/course/tests/behat/activity_resource_delete.feature new file mode 100644 index 0000000000000..15f03d063d07d --- /dev/null +++ b/course/tests/behat/activity_resource_delete.feature @@ -0,0 +1,39 @@ +@core @core_course +Feature: Delete activity and resource works correctly + As a teacher + I want to be able to delete an activity and resource + So that I can remove it from the course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | course | name | intro | + | label | C1 | Label 1 | Label 1 | + | glossary | C1 | Glossary 1 | Glossary 1 | + + @javascript + Scenario: Activity and resource can be deleted properly + Given I am on the "Course 1" course page logged in as teacher1 + And I turn editing mode on + And I open "Label 1" actions menu + When I click on "Delete" "link" in the "Label 1" activity + And I click on "Yes" "button" in the "Confirm" "dialogue" + # Confirm that label is successfully deleted + Then I should not see "Label 1" + And I open "Glossary 1" actions menu + And I click on "Delete" "link" in the "Glossary 1" activity + And I click on "Yes" "button" in the "Confirm" "dialogue" + # Confirm that glossary is successfully deleted + And I should not see "Glossary 1" + # Reload the page and confirm that both the label and glossary are really deleted + And I reload the page + And I should not see "Label 1" + And I should not see "Glossary 1" 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/tests/reportbuilder/datasource/participants_test.php b/course/tests/reportbuilder/datasource/participants_test.php index 25025ae019113..3c9c1abf9ccf2 100644 --- a/course/tests/reportbuilder/datasource/participants_test.php +++ b/course/tests/reportbuilder/datasource/participants_test.php @@ -80,7 +80,7 @@ public function test_participants_datasource(): void { // Update final grade for the user. $courseitem = grade_item::fetch_course_item($course->id); - $courseitem->update_final_grade($user1->id, 80); + $courseitem->update_final_grade($user1->id, 42.5); // Set some last access value for the user in the course. $DB->insert_record('user_lastaccess', @@ -152,7 +152,7 @@ public function test_participants_datasource(): void { '', // Reagreggate. '2', // Days taking course. '2', // Days until completion. - '80.00', // Grade. + '42.50', // Grade. ], array_values($content[0])); } 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/lti/classes/local/ltiadvantage/task/sync_grades.php b/enrol/lti/classes/local/ltiadvantage/task/sync_grades.php index d2ecd53a69416..cd2ea3a88bd33 100644 --- a/enrol/lti/classes/local/ltiadvantage/task/sync_grades.php +++ b/enrol/lti/classes/local/ltiadvantage/task/sync_grades.php @@ -195,7 +195,8 @@ protected function sync_grades_for_resource($resource): array { continue; } - if ($response['status'] == 200) { + $successresponses = [200, 201, 202, 204]; + if (in_array($response['status'], $successresponses)) { $user->set_lastgrade(grade_floatval($grade)); $syncedusergrades[$user->get_id()] = $user; mtrace("Success - The grade '$floatgrade' $mtracecontent was sent."); diff --git a/enrol/lti/tests/local/ltiadvantage/task/sync_grades_test.php b/enrol/lti/tests/local/ltiadvantage/task/sync_grades_test.php index 3282491e18938..62b9a36396a5a 100644 --- a/enrol/lti/tests/local/ltiadvantage/task/sync_grades_test.php +++ b/enrol/lti/tests/local/ltiadvantage/task/sync_grades_test.php @@ -144,6 +144,70 @@ protected function override_resource_completion_status_for_user(\stdClass $resou } } + + /** + * Data provider for test_grade_sync_positive_case. + * + * @return array + */ + public static function grade_sync_positive_cases(): array { + return [ + [200], + [201], + [202], + [204], + ]; + } + + /** + * Test the sync grades task works correct when platform responses with given status code. + * + * @covers ::execute + * @param string $statuscode the response status code with which the job should work correctly + * @dataProvider grade_sync_positive_cases + */ + public function test_grade_sync_positive_case($statuscode): void { + $this->resetAfterTest(); + + [$course, $resource] = $this->create_test_environment(); + $launchservice = $this->get_tool_launch_service(); + $task = $this->get_task_with_mocked_grade_service($statuscode); + + // Launch the resource for an instructor which will create the domain objects needed for service calls. + $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); + $instructoruser = $this->getDataGenerator()->create_user(); + [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); + + // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. + $studentusers = $this->get_mock_launch_users_with_ids(['2'], false, + 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); + + $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); + $student1user = $this->getDataGenerator()->create_user(); + [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); + + // Grade student1. + $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); + + // Sync and verify that only student1's grade is sent. + ob_start(); + $task->execute(); + $ob = ob_get_contents(); + ob_end_clean(); + $expectedtraces = [ + "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", + "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". + "'$course->id'.", + "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". + "'$resource->id' and the course '$course->id' was sent.", + "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". + "Processed 2 users; sent 1 grades." + ]; + foreach ($expectedtraces as $expectedtrace) { + $this->assertStringContainsString($expectedtrace, $ob); + } + } + /** * Test confirming task name. * diff --git a/enrol/manual/lib.php b/enrol/manual/lib.php index 3ae5d0fd1761d..d896db70780d7 100644 --- a/enrol/manual/lib.php +++ b/enrol/manual/lib.php @@ -173,6 +173,9 @@ public function update_instance($instance, $data) { } } } + + $data->notifyall = $data->expirynotify == 2 ? 1 : 0; + return parent::update_instance($instance, $data); } diff --git a/enrol/manual/tests/lib_test.php b/enrol/manual/tests/lib_test.php index a8a6e86a3bdaa..0f4b09bdc1094 100644 --- a/enrol/manual/tests/lib_test.php +++ b/enrol/manual/tests/lib_test.php @@ -647,4 +647,118 @@ public function default_enrolment_instance_data_provider(): array { ], ]; } + + /** + * Tests an enrolment instance is updated properly. + * + * @covers \enrol_manual::update_instance + * @dataProvider update_enrolment_instance_data_provider + * + * @param stdClass $expectation + * @param stdClass $updatedata + */ + public function test_enrolment_instance_is_updated(stdClass $expectation, stdClass $updatedata): void { + global $DB; + + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + + $studentroles = get_archetype_roles('student'); + $studentrole = array_shift($studentroles); + + // Given the plugin is globally configured with the following settings. + $plugin = enrol_get_plugin('manual'); + $plugin->set_config('status', ENROL_INSTANCE_ENABLED); + $plugin->set_config('roleid', $studentrole->id); + $plugin->set_config('enrolperiod', 30 * DAYSECS); + $plugin->set_config('expirynotify', 1); + $plugin->set_config('expirythreshold', 2 * DAYSECS); + + // And a course is created with the default enrolment instance. + $course = $generator->create_course(); + + // When the enrolment instance is being updated. + $enrolinstance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'manual']); + $successfullyupdated = $plugin->update_instance($enrolinstance, $updatedata); + + // Then the update is successful. + $this->assertTrue($successfullyupdated); + + // And the updated enrolment instance contains the expected values. + $enrolinstance = $DB->get_record('enrol', ['id' => $enrolinstance->id]); + $this->assertEquals($expectation->status, $enrolinstance->status); + $this->assertEquals($expectation->roleid, $enrolinstance->roleid); + $this->assertEquals($expectation->enrolperiod, $enrolinstance->enrolperiod); + $this->assertEquals($expectation->expirynotify, $enrolinstance->expirynotify); + $this->assertEquals($expectation->notifyall, $enrolinstance->notifyall); + $this->assertEquals($expectation->expirythreshold, $enrolinstance->expirythreshold); + } + + /** + * Data provider for test_enrolment_instance_is_updated(). + * + * @return array + */ + public function update_enrolment_instance_data_provider(): array { + $studentroles = get_archetype_roles('student'); + $studentrole = array_shift($studentroles); + + $teacherroles = get_archetype_roles('teacher'); + $teacherrole = array_shift($teacherroles); + + return [ + 'disabled, all the others are default' => [ + 'expectation' => (object) [ + 'status' => ENROL_INSTANCE_DISABLED, + 'roleid' => $studentrole->id, + 'enrolperiod' => 30 * DAYSECS, + 'expirynotify' => 1, + 'notifyall' => 0, + 'expirythreshold' => 2 * DAYSECS, + ], + 'update data' => (object) [ + 'status' => ENROL_INSTANCE_DISABLED, + 'roleid' => $studentrole->id, + 'enrolperiod' => 30 * DAYSECS, + 'expirynotify' => 1, + 'expirythreshold' => 2 * DAYSECS, + ], + ], + 'enabled, teacher role, no duration set, notify no one on expiry, 0 notification threshold' => [ + 'expectation' => (object) [ + 'status' => ENROL_INSTANCE_ENABLED, + 'roleid' => $teacherrole->id, + 'enrolperiod' => 0, + 'expirynotify' => 0, + 'notifyall' => 0, + 'expirythreshold' => 0, + ], + 'update data' => (object) [ + 'status' => ENROL_INSTANCE_ENABLED, + 'roleid' => $teacherrole->id, + 'enrolperiod' => 0, + 'expirynotify' => 0, + 'expirythreshold' => 0, + ], + ], + 'notify enroller and enrolled on expiry, all the others are default' => [ + 'expectation' => (object) [ + 'status' => ENROL_INSTANCE_ENABLED, + 'roleid' => $studentrole->id, + 'enrolperiod' => 30 * DAYSECS, + 'expirynotify' => 2, + 'notifyall' => 1, + 'expirythreshold' => 2 * DAYSECS, + ], + 'update data' => (object) [ + 'status' => ENROL_INSTANCE_ENABLED, + 'roleid' => $studentrole->id, + 'enrolperiod' => 30 * DAYSECS, + 'expirynotify' => 2, + 'expirythreshold' => 2 * DAYSECS, + ], + ], + ]; + } } diff --git a/filter/tidy/filter.php b/filter/tidy/filter.php index 2bdb79ffba39b..350beb24f2354 100644 --- a/filter/tidy/filter.php +++ b/filter/tidy/filter.php @@ -47,7 +47,6 @@ function filter($text, array $options = array()) { 'show-body-only' => true, 'tidy-mark' => false, 'drop-proprietary-attributes' => true, - 'clean' => true, 'drop-empty-paras' => true, 'indent' => true, 'quiet' => true, diff --git a/grade/classes/external/get_groups_for_search_widget.php b/grade/classes/external/get_groups_for_search_widget.php index 7ca8230ec63cf..300cb4627751c 100644 --- a/grade/classes/external/get_groups_for_search_widget.php +++ b/grade/classes/external/get_groups_for_search_widget.php @@ -63,12 +63,8 @@ public static function execute_parameters(): external_function_parameters { * @param int $courseid * @param string $actionbaseurl The base URL for the group action. * @return array Groups and warnings to pass back to the calling widget. - * @throws coding_exception - * @throws invalid_parameter_exception - * @throws moodle_exception - * @throws restricted_context_exception */ - protected static function execute(int $courseid, string $actionbaseurl): array { + public static function execute(int $courseid, string $actionbaseurl): array { global $DB, $USER, $COURSE; $params = self::validate_parameters( diff --git a/grade/edit/tree/lib.php b/grade/edit/tree/lib.php index d49b451fe527c..9a82049c0a7bc 100644 --- a/grade/edit/tree/lib.php +++ b/grade/edit/tree/lib.php @@ -312,8 +312,8 @@ public function build_html_tree($element, $totals, $parents, $level, &$row_count $headercell = new html_table_cell(); $headercell->header = true; $headercell->scope = 'row'; - $headercell->attributes['title'] = $object->stripped_name; $headercell->attributes['class'] = 'cell column-rowspan rowspan ' . $levelclass; + $headercell->attributes['aria-hidden'] = 'true'; $headercell->rowspan = $row_count + 1; $row->cells[] = $headercell; @@ -338,6 +338,7 @@ public function build_html_tree($element, $totals, $parents, $level, &$row_count $endcell = new html_table_cell(); $endcell->colspan = (19 - $level); $endcell->attributes['class'] = 'emptyrow colspan ' . $levelclass; + $endcell->attributes['aria-hidden'] = 'true'; $returnrows[] = new html_table_row(array($endcell)); @@ -749,13 +750,11 @@ public abstract function get_header_cell(); public function get_category_cell($category, $levelclass, $params) { $cell = clone($this->categorycell); $cell->attributes['class'] .= ' ' . $levelclass; - $cell->attributes['text'] = ''; return $cell; } public function get_item_cell($item, $params) { $cell = clone($this->itemcell); - $cell->attributes['text'] = ''; if (isset($params['level'])) { $level = $params['level'] + (($item->itemtype == 'category' || $item->itemtype == 'course') ? 0 : 1); $cell->attributes['class'] .= ' level' . $level; @@ -1029,7 +1028,7 @@ public function get_category_cell($category, $levelclass, $params) { } // Build the master checkbox. $mastercheckbox = new \core\output\checkbox_toggleall($togglegroup, true, [ - 'id' => $togglegroup, + 'id' => 'select_category_' . $category->id, 'name' => $togglegroup, 'value' => 1, 'classes' => 'itemselect ignoredirty', diff --git a/grade/export/index.php b/grade/export/index.php index 7901e2b75a972..7e2957b8ee040 100644 --- a/grade/export/index.php +++ b/grade/export/index.php @@ -37,14 +37,21 @@ $context = context_course::instance($courseid); require_capability('moodle/grade:export', $context); -$exportplugins = core_component::get_plugin_list('gradeexport'); +// Retrieve all grade export plugins the current user can access. +$exportplugins = array_filter(core_component::get_plugin_list('gradeexport'), + static function(string $exportplugin) use ($context): bool { + return has_capability("gradeexport/{$exportplugin}:view", $context); + }, + ARRAY_FILTER_USE_KEY +); + if (!empty($exportplugins)) { $exportplugin = array_key_first($exportplugins); $url = new moodle_url("/grade/export/{$exportplugin}/index.php", ['id' => $courseid]); redirect($url); } -// Otherwise, output the page with a notification stating that there are no available grade import options. +// Otherwise, output the page with a notification stating that there are no available grade export options. $PAGE->set_title(get_string('export', 'grades')); $PAGE->set_pagelayout('incourse'); $PAGE->set_heading($course->fullname); diff --git a/grade/export/txt/tests/behat/export.feature b/grade/export/txt/tests/behat/export.feature index 9ed149dced5f4..6ef31567d8571 100644 --- a/grade/export/txt/tests/behat/export.feature +++ b/grade/export/txt/tests/behat/export.feature @@ -20,13 +20,10 @@ Feature: I need to export grades as text | activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | | assign | C1 | a1 | Test assignment name | Submit your online text | 1 | | assign | C1 | a2 | Test assignment name 2 | Submit your online text | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook - And I turn editing mode on - And I change window size to "large" - And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name" - And I press "Save changes" + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment name | student1 | 80.00 | + And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" @javascript Scenario: Export grades as text diff --git a/grade/export/xml/tests/behat/export.feature b/grade/export/xml/tests/behat/export.feature index 60308cf2dc940..de88363aaf5f0 100644 --- a/grade/export/xml/tests/behat/export.feature +++ b/grade/export/xml/tests/behat/export.feature @@ -19,15 +19,13 @@ Feature: I need to export grades as xml | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: - | activity | course | idnumber | name | intro | - | assign | C1 | a1 | Test assignment name | Submit something! | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook - And I turn editing mode on - And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name" - And I give the grade "42.00" to the user "Student 2" for the grade item "Test assignment name" - And I press "Save changes" + | activity | course | idnumber | name | + | assign | C1 | a1 | Test assignment name | + And the following "grade grades" exist: + | gradeitem | user | grade | + | Test assignment name | student1 | 80.00 | + | Test assignment name | student2 | 42.00 | + And I am on the "Course 1" course page logged in as teacher1 @javascript Scenario: Export grades as XML 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/import/index.php b/grade/import/index.php index fc8d4f9cbf34a..e5acd9b87308a 100644 --- a/grade/import/index.php +++ b/grade/import/index.php @@ -37,7 +37,14 @@ $context = context_course::instance($courseid); require_capability('moodle/grade:import', $context); -$importplugins = core_component::get_plugin_list('gradeimport'); +// Retrieve all grade import plugins the current user can access. +$importplugins = array_filter(core_component::get_plugin_list('gradeimport'), + static function(string $importplugin) use ($context): bool { + return has_capability("gradeimport/{$importplugin}:view", $context); + }, + ARRAY_FILTER_USE_KEY +); + if (!empty($importplugins)) { $importplugin = array_key_first($importplugins); $url = new moodle_url("/grade/import/{$importplugin}/index.php", ['id' => $courseid]); diff --git a/grade/lib.php b/grade/lib.php index f069d54da72ee..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. @@ -889,7 +889,7 @@ public function __construct($id, $link, $string, $parent=null) { function print_grade_page_head(int $courseid, string $active_type, ?string $active_plugin = null, $heading = false, bool $return = false, $buttons = false, bool $shownavigation = true, ?string $headerhelpidentifier = null, ?string $headerhelpcomponent = null, ?stdClass $user = null, ?action_bar $actionbar = null, $showtitle = true) { - global $CFG, $OUTPUT, $PAGE; + global $CFG, $OUTPUT, $PAGE, $USER; // Put a warning on all gradebook pages if the course has modules currently scheduled for background deletion. require_once($CFG->dirroot . '/course/lib.php'); @@ -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'); @@ -970,15 +982,16 @@ function print_grade_page_head(int $courseid, string $active_type, ?string $acti $output = ''; // Add a help dialogue box if provided. - if (isset($headerhelpidentifier)) { + if (isset($headerhelpidentifier) && !empty($heading)) { $output = $OUTPUT->heading_with_help($heading, $headerhelpidentifier, $headerhelpcomponent); - } else { - if (isset($user)) { - $renderer = $PAGE->get_renderer('core_grades'); - $output = $OUTPUT->heading($renderer->user_heading($user, $courseid)); - } else { - $output = $OUTPUT->heading($heading); - } + } else if (isset($user)) { + $renderer = $PAGE->get_renderer('core_grades'); + // If the user is viewing their own grade report, no need to show the "Message" + // and "Add to contact" buttons in the user heading. + $showuserbuttons = $user->id != $USER->id; + $output = $renderer->user_heading($user, $courseid, $showuserbuttons); + } else if (!empty($heading)) { + $output = $OUTPUT->heading($heading); } if ($return) { @@ -987,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/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature index ad68e12ced013..9f21eacabe729 100644 --- a/grade/report/grader/tests/behat/switch_views.feature +++ b/grade/report/grader/tests/behat/switch_views.feature @@ -75,8 +75,7 @@ Feature: We can change what we are viewing on the grader report And the following "role capability" exists: | role | editingteacher | | moodle/grade:viewhidden | prevent | - And I am on the "C1" "course" page logged in as "teacher1" - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" Then I should see "Test assignment name 1" in the "user-grades" "table" And I should see "Test assignment name 2" in the "user-grades" "table" And I should see "Manual grade" diff --git a/grade/report/history/tests/behat/basic_functionality.feature b/grade/report/history/tests/behat/basic_functionality.feature index d633f622badc6..0889b4e176b43 100644 --- a/grade/report/history/tests/behat/basic_functionality.feature +++ b/grade/report/history/tests/behat/basic_functionality.feature @@ -25,14 +25,12 @@ Feature: A teacher checks the grade history report in a course | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: - | activity | course | section | name | intro | - | assign | C1 | 1 | The greatest assignment ever | Write a behat test for Moodle - it's amazing | - | assign | C1 | 1 | Rewarding assignment | After writing your behat test go grab a beer! | + | activity | course | name | + | assign | C1 | The greatest assignment ever | + | assign | C1 | Rewarding assignment | Given the following config values are set as admin: | showuseridentity | email,profile_field_food | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" And I should see "apple" in the "student1" "table_row" And I should see "orange" in the "student2" "table_row" And I turn editing mode on @@ -41,10 +39,7 @@ Feature: A teacher checks the grade history report in a course And I give the grade "50.00" to the user "Student 2" for the grade item "The greatest assignment ever" And I give the grade "60.00" to the user "Student 2" for the grade item "Rewarding assignment" And I press "Save changes" - And I log out - And I log in as "teacher2" - And I am on "Course 1" course homepage - And I navigate to "View > Grader report" in the course gradebook + And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher2" And I should see "apple" in the "student1" "table_row" And I should see "orange" in the "student2" "table_row" And I turn editing mode on 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 @@ }}