Merge branch 'primary_secondary_nav' of https://github.com/Chocolate-lightning/moodle

This commit is contained in:
Jun Pataleta
2021-08-25 10:42:57 +08:00
117 changed files with 3941 additions and 289 deletions
+6 -1
View File
@@ -46,6 +46,8 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
}
}
$PAGE->has_secondary_navigation_setter(false);
// and finally, if we get here, then there are matching settings and we have to print a form
// to modify them
echo $OUTPUT->header($focus);
@@ -88,7 +90,10 @@ if ($hassiteconfig) {
if ($showsettingslinks) {
$node = $PAGE->settingsnav->find('root', navigation_node::TYPE_SITE_ADMIN);
if ($node) {
echo $OUTPUT->render_from_template('core/settings_link_page', ['node' => $node]);
$moremenu = new \core\navigation\output\more_menu($PAGE->secondarynav, 'nav-tabs');
$secondarynavigation = $moremenu->export_for_template($OUTPUT);
echo $OUTPUT->render_from_template('core/settings_link_page',
['node' => $node, 'secondarynavigation' => $secondarynavigation]);
}
}
@@ -34,7 +34,7 @@ Feature: Verify that keyboard steps work as expected
| username | email | firstname | lastname |
| saffronr | saffron.rutledge@example.com | Saffron | Rutledge |
And I log in as "saffronr"
And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
And I click on "Saffron Rutledge" "button" in the ".usermenu" "css_element"
When I press the up key
Then the focused element is "Log out" "link"
@@ -14,20 +14,20 @@ Feature: Within a moodle instance, an administrator should be able to modify lan
And I press "Open language pack for editing"
And I press "Continue"
And I set the field "Show strings of these components" to "moodle.php"
And I set the field "String identifier" to "administrationsite"
And I set the field "String identifier" to "moodledocslink"
And I press "Show strings"
And I set the field "core/administrationsite" to "Custom string example"
And I set the field "core/moodledocslink" to "moodle documents"
@javascript
Scenario: Edit an string but don't save it to lang pack.
When I press "Apply changes and continue editing"
Then I should see "Site administration" in the "page-header" "region"
And I should not see "Custom string example" in the "page-header" "region"
Then I should see "Help and documentation" in the ".helplink" "css_element"
And I should not see "moodle documents" in the ".helplink" "css_element"
@javascript
Scenario: Customize an string as admin and save it to lang pack.
Given I press "Save changes to the language pack"
And I should see "There are 1 modified strings."
When I click on "Continue" "button"
Then I should see "Custom string example" in the "page-header" "region"
And I should not see "Site administration" in the "page-header" "region"
Then I should see "moodle documents" in the ".helplink" "css_element"
And I should not see "Help and documentation" in the ".helplink" "css_element"
@@ -24,7 +24,7 @@ Feature: Within a moodle instance, an administrator should be able to import mod
And I should see "There are 1 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "An amazing import feature" in the "page-header" "region"
And I should see "An amazing import feature"
@javascript
Scenario: Try to import a PHP file from a non existent component
@@ -46,6 +46,6 @@ Feature: Within a moodle instance, an administrator should be able to import mod
And I should see "There are 3 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"
And I should see "An amazing import feature" in the "page-header" "region"
And I should see "Uploaded custom string"
And I should see "Another Uploaded string"
And I should see "An amazing import feature"
@@ -21,7 +21,7 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I press "Save changes to the language pack"
And I should see "There are 1 modified strings."
And I click on "Continue" "button"
And I should see "Custom string example" in the "page-header" "region"
And I should see "Custom string example"
@javascript
Scenario: Update only customized strings
@@ -36,12 +36,12 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should not see "Custom string example" in the "page-header" "region"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should not see "Another Uploaded string" in the "page-header" "region"
And I should not see "Custom string example"
And I should see "Uploaded custom string"
And I should not see "Another Uploaded string"
@javascript
Scenario: Create only new strings
@@ -56,12 +56,12 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "Custom string example" in the "page-header" "region"
And I should not see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"
And I should see "Custom string example"
And I should not see "Uploaded custom string"
And I should see "Another Uploaded string"
@javascript
Scenario: Import all strings
@@ -76,9 +76,9 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 2 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should not see "Custom string example" in the "page-header" "region"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"
And I should not see "Custom string example"
And I should see "Uploaded custom string"
And I should see "Another Uploaded string"
+1 -1
View File
@@ -56,7 +56,7 @@ Feature: Manage plearning plan
And I set the field "Select cohorts to sync" to "cohort plan"
When I click on "Add cohorts" "button"
Then I should see "2 learning plans were created."
And I follow "Learning plan templates"
And I navigate to "Competencies > Learning plan templates" in site administration
And I click on ".template-userplans" "css_element" in the "Science template cohort" "table_row"
And I should see "Student 1"
And I should see "Student 2"
@@ -149,7 +149,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
And I follow "Learning plans"
When I click on "Send back to draft" of edit menu in the "Test-Plan3" row
And I follow "Test-Plan4"
And I follow "Send back to draft"
@@ -168,7 +167,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
And I follow "Learning plans"
When I click on "Complete this learning plan" of edit menu in the "Test-Plan3" row
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
@@ -190,7 +188,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
And I follow "Learning plans"
When I click on "Reopen this learning plan" of edit menu in the "Test-Plan3" row
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
And I follow "Test-Plan4"
@@ -24,11 +24,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -57,10 +57,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "givenName" "table_row"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -89,10 +89,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "first_name" "table_row"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -126,10 +126,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "username" in the "ocs-data-id" "table_row"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -159,11 +159,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Open Badges" "table_row"
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Open Badges" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Open Badges" "table_row"
And I set the following fields to these values:
| Name | IMS Global |
@@ -194,11 +194,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Google custom" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Google custom" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Google custom" "table_row"
And I set the following fields to these values:
| Name | Google custom modified |
@@ -227,11 +227,11 @@ Feature: Basic OAuth2 functionality
And I should see "-" in the "Invalid custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Invalid custom service" "table_row"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Invalid custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Invalid custom service" "table_row"
And I set the following fields to these values:
| Name | Valid custom service |
@@ -268,11 +268,11 @@ Feature: Basic OAuth2 functionality
And I should see "-" in the "Empty custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Empty custom service" "table_row"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Empty custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Empty custom service" "table_row"
# Check it works as expected too without slash at the end of the service base URL.
And I set the following fields to these values:
+1 -1
View File
@@ -93,7 +93,7 @@ Feature: Manage analytics models
And I click on "View" "link"
And I should see "Log extra info"
And I click on "Close" "button"
And I click on "Analytics models" "link"
And I navigate to "Analytics > Analytics models" in site administration
# Execute scheduled analysis
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Execute scheduled analysis" in the open action menu
+2 -2
View File
@@ -61,7 +61,7 @@ $PAGE->set_context($context);
$PAGE->set_url($currenturl);
$PAGE->set_heading($badge->name);
$PAGE->set_title($badge->name);
$PAGE->navbar->add($badge->name);
$PAGE->navbar->add($badge->name, $currenturl);
$output = $PAGE->get_renderer('core', 'badges');
$msg = optional_param('msg', '', PARAM_TEXT);
@@ -105,4 +105,4 @@ if ($badge->has_criteria()) {
echo $OUTPUT->box(get_string('addcriteriatext', 'badges'));
}
echo $OUTPUT->footer();
echo $OUTPUT->footer();
+3 -3
View File
@@ -44,7 +44,7 @@ Feature: Add badges to the system
And I should see "Issuer details"
And I should see "Test Badge Site"
And I should see "testuser@example.com"
And I follow "Manage badges"
And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I should not see "There are no badges available."
@@ -61,7 +61,7 @@ Feature: Add badges to the system
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I wait until the page is ready
And I follow "Manage badges"
And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I press "Add a new badge"
And I set the following fields to these values:
@@ -157,7 +157,7 @@ Feature: Add badges to the system
And I should see "Related badges (0)"
And I should see "Alignments (0)"
And I should not see "Create badge"
And I follow "Manage badges"
And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I should not see "There are no badges available."
# See buttons from the "Site badges" page.
+7 -8
View File
@@ -21,7 +21,6 @@ Feature: Award badges
And I am on "Course 1" course homepage
# Create course badge 1.
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
| Description | Course badge 1 description |
@@ -38,7 +37,6 @@ Feature: Award badges
# Badge #2
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
| Description | Course badge 2 description |
@@ -57,6 +55,7 @@ Feature: Award badges
# Award course badge 1 to student 1.
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge 1"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
@@ -149,6 +148,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "student 1 (student1@example.com)"
And I press "Award badge"
And I navigate to "Badges > Manage badges" in site administration
When I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -174,7 +174,6 @@ Feature: Award badges
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -191,6 +190,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (2)"
And I log out
@@ -225,7 +225,6 @@ Feature: Award badges
| id_completion | 1 |
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -279,7 +278,6 @@ Feature: Award badges
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
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -328,7 +326,6 @@ Feature: Award badges
And I am on "Course 1" course homepage
# Create course badge 1.
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
| Description | Course badge description |
@@ -347,13 +344,13 @@ Feature: Award badges
# Award course badge 1 to student 1.
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge 1"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
# Add course badge 2.
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
| Description | Course badge description |
@@ -372,6 +369,7 @@ Feature: Award badges
# Award course badge 2 to student 2.
And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
When I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge 2"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
@@ -409,7 +407,6 @@ Feature: Award badges
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -426,6 +423,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (2)"
And I follow "Recipients (2)"
@@ -434,5 +432,6 @@ Feature: Award badges
And I press "Revoke badge"
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
When I press "Revoke badge"
And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (0)"
@@ -37,7 +37,6 @@ Feature: Award badges with separate groups
And I set the field "Group mode" to "Separate groups"
And I press "Save and display"
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -59,7 +58,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
And I set the field "role" to "Non-editing teacher"
@@ -71,6 +69,7 @@ Feature: Award badges with separate groups
# Non-editing teacher can award the badge
And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
And I press "Award badge"
And I follow "Manage badges"
And I follow "Course Badge"
And I should see "Recipients (1)"
And I log out
@@ -85,7 +84,6 @@ Feature: Award badges with separate groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Users > Groups" in current page administration
And I follow "Groups"
And I set the field "groups" to "Class B (2)"
And I press "Add/remove users"
And I set the field "addselect" to "Teacher 2 (teacher2@example.com)"
@@ -94,7 +92,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
And I set the field "role" to "Non-editing teacher"
@@ -112,7 +109,6 @@ Feature: Award badges with separate groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Users > Groups" in current page administration
And I follow "Groups"
And I set the field "groups" to "Class A (2)"
And I press "Add/remove users"
And I set the field "removeselect" to "Teacher 2 (teacher2@example.com)"
@@ -122,7 +118,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
# Teacher 2 shouldn't be able to go further
@@ -36,7 +36,6 @@ Feature: Award badges based on activity completion
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -152,6 +152,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (1)"
And I log out
@@ -203,6 +204,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -259,6 +261,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -317,6 +320,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (1)"
And I log out
@@ -40,7 +40,6 @@ Feature: Award badges based on competency completion
# Add a badge to the course
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -22,7 +22,6 @@ Feature: Test role visibility for the badge administration page
Given I log in as "manager1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -37,7 +36,6 @@ Feature: Test role visibility for the badge administration page
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
+4
View File
@@ -35,6 +35,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check badge details are displayed.
And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
When I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -59,6 +60,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check badge details are displayed.
And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
When I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -107,6 +109,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check "Expires" date is displayed.
And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
And I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -130,6 +133,7 @@ Feature: Display badges
# Wait 1 second to guarantee the badge is expired.
And I wait "1" seconds
# Check "Expired" date is displayed.
And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
And I click on "View issued badge" "link" in the "Student 1" "table_row"
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_block\local\views;
/**
* Class secondary
*
* @package core_block
* @category navigation
*/
class secondary extends \core\navigation\views\secondary {
/**
* Blocks don't require secondary navs as they can be accessed from multiple places and in different contexts.
*/
public function initialise(): void {
}
}
-1
View File
@@ -59,5 +59,4 @@ Feature: Block appearances
| Visible | No |
And I press "Save changes"
And I press "Turn editing off"
And I follow "Test book name"
Then I should not see "Comments"
+8 -6
View File
@@ -33,6 +33,10 @@ Feature: Add cohorts of users
Then the following should exist in the "generaltable" table:
| Name | Cohort ID | Description |
| My new cohort | mynewcohort | My new cohort is cool |
And I should see "Test cohort name"
And I should see "333"
And I should see "Test cohort description"
And I should see "Created manually"
@javascript
Scenario: Add users to a cohort selecting them from the system users list
@@ -76,8 +80,7 @@ Feature: Add cohorts of users
@javascript
Scenario: Add users to a cohort using a bulk user action
When I follow "Accounts"
And I follow "Bulk user actions"
When I navigate to "Users > Accounts > Bulk user actions" in site administration
And I set the field "Available" to "Third User"
And I press "Add to selection"
And I set the field "Available" to "Forth User"
@@ -86,8 +89,7 @@ Feature: Add cohorts of users
And I press "Go"
And I set the field "Cohort" to "Test cohort name [333]"
And I press "Add to cohort"
And I follow "Accounts"
And I follow "Cohorts"
And I navigate to "Users > Accounts > Cohorts" in site administration
Then I should see "2" in the "#cohorts" "css_element"
And I click on "Assign" "link" in the "Test cohort name" "table_row"
And the "Current users" select box should contain "Third User (third@example.com)"
@@ -96,9 +98,9 @@ Feature: Add cohorts of users
@javascript
Scenario: Edit cohort name in-place
When I follow "Cohorts"
When I navigate to "Users > Accounts > Cohorts" in site administration
And I set the field "Edit cohort name" to "Students cohort"
Then I should not see "Test cohort name"
And I should see "Students cohort"
And I follow "Cohorts"
And I navigate to "Users > Accounts > Cohorts" in site administration
And I should see "Students cohort"
+1 -1
View File
@@ -91,7 +91,7 @@ Feature: Restricting access to course lists
And I should see "Biology"
And I should not see "Humanities"
And I click on "Courses" "link" in the "Navigation" "block"
And "category" "text" should not exist in the ".breadcrumb" "css_element"
# And "category" "text" should not exist in the ".breadcrumb" "css_element"
And I should see "Science category"
And I should see "English category"
And I should not see "Other category"
+8 -8
View File
@@ -81,12 +81,12 @@ Feature: Course activity controls works as expected
Examples:
| courseformat | coursedisplay | targetpage | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage |
| topics | 0 | "Course 1" | should | should | "Topic 2" |
| topics | 0 | "C1" | should | should | "Topic 2" |
| topics | 1 | "Topic 1" | should not | should not | "Topic 2" |
| topics | 1 | "Course 1" | should | should not | "Topic 2" |
| weeks | 0 | "Course 1" | should | should | "8 January - 14 January" |
| topics | 1 | "C1" | should | should not | "Topic 2" |
| weeks | 0 | "C1" | should | should | "8 January - 14 January" |
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
| weeks | 1 | "Course 1" | should | should not | "8 January - 14 January" |
| weeks | 1 | "C1" | should | should not | "8 January - 14 January" |
Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected
Given the following "users" exist:
@@ -146,9 +146,9 @@ Feature: Course activity controls works as expected
Examples:
| courseformat | coursedisplay | targetpage | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage |
| topics | 0 | "Course 1" | should | should | "Topic 2" |
| topics | 0 | "C1" | should | should | "Topic 2" |
| topics | 1 | "Topic 1" | should not | should not | "Topic 2" |
| topics | 1 | "Course 1" | should | should not | "Topic 2" |
| weeks | 0 | "Course 1" | should | should | "8 January - 14 January" |
| topics | 1 | "C1" | should | should not | "Topic 2" |
| weeks | 0 | "C1" | should | should | "8 January - 14 January" |
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
| weeks | 1 | "Course 1" | should | should not | "8 January - 14 January" |
| weeks | 1 | "C1" | should | should not | "8 January - 14 January" |
-1
View File
@@ -23,7 +23,6 @@ Feature: Edit course settings
| Course short name | Edited course shortname |
| Course summary | Edited course summary |
And I press "Save and display"
And I follow "Edited course fullname"
Then I should not see "Course 1"
And I should not see "C1"
And I should see "Edited course fullname"
+1 -1
View File
@@ -85,7 +85,7 @@ class behat_grading extends behat_base {
// Step to access the user grade page from the grading page.
$gradetext = get_string('gradeverb');
$this->execute("behat_general::i_click_on_in_the", [$this->escape($activityname), 'link', 'page', 'region']);
$this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname));
$this->execute('behat_navigation::i_navigate_to_in_current_page_administration',
get_string('viewgrading', 'mod_assign'));
+5
View File
@@ -1006,6 +1006,7 @@ $string['hits'] = 'Hits';
$string['hitsoncourse'] = 'Hits on {$a->coursename} by {$a->username}';
$string['hitsoncoursetoday'] = 'Today\'s hits on {$a->coursename} by {$a->username}';
$string['home'] = 'Home';
$string['homeheader'] = 'Home menu';
$string['hour'] = 'hour';
$string['hours'] = 'hours';
$string['howtomakethemes'] = 'How to make new themes';
@@ -1140,6 +1141,7 @@ $string['langltr'] = 'Language direction left-to-right';
$string['langrtl'] = 'Language direction right-to-left';
$string['language'] = 'Language';
$string['languagegood'] = 'This language pack is up-to-date! :-)';
$string['languageselector'] = 'Language selector';
$string['last'] = 'Last';
$string['lastaccess'] = 'Last access';
$string['lastcourseaccess'] = 'Last access to course';
@@ -1350,6 +1352,7 @@ $string['movefilestohere'] = 'Move files to here';
$string['movefull'] = 'Move {$a} to this location';
$string['movehere'] = 'Move to here';
$string['moveleft'] = 'Move left';
$string['moremenu'] = 'More';
$string['moveright'] = 'Move right';
$string['movesection'] = 'Move section {$a}';
$string['moveselectedcategoriesto'] = 'Move selected categories to';
@@ -1735,6 +1738,7 @@ $string['rename'] = 'Rename';
$string['renamefileto'] = 'Rename <b>{$a}</b> to';
$string['report'] = 'Report';
$string['reports'] = 'Reports';
$string['reporttype'] = 'Report type';
$string['repositories'] = 'Repositories';
$string['requestcourse'] = 'Request a course';
$string['requestedby'] = 'Requested by';
@@ -2214,6 +2218,7 @@ $string['userdetails'] = 'User details';
$string['userfiles'] = 'User files';
$string['userlist'] = 'User list';
$string['usermenu'] = 'User menu';
$string['usermenugoback'] = 'Go back to user menu';
$string['username'] = 'Username';
$string['usernameemail'] = 'Username / email';
$string['usernameemailmatch'] = 'The username and email address do not relate to the same user';
+2
View File
@@ -0,0 +1,2 @@
define ("core/menu_navigation",["exports","core/key_codes"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var c={menuitem:"[role=\"menuitem\"]",menu:"[role=\"menu\"]"},d=null,e=function(a,b){if(null!==a){return a}else{return b}},f=function(a){var e=a.srcElement,f=a.currentTarget.firstElementChild,k=j(a.currentTarget);if(e.classList.contains("dropdown-item")){if(a.keyCode===b.arrowRight||a.keyCode===b.arrowLeft){a.preventDefault();if(null!==d){d.parentElement.click()}}if(a.keyCode===b.space||a.keyCode===b.enter){a.preventDefault();Array.prototype.forEach.call(e.closest(".dropdown-menu").children,function(a){a.querySelector(c.menuitem).classList.remove("active");a.setAttribute("aria-current","false")});if(!e.parentElement.classList.contains("dropdown")){e.click()}}}else{if(a.keyCode===b.arrowRight){a.preventDefault();g(e,f)}if(a.keyCode===b.arrowLeft){a.preventDefault();h(e,k)}if(a.keyCode===b.arrowUp||a.keyCode===b.arrowDown){d=e;a.preventDefault()}if(a.keyCode===b.home){a.preventDefault();e.setAttribute("aria-current","false");i(f)}if(a.keyCode===b.end){a.preventDefault();e.currentNode.setAttribute("aria-current","false");i(k)}if(a.keyCode===b.space||a.keyCode===b.enter){a.preventDefault();if(!e.parentElement.classList.contains("dropdown")){e.click()}}}};a.default=function(a){a.removeEventListener("keydown",f);a.addEventListener("keydown",f)};var g=function(a,b){var d=a.parentElement.nextElementSibling,f=e(d,b),g=f.querySelector(c.menuitem);a.setAttribute("aria-current","false");g.setAttribute("aria-current","true");g.focus()},h=function(a,b){var d=a.parentElement.previousElementSibling,f=e(d,b),g=f.querySelector(c.menuitem);a.setAttribute("aria-current","false");g.setAttribute("aria-current","true");g.focus()},i=function(a){a.querySelector(c.menuitem).focus();a.querySelector(c.menuitem).setAttribute("aria-current","true")},j=function(a){var b=a.lastElementChild;if(!b.classList.contains("d-none")){return a.lastElementChild}else{var c=Array.prototype.map.call(a.children,function(a){return a}).reverse(),d=c.filter(function(a){if(!a.classList.contains("d-none")){return a}});if(0!==d.length){return d[0]}else{return a.firstElementChild}}};return a.default});
//# sourceMappingURL=menu_navigation.min.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
define ("core/moremenu",["exports","jquery","core/menu_navigation"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{moredropdown:"[data-region=\"moredropdown\"]",morebutton:"[data-region=\"morebutton\"]"},classes:{dropdownitem:"dropdown-item",dropdownmoremenu:"dropdownmoremenu",dropdowntoggle:"dropdown-toggle",hidden:"d-none",active:"active",nav:"nav",navlink:"nav-link",observed:"observed"},attributes:{menu:"[role=\"menu\"]"}},g=function(a){var b=a.parentNode.offsetHeight+1,c=a.querySelector(f.regions.moredropdown),d=a.querySelector(f.regions.morebutton);if(a.offsetHeight>b){d.classList.remove(f.classes.hidden);var e=Array.from(a.children).reverse();e.forEach(function(c){if(!c.classList.contains(f.classes.dropdownmoremenu)){if(a.offsetHeight>b){var d=a.removeChild(c);h(a,d,!0)}}})}else{if("children"in c){var j=Array.from(c.children);j.forEach(function(d){if(a.offsetHeight<b&&"true"!==d.dataset.forceintomoremenu){var e=c.removeChild(d);i(a,e)}});if(0===j.length){d.classList.add(f.classes.hidden)}}if(a.offsetHeight>b){g(a)}}a.parentNode.classList.add(f.classes.observed)},h=function(a,b){var c=2<arguments.length&&arguments[2]!==void 0?arguments[2]:!1,d=a.querySelector(f.regions.moredropdown),e=a.querySelector("."+f.classes.dropdowntoggle),g=b.querySelector("."+f.classes.navlink);if(g.classList.contains(f.classes.active)){e.classList.add(f.classes.active)}g.classList.remove(f.classes.navlink);g.classList.add(f.classes.dropdownitem);if(c){d.prepend(b)}else{d.append(b)}},i=function(a,b){var c=a.querySelector(f.regions.morebutton),d=a.querySelector("."+f.classes.dropdowntoggle),e=b.querySelector("."+f.classes.dropdownitem);if(e.classList.contains(f.classes.active)){d.classList.remove(f.classes.active)}e.classList.remove(f.classes.dropdownitem);e.classList.add(f.classes.navlink);a.insertBefore(b,c)},j=function(a){a.firstElementChild.querySelector("[role=\"menuitem\"]").setAttribute("tabindex","0");if("children"in a){var d=a.querySelector(f.regions.morebutton),e=Array.from(a.children);e.forEach(function(b){if(!b.classList.contains(f.classes.dropdownmoremenu)&&"true"===b.dataset.forceintomoremenu){h(a,b,!1);if(d.classList.contains(f.classes.hidden)){d.classList.remove(f.classes.hidden)}}})}g(a);(0,c.default)(a);window.addEventListener("resize",function(){g(a);(0,c.default)(a)});var i=function(a){var b=a.target.parentNode.querySelector(f.attributes.menu);if(b){b.classList.toggle("show")}a.stopPropagation()};(0,b.default)("."+f.classes.dropdownmoremenu).on("show.bs.dropdown",function(){var b=a.querySelector(f.regions.moredropdown);b.querySelectorAll(".dropdown").forEach(function(a){a.removeEventListener("click",i,!0);a.addEventListener("click",i,!0)})})};a.default=j;return a.default});
//# sourceMappingURL=moremenu.min.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
define ("core/usermenu",["exports","jquery","core/key_codes"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var d={userMenu:".usermenu",userMenuCarousel:".usermenu #usermenu-carousel",userMenuCarouselItem:".usermenu #usermenu-carousel .carousel-item",userMenuCarouselItemActive:".usermenu #usermenu-carousel .carousel-item.active",userMenuCarouselNavigationLink:".usermenu #usermenu-carousel .carousel-navigation-link"},e=function(){var a=document.querySelector(d.userMenu);(0,b.default)(d.userMenu).on("shown.bs.dropdown",function(){var b=document.querySelector(d.userMenuCarouselItemActive);b.focus();a.querySelectorAll(d.userMenuCarouselItem).forEach(function(a){if(!a.classList.contains("active")){a.style.width=b.offsetWidth+"px";a.style.height=b.offsetHeight+"px"}})});a.addEventListener("click",function(a){if(a.target.matches(d.userMenuCarouselNavigationLink)){f(a)}});a.addEventListener("keydown",function(a){if((a.keyCode===c.space||a.keyCode===c.enter)&&a.target.matches(d.userMenuCarouselNavigationLink)){a.preventDefault();f(a)}});var f=function(c){c.stopPropagation();var e=c.target.dataset.carouselTargetId,f=a.querySelector("#"+e),g=Array.from(f.parentNode.children).indexOf(f);(0,b.default)(d.userMenuCarousel).carousel(g)};(0,b.default)(d.userMenu).on("hide.bs.dropdown",function(){(0,b.default)(d.userMenuCarousel).carousel(0)});(0,b.default)(d.userMenuCarousel).on("slid.bs.carousel",function(){var b=a.querySelector(d.userMenuCarouselItemActive);b.focus()})};a.default={init:function init(){e()}};return a.default});
//# sourceMappingURL=usermenu.min.js.map
File diff suppressed because one or more lines are too long
+201
View File
@@ -0,0 +1,201 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Keyboard initialization for a given html node.
*
* @module core/keyboard_navigation
* @copyright 2021 Moodle
* @author Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {space, enter, arrowRight, arrowLeft, arrowDown, arrowUp, home, end} from 'core/key_codes';
const SELECTORS = {
'menuitem': '[role="menuitem"]',
'menu': '[role="menu"]'
};
let openDropdownNode = null;
/**
* Small helper function to check if a given node is null or not.
*
* @param {HTMLElement|null} item The node that we want to compare.
* @param {HTMLElement} fallback Either the first node or final node that can be focused on.
* @return {HTMLElement}
*/
const clickErrorHandler = (item, fallback) => {
if (item !== null) {
return item;
} else {
return fallback;
}
};
/**
* Defined event handling so we can remove listeners on nodes on resize etc.
*
* @param {event} e The triggering element and key presses etc.
*/
const listenerEvents = e => {
const src = e.srcElement;
const firstNode = e.currentTarget.firstElementChild;
const lastNode = findUsableLastNode(e.currentTarget);
// Handling for dropdown escapes.
// A bulk of the handling is already done by aria.js just add polish.
if (src.classList.contains('dropdown-item')) {
if (e.keyCode === arrowRight ||
e.keyCode === arrowLeft) {
e.preventDefault();
if (openDropdownNode !== null) {
openDropdownNode.parentElement.click();
}
}
if (e.keyCode === space ||
e.keyCode === enter) {
e.preventDefault();
// Remove active class from any other dropdown elements.
Array.prototype.forEach.call(src.closest('.dropdown-menu').children, node => {
node.querySelector(SELECTORS.menuitem).classList.remove('active');
node.setAttribute('aria-current', 'false');
});
if (!src.parentElement.classList.contains('dropdown')) {
src.click();
}
}
} else {
if (e.keyCode === arrowRight) {
e.preventDefault();
setFocusNext(src, firstNode);
}
if (e.keyCode === arrowLeft) {
e.preventDefault();
setFocusPrev(src, lastNode);
}
// Let aria.js handle the dropdowns.
if (e.keyCode === arrowUp ||
e.keyCode === arrowDown) {
openDropdownNode = src;
e.preventDefault();
}
if (e.keyCode === home) {
e.preventDefault();
src.setAttribute('aria-current', 'false');
setFocusHomeEnd(firstNode);
}
if (e.keyCode === end) {
e.preventDefault();
src.currentNode.setAttribute('aria-current', 'false');
setFocusHomeEnd(lastNode);
}
if (e.keyCode === space ||
e.keyCode === enter) {
e.preventDefault();
// Aria.js handles dropdowns etc.
if (!src.parentElement.classList.contains('dropdown')) {
src.click();
}
}
}
};
/**
* The initial entry point that a given module can pass a HTMLElement.
*
* @param {HTMLElement} elementRoot The menu to add handlers upon.
*/
export default elementRoot => {
elementRoot.removeEventListener('keydown', listenerEvents);
elementRoot.addEventListener('keydown', listenerEvents);
};
/**
* Handle the focusing to the next element in the dropdown.
*
* @param {HTMLElement|null} currentNode The node that we want to take action on.
* @param {HTMLElement} firstNode The backup node to focus as a last resort.
*/
const setFocusNext = (currentNode, firstNode) => {
const nextListItem = currentNode.parentElement.nextElementSibling;
const nodeToSelect = clickErrorHandler(nextListItem, firstNode);
const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);
currentNode.setAttribute('aria-current', 'false');
menuItem.setAttribute('aria-current', 'true');
menuItem.focus();
};
/**
* Handle the focusing to the previous element in the dropdown.
*
* @param {HTMLElement|null} currentNode The node that we want to take action on.
* @param {HTMLElement} lastNode The backup node to focus as a last resort.
*/
const setFocusPrev = (currentNode, lastNode) => {
const nextListItem = currentNode.parentElement.previousElementSibling;
const nodeToSelect = clickErrorHandler(nextListItem, lastNode);
const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);
currentNode.setAttribute('aria-current', 'false');
menuItem.setAttribute('aria-current', 'true');
menuItem.focus();
};
/**
* Focus on either the start or end of a nav list.
*
* @param {HTMLElement} node The element to focus on.
*/
const setFocusHomeEnd = node => {
node.querySelector(SELECTORS.menuitem).focus();
node.querySelector(SELECTORS.menuitem).setAttribute('aria-current', 'true');
};
/**
* We need to look within the menu to find a last node we can add focus to.
*
* @param {HTMLElement} elementRoot Menu to find a final child node within.
* @return {HTMLElement}
*/
const findUsableLastNode = elementRoot => {
const lastNode = elementRoot.lastElementChild;
// An example is the more menu existing but hidden on the page for the time being.
if (!lastNode.classList.contains('d-none')) {
return elementRoot.lastElementChild;
} else {
// Cast the HTMLCollection & reverse it.
const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {
return node;
}).reverse();
// Get rid of any nodes we can not set focus on.
const nodesToUse = extractedNodes.filter((node => {
if (!node.classList.contains('d-none')) {
return node;
}
}));
// If we find no elements we can set focus on, fall back to the absolute first element.
if (nodesToUse.length !== 0) {
return nodesToUse[0];
} else {
return elementRoot.firstElementChild;
}
}
};
+215
View File
@@ -0,0 +1,215 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moves wrapping navigation items into a more menu.
*
* @module core/moremenu
* @copyright 2021 Moodle
* @author Bas Brands <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import menu_navigation from "core/menu_navigation";
/**
* Moremenu selectors.
*/
const Selectors = {
regions: {
moredropdown: '[data-region="moredropdown"]',
morebutton: '[data-region="morebutton"]'
},
classes: {
dropdownitem: 'dropdown-item',
dropdownmoremenu: 'dropdownmoremenu',
dropdowntoggle: 'dropdown-toggle',
hidden: 'd-none',
active: 'active',
nav: 'nav',
navlink: 'nav-link',
observed: 'observed',
},
attributes: {
menu: '[role="menu"]'
}
};
/**
* Auto Collapse navigation items that wrap into a dropdown menu.
*
* @param {HTMLElement} menu The navbar container.
*/
const autoCollapse = menu => {
const maxHeight = menu.parentNode.offsetHeight + 1;
const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
const moreButton = menu.querySelector(Selectors.regions.morebutton);
// If the menu items wrap and the menu height is larger than the height of the
// parent then start pushing navlinks into the moreDropdown.
if (menu.offsetHeight > maxHeight) {
moreButton.classList.remove(Selectors.classes.hidden);
const menuNodes = Array.from(menu.children).reverse();
menuNodes.forEach(item => {
if (!item.classList.contains(Selectors.classes.dropdownmoremenu)) {
// After moving the menu items into the moreDropdown check again
// if the menu height is still larger then the height of the parent.
if (menu.offsetHeight > maxHeight) {
const lastNode = menu.removeChild(item);
// Move this node into the more dropdown menu.
moveIntoMoreDropdown(menu, lastNode, true);
}
}
});
} else {
// If the menu height is smaller than the height of the parent, then try returning navlinks to the menu.
if ('children' in moreDropdown) {
const menuNodes = Array.from(moreDropdown.children);
menuNodes.forEach(item => {
// Don't move the node to the more menu if it is explicitly defined that
// this node should be displayed in the more dropdown menu at all times.
if (menu.offsetHeight < maxHeight && item.dataset.forceintomoremenu !== 'true') {
const lastNode = moreDropdown.removeChild(item);
// Move this node from the more dropdown menu into the main section of the menu.
moveOutOfMoreDropdown(menu, lastNode);
}
});
// If there are no more menuNodes in the dropdown we can hide the moreButton.
if (menuNodes.length === 0) {
moreButton.classList.add(Selectors.classes.hidden);
}
}
if (menu.offsetHeight > maxHeight) {
autoCollapse(menu);
}
}
menu.parentNode.classList.add(Selectors.classes.observed);
};
/**
* Move a node into the "more" dropdown menu.
*
* This method forces a given navigation node to be added and displayed within the "more" dropdown menu.
*
* @param {HTMLElement} menu The navbar moremenu.
* @param {HTMLElement} navNode The navigation node.
* @param {boolean} prepend Whether to prepend or append the node to the content in the more dropdown menu.
*/
const moveIntoMoreDropdown = (menu, navNode, prepend = false) => {
const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);
const navLink = navNode.querySelector('.' + Selectors.classes.navlink);
// If there are navLinks that contain an active link in the moreDropdown
// make the dropdownToggle in the moreButton active.
if (navLink.classList.contains(Selectors.classes.active)) {
dropdownToggle.classList.add(Selectors.classes.active);
}
// Change the styling of the navLink to a dropdownitem and push it into
// the moreDropdown.
navLink.classList.remove(Selectors.classes.navlink);
navLink.classList.add(Selectors.classes.dropdownitem);
if (prepend) {
moreDropdown.prepend(navNode);
} else {
moreDropdown.append(navNode);
}
};
/**
* Move a node out of the "more" dropdown menu.
*
* This method forces a given node from the "more" dropdown menu to be displayed in the main section of the menu.
*
* @param {HTMLElement} menu The navbar moremenu.
* @param {HTMLElement} navNode The navigation node.
*/
const moveOutOfMoreDropdown = (menu, navNode) => {
const moreButton = menu.querySelector(Selectors.regions.morebutton);
const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);
const navLink = navNode.querySelector('.' + Selectors.classes.dropdownitem);
// Stop displaying the active state on the dropdownToggle if
// the active navlink is removed.
if (navLink.classList.contains(Selectors.classes.active)) {
dropdownToggle.classList.remove(Selectors.classes.active);
}
navLink.classList.remove(Selectors.classes.dropdownitem);
navLink.classList.add(Selectors.classes.navlink);
menu.insertBefore(navNode, moreButton);
};
/**
* Initialise the more menus.
*
* @param {HTMLElement} menu The navbar moremenu.
*/
export default menu => {
menu.firstElementChild.querySelector('[role="menuitem"]').setAttribute('tabindex', '0');
// Pre-populate the "more" dropdown menu with navigation nodes which are set to be displayed in this menu
// by default at all times.
if ('children' in menu) {
const moreButton = menu.querySelector(Selectors.regions.morebutton);
const menuNodes = Array.from(menu.children);
menuNodes.forEach((item) => {
if (!item.classList.contains(Selectors.classes.dropdownmoremenu) &&
item.dataset.forceintomoremenu === 'true') {
// Append this node into the more dropdown menu.
moveIntoMoreDropdown(menu, item, false);
// After adding the node into the more dropdown menu, make sure that the more dropdown menu button
// is displayed.
if (moreButton.classList.contains(Selectors.classes.hidden)) {
moreButton.classList.remove(Selectors.classes.hidden);
}
}
});
}
// Populate the more dropdown menu with additional nodes if necessary, depending on the current screen size.
autoCollapse(menu);
menu_navigation(menu);
// When the screen size changes make sure the menu still fits.
window.addEventListener('resize', () => {
autoCollapse(menu);
menu_navigation(menu);
});
const toggledropdown = e => {
const innerMenu = e.target.parentNode.querySelector(Selectors.attributes.menu);
if (innerMenu) {
innerMenu.classList.toggle('show');
}
e.stopPropagation();
};
// If there are dropdowns in the MoreMenu, add a new
// event listener to show the contents on click and prevent the
// moreMenu from closing.
$('.' + Selectors.classes.dropdownmoremenu).on('show.bs.dropdown', function() {
const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
moreDropdown.querySelectorAll('.dropdown').forEach((dropdown) => {
dropdown.removeEventListener('click', toggledropdown, true);
dropdown.addEventListener('click', toggledropdown, true);
});
});
};
+127
View File
@@ -0,0 +1,127 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Initializes and handles events in the user menu.
*
* @module core/usermenu
* @copyright 2021 Moodle
* @author Mihail Geshoski <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import {space, enter} from 'core/key_codes';
/**
* User menu constants.
*/
const selectors = {
userMenu: '.usermenu',
userMenuCarousel: '.usermenu #usermenu-carousel',
userMenuCarouselItem: '.usermenu #usermenu-carousel .carousel-item',
userMenuCarouselItemActive: '.usermenu #usermenu-carousel .carousel-item.active',
userMenuCarouselNavigationLink: '.usermenu #usermenu-carousel .carousel-navigation-link',
};
/**
* Register event listeners.
*/
const registerEventListeners = () => {
const userMenu = document.querySelector(selectors.userMenu);
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
$(selectors.userMenu).on('shown.bs.dropdown', () => {
const activeCarouselItem = document.querySelector(selectors.userMenuCarouselItemActive);
// Set the focus on the active carousel item.
activeCarouselItem.focus();
userMenu.querySelectorAll(selectors.userMenuCarouselItem).forEach(element => {
// Resize all non-active carousel items to match the height and width of the current active (main)
// carousel item to avoid sizing inconsistencies. This has to be done once the dropdown menu is fully
// displayed ('shown.bs.dropdown') as the offsetWidth and offsetHeight cannot be obtained when the
// element is hidden.
if (!element.classList.contains('active')) {
element.style.width = activeCarouselItem.offsetWidth + 'px';
element.style.height = activeCarouselItem.offsetHeight + 'px';
}
});
});
// Handle click events in the user menu.
userMenu.addEventListener('click', (e) => {
// Handle click event on the carousel navigation (control) links in the user menu.
if (e.target.matches(selectors.userMenuCarouselNavigationLink)) {
carouselManagement(e);
}
});
userMenu.addEventListener('keydown', e => {
// Handle keydown event on the carousel navigation (control) links in the user menu.
if ((e.keyCode === space ||
e.keyCode === enter) &&
e.target.matches(selectors.userMenuCarouselNavigationLink)) {
e.preventDefault();
carouselManagement(e);
}
});
/**
* We do the same actions here even if the caller was a click or button press.
*
* @param {Event} e The triggering element and key presses etc.
*/
const carouselManagement = e => {
// By default the user menu dropdown element closes on a click event. This behaviour is not desirable
// as we need to be able to navigate through the carousel items (submenus of the user menu) within the
// user menu. Therefore, we need to prevent the propagation of this event and then manually call the
// carousel transition.
e.stopPropagation();
// The id of the targeted carousel item.
const targetedCarouselItemId = e.target.dataset.carouselTargetId;
const targetedCarouselItem = userMenu.querySelector('#' + targetedCarouselItemId);
// Get the position (index) of the targeted carousel item within the parent container element.
const index = Array.from(targetedCarouselItem.parentNode.children).indexOf(targetedCarouselItem);
// Navigate to the targeted carousel item.
$(selectors.userMenuCarousel).carousel(index);
};
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
$(selectors.userMenu).on('hide.bs.dropdown', () => {
// Reset the state once the user menu dropdown is closed and return back to the first (main) carousel item
// if necessary.
$(selectors.userMenuCarousel).carousel(0);
});
// Handle the 'slid.bs.carousel' event (Fired when the carousel has completed its slide transition).
$(selectors.userMenuCarousel).on('slid.bs.carousel', () => {
const activeCarouselItem = userMenu.querySelector(selectors.userMenuCarouselItemActive);
// Set the focus on the newly activated carousel item.
activeCarouselItem.focus();
});
};
/**
* Initialize the user menu.
*/
const init = () => {
registerEventListeners();
};
export default {
init: init,
};
+20
View File
@@ -1520,6 +1520,7 @@ class block_manager {
$deleteurlparams = $this->page->url->params();
$deletepage->set_url($deleteurlbase, $deleteurlparams);
$deletepage->set_block_actions_done();
$deletepage->set_secondarynav($this->get_secondarynav($block));
// At this point we are either going to redirect, or display the form, so
// overwrite global $PAGE ready for this. (Formslib refers to it.)
$PAGE = $deletepage;
@@ -1608,6 +1609,23 @@ class block_manager {
return true;
}
/**
* Convenience function to check whether a block is implementing a secondary nav class and return it
* initialised to the calling function
*
* @param block_base $block
* @return \core\navigation\views\secondary
*/
protected function get_secondarynav(block_base $block): \core\navigation\views\secondary {
$class = "core_block\\local\\views\\secondary";
if (class_exists("block_{$block->name()}\\local\\views\\secondary")) {
$class = "block_{$block->name()}\\local\\views\\secondary";
}
$secondarynav = new $class($this->page);
$secondarynav->initialise();
return $secondarynav;
}
/**
* Handle showing/processing the submission from the block editing form.
* @return boolean true if the form was submitted and the new config saved. Does not
@@ -1635,6 +1653,8 @@ class block_manager {
$editpage->set_course($this->page->course);
//$editpage->set_context($block->context);
$editpage->set_context($this->page->context);
$editpage->set_secondarynav($this->get_secondarynav($block));
if ($this->page->cm) {
$editpage->set_cm($this->page->cm);
}
@@ -0,0 +1,71 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\navigation\output;
use renderable;
use renderer_base;
use templatable;
use custom_menu;
/**
* more menu navigation renderable
*
* @package core
* @category navigation
* @copyright 2021 onwards Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class more_menu implements renderable, templatable {
protected $content;
protected $navbarstyle;
protected $haschildren;
/**
* Constructor for this class.
*
* @param object $content Navigation objects.
* @param string $navbarstyle class name.
* @param bool $haschildren The content has children.
*/
public function __construct(object $content, string $navbarstyle, bool $haschildren = true) {
$this->content = $content;
$this->navbarstyle = $navbarstyle;
$this->haschildren = $haschildren;
}
/**
* Return data for rendering a template.
*
* @param renderer_base $output The output
* @return array Data for rendering a template
*/
public function export_for_template(renderer_base $output): array {
$data = ['navbarstyle' => $this->navbarstyle];
if ($this->haschildren) {
if (!isset($this->content->children) || count($this->content->children) == 0) {
$data = [];
}
$data['nodecollection'] = $this->content;
} else {
$data['nodearray'] = (array) $this->content;
}
return $data;
}
}
+273
View File
@@ -0,0 +1,273 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\navigation\output;
use renderable;
use renderer_base;
use templatable;
use custom_menu;
/**
* Primary navigation renderable
*
* This file combines primary nav, custom menu, lang menu and
* usermenu into a standardized format for the frontend
*
* @package core
* @category navigation
* @copyright 2021 onwards Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class primary implements renderable, templatable {
/** @var moodle_page $page the moodle page that the navigation belongs to */
private $page = null;
/**
* primary constructor.
* @param \moodle_page $page
*/
public function __construct($page) {
$this->page = $page;
}
/**
* Combine the various menus into a standardized output.
*
* @param renderer_base|null $output
* @return array
*/
public function export_for_template(?renderer_base $output = null): array {
if (!$output) {
$output = $this->page->get_renderer('core');
}
$menudata = (object) array_merge($this->get_primary_nav(), $this->get_custom_menu($output));
$moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false);
return [
'moremenu' => $moremenu->export_for_template($output),
'lang' => !isloggedin() || isguestuser() ? $this->get_lang_menu($output) : [],
'user' => $this->get_user_menu($output),
];
}
/**
* Get the primary nav object and standardize the output
*
* @return array
*/
protected function get_primary_nav(): array {
$nodes = [];
foreach ($this->page->primarynav->children as $node) {
$nodes[] = [
'title' => $node->get_title(),
'url' => $node->action(),
'text' => $node->text,
'icon' => $node->icon,
'isactive' => $node->isactive,
];
}
return $nodes;
}
/**
* Custom menu items reside on the same level as the original nodes.
* Fetch and convert the nodes to a standardised array.
*
* @param renderer_base $output
* @return array
*/
protected function get_custom_menu(renderer_base $output): array {
global $CFG;
// Early return if a custom menu does not exists.
if (empty($CFG->custommenuitems)) {
return [];
}
$custommenuitems = $CFG->custommenuitems;
$currentlang = current_language();
$custommenunodes = custom_menu::convert_text_to_menu_nodes($custommenuitems, $currentlang);
$nodes = [];
foreach ($custommenunodes as $node) {
$nodes[] = $node->export_for_template($output);
}
return $nodes;
}
/**
* Get a list of options for the lang picker.
*
* @param renderer_base $output
* @return array
*/
protected function get_lang_menu(renderer_base $output): array {
// Early return if a lang menu does not exists.
if (empty($output->lang_menu())) {
return [];
}
$currentlang = current_language();
$langs = get_string_manager()->get_list_of_translations();
$nodes = [];
$activelanguage = '';
// Add the lang picker if needed.
foreach ($langs as $langtype => $langname) {
$isactive = $langtype == $currentlang;
$node = [
'title' => $langname,
'text' => $langname,
'link' => true,
'isactive' => $isactive,
'url' => $isactive ? new \moodle_url('#') : new \moodle_url($this->page->url, ['lang' => $langtype]),
];
$nodes[] = $node;
if ($isactive) {
$activelanguage = $langname;
}
}
return [
'title' => $activelanguage,
'items' => $nodes,
];
}
/**
* Get/Generate the user menu.
*
* This is leveraging the data from user_get_user_navigation_info and the logic in $OUTPUT->user_menu()
*
* @param renderer_base $output
* @return array
*/
public function get_user_menu(renderer_base $output): array {
global $CFG, $USER, $PAGE;
require_once($CFG->dirroot . '/user/lib.php');
$usermenudata = [];
$submenusdata = [];
$info = user_get_user_navigation_info($USER, $PAGE);
if (isset($info->unauthenticateduser)) {
$info->unauthenticateduser['content'] = get_string($info->unauthenticateduser['content']);
$info->unauthenticateduser['url'] = get_login_url();
return (array) $info;
}
// Gather all the avatar data to be displayed in the user menu.
$usermenudata['avatardata'][] = [
'content' => $info->metadata['useravatar'],
'classes' => 'current'
];
$usermenudata['userfullname'] = $info->metadata['realuserfullname'] ?? $info->metadata['userfullname'];
// Logged in as someone else.
if ($info->metadata['asotheruser']) {
$usermenudata['avatardata'][] = [
'content' => $info->metadata['realuseravatar'],
'classes' => 'realuser'
];
$usermenudata['metadata'][] = [
'content' => get_string('loggedinas', 'moodle', $info->metadata['userfullname']),
'classes' => 'viewingas'
];
}
// Gather all the meta data to be displayed in the user menu.
$metadata = [
'asotherrole' => [
'value' => 'rolename',
'class' => 'role role-##GENERATEDCLASS##',
],
'userloginfail' => [
'value' => 'userloginfail',
'class' => 'loginfailures',
],
'asmnetuser' => [
'value' => 'mnetidprovidername',
'class' => 'mnet mnet-##GENERATEDCLASS##',
],
];
foreach ($metadata as $key => $value) {
if (!empty($info->metadata[$key])) {
$content = $info->metadata[$value['value']] ?? '';
$generatedclass = strtolower(preg_replace('#[ ]+#', '-', trim($content)));
$customclass = str_replace('##GENERATEDCLASS##', $generatedclass, ($value['class'] ?? ''));
$usermenudata['metadata'][] = [
'content' => $content,
'classes' => $customclass
];
}
}
$modifiedarray = array_map(function($value) {
$value->divider = $value->itemtype == 'divider';
$value->link = $value->itemtype == 'link';
if (isset($value->pix) && !empty($value->pix)) {
$value->pixicon = $value->pix;
unset($value->pix);
}
return $value;
}, $info->navitems);
// Include the language menu as a submenu within the user menu.
$langmenu = $this->get_lang_menu($output);
if (!empty($langmenu)) {
$languageitems = $langmenu['items'];
// If there are available languages, generate the data for the the language selector submenu.
if (!empty($languageitems)) {
$langsubmenuid = uniqid();
// Generate the data for the link to language selector submenu.
$language = (object) [
'itemtype' => 'submenu-link',
'submenuid' => $langsubmenuid,
'title' => get_string('language'),
'pixicon' => 'i/language',
'divider' => false,
'submenulink' => true,
];
// Place the link before the 'Log out' menu item which is either the last item in the menu or
// second to last when 'Switch roles' is available.
$menuposition = count($modifiedarray) - 1;
if (has_capability('moodle/role:switchroles', $PAGE->context)) {
$menuposition = count($modifiedarray) - 2;
}
array_splice($modifiedarray, $menuposition, 0, [$language]);
// Generate the data for the language selector submenu.
$submenusdata[] = (object)[
'id' => $langsubmenuid,
'title' => get_string('languageselector'),
'items' => $languageitems,
];
}
}
// Add dividers after the first item and before the last item.
$modifiedarray[0]->divider = true;
$modifiedarray[count($modifiedarray) - 2]->divider = true;
$usermenudata['items'] = $modifiedarray;
$usermenudata['submenus'] = array_values($submenusdata);
return $usermenudata;
}
}
+3 -1
View File
@@ -35,8 +35,10 @@ class primary extends view {
return;
}
$this->id = 'primary_navigation';
$this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM,
if (get_home_page() == HOMEPAGE_SITE && isloggedin() && !isguestuser()) {
$this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM,
null, 'home', new \pix_icon('i/home', ''));
}
// Add the dashboard link.
if (isloggedin() && !isguestuser()) { // Makes no sense if you aren't logged in.
+104 -14
View File
@@ -32,6 +32,10 @@ use navigation_node;
class secondary extends view {
/** @var string $headertitle The header for this particular menu*/
public $headertitle;
/** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
const MAX_DISPLAYED_NAV_NODES = 5;
/**
* Defines the default structure for the secondary nav in a course context.
*
@@ -85,13 +89,13 @@ class secondary extends view {
return [
self::TYPE_SETTING => [
'modedit' => 1,
'roleoverride' => 3,
'rolecheck' => 3.1,
'logreport' => 4,
"mod_{$this->page->activityname}_useroverrides" => 5, // Overrides are module specific.
"mod_{$this->page->activityname}_groupoverrides" => 6,
'roleassign' => 7,
'filtermanage' => 8,
"mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
"mod_{$this->page->activityname}_groupoverrides" => 4,
'roleassign' => 5,
'filtermanage' => 6,
'roleoverride' => 7,
'rolecheck' => 7.1,
'logreport' => 8,
'backup' => 9,
'restore' => 10,
'competencybreakdown' => 11,
@@ -102,6 +106,34 @@ class secondary extends view {
];
}
/**
* Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
*
* @return array
*/
protected function get_default_course_more_menu_nodes(): array {
return [];
}
/**
* Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
*
* @return array
*/
protected function get_default_module_more_menu_nodes(): array {
return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
'competencybreakdown'];
}
/**
* Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
*
* @return array
*/
protected function get_default_admin_more_menu_nodes(): array {
return [];
}
/**
* Initialise the view based navigation based on the current context.
*
@@ -119,23 +151,36 @@ class secondary extends view {
$this->id = 'secondary_navigation';
$context = $this->context;
$this->headertitle = get_string('menu');
$defaultmoremenunodes = [];
switch ($context->contextlevel) {
case CONTEXT_COURSE:
if ($this->page->course->id != $SITE->id) {
$this->headertitle = get_string('courseheader');
$this->load_course_navigation();
$defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
}
break;
case CONTEXT_MODULE:
$this->headertitle = get_string('activityheader');
$this->load_module_navigation();
$defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
break;
case CONTEXT_SYSTEM:
$this->headertitle = get_string('homeheader');
$this->load_admin_navigation();
$defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
break;
}
$this->remove_unwanted_nodes();
// Don't need to show anything if only the view node is available. Remove it.
if ($this->children->count() == 1) {
$this->children->remove('modulepage');
}
$this->force_nodes_into_more_menu($defaultmoremenunodes);
// Search and set the active node.
$this->scan_for_active_node($this);
$this->initialised = true;
@@ -157,14 +202,17 @@ class secondary extends view {
$this->add(get_string('coursepage', 'admin'), $url, self::TYPE_COURSE, null, 'coursehome');
$nodes = $this->get_default_course_mapping();
$nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings']);
$nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation']);
$nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
$nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
$this->add_ordered_nodes($nodesordered);
// All additional nodes will be available under the 'Course admin' page.
$text = get_string('courseadministration');
$url = new \moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
$this->add($text, $url, null, null, 'courseadmin', new \pix_icon('t/edit', $text));
$coursecontext = \context_course::instance($course->id);
if (has_capability('moodle/course:update', $coursecontext)) {
// All additional nodes will be available under the 'Course admin' page.
$text = get_string('courseadministration');
$url = new \moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
$this->add($text, $url, null, null, 'courseadmin', new \pix_icon('t/edit', $text));
}
}
/**
@@ -180,7 +228,12 @@ class secondary extends view {
$nodes = $this->get_default_module_mapping();
if ($mainnode) {
$this->add(get_string('module', 'course'), $this->page->url, null, null, 'modulepage');
$url = new \moodle_url('/mod/' . $this->page->activityname . '/view.php', ['id' => $this->page->cm->id]);
$setactive = $url->compare($this->page->url, URL_MATCH_BASE);
$node = $this->add(get_string('module', 'course'), $url, null, null, 'modulepage');
if ($setactive) {
$node->make_active();
}
// Add the initial nodes.
$nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
$this->add_ordered_nodes($nodesordered);
@@ -269,4 +322,41 @@ class secondary extends view {
}
}
}
/**
* Force certain secondary navigation nodes to be displayed in the "more" menu.
*
* @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
* should be added into the "more" menu by default
*/
protected function force_nodes_into_more_menu(array $defaultmoremenunodes = []) {
// Counter of the navigation nodes that are initially displayed in the secondary nav
// (excludes the nodes from the "more" menu).
$displayednodescount = 0;
foreach ($this->children as $child) {
// Skip if the navigation node has been already forced into the "more" menu.
if ($child->forceintomoremenu) {
continue;
}
// If the navigation node is in the pre-defined list of nodes that should be added by default in the
// "more" menu or the maximum limit of displayed navigation nodes has been reached.
if (in_array($child->key, $defaultmoremenunodes) || $displayednodescount >= self::MAX_DISPLAYED_NAV_NODES) {
// Force the node and its children into the "more" menu.
$child->set_force_into_more_menu(true);
continue;
}
$displayednodescount++;
}
}
/**
* Remove navigation nodes that should not be displayed in the secondary navigation.
*/
protected function remove_unwanted_nodes() {
foreach ($this->children as $child) {
if (!$child->showinsecondarynavigation) {
$child->remove();
}
}
}
}
@@ -345,6 +345,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/warning' => 'fa-exclamation text-warning',
'core:i/window_close' => 'fa-window-close',
'core:i/withsubcat' => 'fa-plus-square',
'core:i/language' => 'fa-language',
'core:m/USD' => 'fa-usd',
'core:t/addcontact' => 'fa-address-card',
'core:t/add' => 'fa-plus',
+1 -1
View File
@@ -61,7 +61,7 @@ class report_helper {
if (!empty($menu)) {
$select = new url_select($menu, $activeurl, null, 'choosecoursereport');
$select->set_label(get_string('report'), ['class' => 'accesshide']);
$select->set_label(get_string('reporttype'), ['class' => 'accesshide']);
$select->attributes['style'] = "margin-bottom: 1.5rem";
$select->class .= " mb-4";
echo $OUTPUT->render($select);
+28
View File
@@ -149,6 +149,10 @@ class navigation_node implements renderable {
public $requiresajaxloading = false;
/** @var bool If set to true this node will be added to the "flat" navigation */
public $showinflatnavigation = false;
/** @var bool If set to true this node will be forced into a "more" menu whenever possible */
public $forceintomoremenu = false;
/** @var bool If set to true this node will be displayed in the "secondary" navigation when applicable */
public $showinsecondarynavigation = true;
/**
* Constructs a new navigation_node
@@ -840,6 +844,30 @@ class navigation_node implements renderable {
return $this->action;
}
/**
* Sets whether the node and its children should be added into a "more" menu whenever possible.
*
* @param bool $forceintomoremenu
*/
public function set_force_into_more_menu(bool $forceintomoremenu = false) {
$this->forceintomoremenu = $forceintomoremenu;
foreach ($this->children as $child) {
$child->set_force_into_more_menu($forceintomoremenu);
}
}
/**
* Sets whether the node and its children should be displayed in the "secondary" navigation when applicable.
*
* @param bool $show
*/
public function set_show_in_secondary_navigation(bool $show = true) {
$this->showinsecondarynavigation = $show;
foreach ($this->children as $child) {
$child->set_show_in_secondary_navigation($show);
}
}
/**
* Add the menu item to handle locking and unlocking of a conext.
*
+7 -1
View File
@@ -3981,6 +3981,10 @@ class context_header implements renderable {
* page => page object. Don't include if the image is an external image.
*/
public $additionalbuttons;
/**
* @var string $prefix A string that is before the title.
*/
public $prefix;
/**
* Constructor.
@@ -3989,8 +3993,9 @@ class context_header implements renderable {
* @param int $headinglevel Main heading 'h' tag level.
* @param string|null $imagedata HTML code for the picture in the page header.
* @param string $additionalbuttons Buttons for the header e.g. Messaging button for the user header.
* @param string $prefix Text that precedes the heading.
*/
public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null) {
public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null, $prefix = null) {
$this->heading = $heading;
$this->headinglevel = $headinglevel;
@@ -4000,6 +4005,7 @@ class context_header implements renderable {
if (isset($this->additionalbuttons)) {
$this->format_button_images();
}
$this->prefix = $prefix;
}
/**
+21 -32
View File
@@ -3321,41 +3321,26 @@ EOD;
$loginpage = $this->is_login_page();
$loginurl = get_login_url();
// If not logged in, show the typical not-logged-in string.
if (!isloggedin()) {
$returnstr = get_string('loggedinnot', 'moodle');
if (!$loginpage) {
$returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
}
return html_writer::div(
html_writer::span(
$returnstr,
'login nav-link'
),
$usermenuclasses
);
}
// If logged in as a guest user, show a string to that effect.
if (isguestuser()) {
$returnstr = get_string('loggedinasguest');
if (!$loginpage && $withlinks) {
$returnstr .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
}
return html_writer::div(
html_writer::span(
$returnstr,
'login nav-link'
),
$usermenuclasses
);
}
// Get some navigation opts.
$opts = user_get_user_navigation_info($user, $this->page);
if (!empty($opts->unauthenticateduser)) {
$returnstr = get_string($opts->unauthenticateduser['content'], 'moodle');
// If not logged in, show the typical not-logged-in string.
if (!$loginpage && (!$opts->unauthenticateduser['guest'] || $withlinks)) {
$returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
}
return html_writer::div(
html_writer::span(
$returnstr,
'login nav-link'
),
$usermenuclasses
);
}
$avatarclasses = "avatars";
$avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
$usertextcontents = $opts->metadata['userfullname'];
@@ -4293,10 +4278,14 @@ EOD;
// Image data.
if (isset($contextheader->imagedata)) {
// Header specific image.
$html .= html_writer::div($contextheader->imagedata, 'page-header-image');
$html .= html_writer::div($contextheader->imagedata, 'page-header-image icon-size-7');
}
// Headings.
if (isset($contextheader->prefix)) {
$prefix = html_writer::div($contextheader->prefix, 'text-muted');
$heading = $prefix . $heading;
}
$html .= html_writer::tag('div', $heading, array('class' => 'page-header-headings'));
// Buttons.
+57 -1
View File
@@ -28,6 +28,7 @@
defined('MOODLE_INTERNAL') || die();
use core\navigation\views\primary;
use core\navigation\views\secondary;
use core\navigation\output\primary as primaryoutput;
/**
* $PAGE is a central store of information about the current page we are
@@ -84,6 +85,7 @@ use core\navigation\views\secondary;
* @property-read secondary $secondarynav The secondary navigation object
* used to display the secondarynav in boost
* @property-read primary $primarynav The primary navigation object used to display the primary nav in boost
* @property-read primaryoutput $primarynavcombined The primary navigation object used to display the primary nav in boost
* @property-read global_navigation $navigation The navigation structure for this page.
* @property-read xhtml_container_stack $opencontainers Tracks XHTML tags on this page that have been opened but not closed.
* mainly for internal use by the rendering code.
@@ -311,6 +313,12 @@ class moodle_page {
*/
protected $_primarynav = null;
/**
* @var primaryoutput Contains the combined nav nodes that will appear
* in the primary navigation. Includes - primarynav, langmenu, usermenu
*/
protected $_primarynavcombined = null;
/**
* @var navbar Contains the navbar structure.
*/
@@ -380,6 +388,11 @@ class moodle_page {
*/
protected $_regionmainsettingsinheader = false;
/**
* @var bool Should the secondary menu be rendered.
*/
protected $_hassecondarynavigation = true;
/**
* Force the settings menu to be displayed on this page. This will only force the
* settings menu on an activity / resource page that is being displayed on a theme that
@@ -806,7 +819,13 @@ class moodle_page {
*/
protected function magic_get_secondarynav() {
if ($this->_secondarynav === null) {
$this->_secondarynav = new secondary($this);
$class = 'core\navigation\views\secondary';
// Try and load a custom class first.
if (class_exists("mod_{$this->activityname}\\local\\views\\secondary")) {
$class = "mod_{$this->activityname}\\local\\views\\secondary";
}
$this->_secondarynav = new $class($this);
$this->_secondarynav->initialise();
}
return $this->_secondarynav;
@@ -824,6 +843,17 @@ class moodle_page {
return $this->_primarynav;
}
/**
* Returns the primary navigation object
* @return primary
*/
protected function magic_get_primarynavcombined() {
if ($this->_primarynavcombined === null) {
$this->_primarynavcombined = new primaryoutput($this);
}
return $this->_primarynavcombined;
}
/**
* Returns request IP address.
*
@@ -1240,6 +1270,14 @@ class moodle_page {
}
}
/**
* Force set secondary_nav. Useful in cases where we dealing with non course modules. e.g. blocks, tools.
* @param secondary $nav
*/
public function set_secondarynav(secondary $nav) {
$this->_secondarynav = $nav;
}
/**
* Adds a CSS class to the body tag of the page.
*
@@ -2133,4 +2171,22 @@ class moodle_page {
public function include_region_main_settings_in_header_actions() : bool {
return $this->_regionmainsettingsinheader;
}
/**
* Set the flag to indicate if the secondary navigation should be rendered.
*
* @param bool $value If the secondary navigation should be rendered.
*/
public function has_secondary_navigation_setter(bool $value) : void {
$this->_hassecondarynavigation = $value;
}
/**
* Check if the secondary navigation should be rendered.
*
* @return bool
*/
public function has_secondary_navigation() : bool {
return $this->_hassecondarynavigation;
}
}
+13 -13
View File
@@ -32,6 +32,19 @@
<div class="col-12 pt-3 pb-3">
<div class="card {{^contextheader}}border-0 bg-transparent{{/contextheader}}">
<div class="card-body {{^contextheader}}p-2{{/contextheader}}">
<div class="d-flex flex-wrap">
{{#hasnavbar}}
<div id="page-navbar">
{{{navbar}}}
</div>
{{/hasnavbar}}
<div class="ml-auto d-flex">
{{{pageheadingbutton}}}
</div>
<div id="course-header">
{{{courseheader}}}
</div>
</div>
<div class="d-sm-flex align-items-center">
{{#contextheader}}
<div class="mr-auto">
@@ -50,19 +63,6 @@
{{/headeractions}}
</div>
</div>
<div class="d-flex flex-wrap">
{{#hasnavbar}}
<div id="page-navbar">
{{{navbar}}}
</div>
{{/hasnavbar}}
<div class="ml-auto d-flex">
{{{pageheadingbutton}}}
</div>
<div id="course-header">
{{{courseheader}}}
</div>
</div>
</div>
</div>
</div>
+70
View File
@@ -0,0 +1,70 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moremenu
The More menu.
Example context (json):
{
"nodecollection": {
"children": [
{
"text": "Home",
"action": "/index.php?redirect=0",
"active": "true"
},
{
"text": "Dashboard",
"action": "/my"
},
{
"text": "Courses",
"action": "/course"
},
{
"text": "Site Administration",
"action": "/admin/search.php"
}
]
}
}
}}
<nav class="moremenu">
<ul id="moremenu-{{ uniqid }}-{{navbarstyle}}" role="menubar" class="nav more-nav {{navbarstyle}}">
{{#nodecollection}}
{{#children}}
{{> core/moremenu_children}}
{{/children}}
{{/nodecollection}}
{{#nodearray}}
{{> core/moremenu_children}}
{{/nodearray}}
<li role="none" class="nav-item dropdown dropdownmoremenu d-none" data-region="morebutton">
<a class="dropdown-toggle nav-link" href="#" id="moremenu-dropdown{{ uniqid }}" role="menuitem" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" tabindex="-1">
{{#str}}moremenu, core{{/str}}
</a>
<ul class="dropdown-menu dropdown-menu-right" data-region="moredropdown" aria-labelledby="moremenu-dropdown{{ uniqid }}" role="menu">
</ul>
</li>
</ul>
</nav>
{{#js}}
require(['core/moremenu'], function(moremenu) {
moremenu(document.querySelector('#moremenu-{{ uniqid }}-{{navbarstyle}}'));
});
{{/js}}
+63
View File
@@ -0,0 +1,63 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moremenu_children
The More menu children
Example context (json):
{
"divider": "",
"haschildren": "",
"uniqid": "Unique string",
"text": "Moodle community",
"children": "",
"title": "Moodle community",
"url": "https://moodle.org"
}
}}
{{#haschildren}}
<li class="dropdown nav-item" role="none" data-forceintomoremenu="{{#forceintomoremenu}}true{{/forceintomoremenu}}{{^forceintomoremenu}}false{{/forceintomoremenu}}">
<a class="dropdown-toggle nav-link" id="drop-down-{{uniqid}}" role="menuitem" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" aria-controls="drop-down-menu-{{uniqid}}" tabindex="-1">
{{{text}}}
</a>
<div class="dropdown-menu" role="menu" id="drop-down-menu-{{uniqid}}" aria-labelledby="drop-down-{{uniqid}}">
{{#children}}
{{^divider}}
<a class="dropdown-item" role="menuitem" href="{{{url}}}">{{{text}}}</a>
{{/divider}}
{{#divider}}
<div class="dropdown-divider"></div>
{{/divider}}
{{/children}}
</div>
</li>
{{/haschildren}}
{{^haschildren}}
<li class="nav-item" role="none" data-forceintomoremenu="{{#forceintomoremenu}}true{{/forceintomoremenu}}{{^forceintomoremenu}}false{{/forceintomoremenu}}">
{{#tab}}
<a role="menuitem" class="nav-link {{#isactive}}active{{/isactive}} {{#classes}}{{.}} {{/classes}}" href="{{tab}}" data-toggle="tab" aria-current="false" tabindex="-1">
{{{text}}}
</a>
{{/tab}}
{{^tab}}
<a role="menuitem" class="nav-link {{#isactive}}active{{/isactive}} {{#classes}}{{.}} {{/classes}}" href="{{{url}}}{{{action}}}" {{#isactive}}aria-current="true"{{/isactive}} tabindex="-1">
{{{text}}}
</a>
{{/tab}}
</li>
{{/haschildren}}
+25 -21
View File
@@ -36,28 +36,32 @@
}
}
}}
<ul class="nav nav-tabs" role="tablist">
<!-- First the top most node and immediate children -->
<li class="nav-item">
<a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab" aria-selected="true">{{node.text}}</a>
</li>
<!-- Now the first level children with sub nodes -->
{{#node.children}}
{{#children.count}}
{{#display}}
{{^is_short_branch}}
<li class="nav-item">
<a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab" aria-selected="false" tabindex="-1">{{text}}</a>
</li>
{{/is_short_branch}}
{{/display}}
{{/children.count}}
{{/node.children}}
</ul>
{{#secondarynavigation}}
{{> core/moremenu}}
{{/secondarynavigation}}
{{^secondarynavigation}}
<ul class="nav nav-tabs" role="tablist">
<!-- First the top most node and immediate children -->
<li class="nav-item">
<a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab" aria-selected="true">{{node.text}}</a>
</li>
<!-- Now the first level children with sub nodes -->
{{#node.children}}
{{#children.count}}
{{#display}}
{{^is_short_branch}}
<li class="nav-item">
<a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab" aria-selected="false" tabindex="-1">{{text}}</a>
</li>
{{/is_short_branch}}
{{/display}}
{{/children.count}}
{{/node.children}}
</ul>
{{/secondarynavigation}}
<div class="tab-content mt-3">
<div class="tab-pane active" id="link{{node.key}}" role="tabpanel">
<div class="tab-pane active" id="link{{node.key}}" {{^secondarynavigation}}role="tabpanel"{{/secondarynavigation}}>
<div class="container">
<div class="row">
<div class="col-sm-3">
@@ -101,7 +105,7 @@
</div>
{{#node.children}}
{{#children.count}}
<div class="tab-pane" id="link{{key}}" role="tabpanel">
<div class="tab-pane" id="link{{key}}" {{^secondarynavigation}}role="tabpanel"{{/secondarynavigation}}>
<div class="container">
<div class="row">
<div class="col-sm-3">
@@ -0,0 +1,84 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/user_action_menu_items
Template for user action menu items.
Context variables required for this template:
* items - The different items to be rendered
* link - If a link is provided render it.
* title - The text to be shown for the link.
* url - The href for the link.
* pixicon - (Optional) The Moodle icon to use
* imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
* submenulink - If a submenu link is provided render it.
* submenuid - The id of the targeted submenu.
* title - The text to be shown for the link.
* pixicon - (Optional) The Moodle icon to use.
* imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
* divider - Whether a divider is to be displayed or not
Example context (json):
{
"items": [
{
"link": {
"title": "Github user",
"url": "https://raw.githubusercontent.com/",
"pixicon": "t/dashboard",
"imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"
},
"divider": 1
},
{
"submenulink": {
"title": "Title",
"submenuid": "86cebd87",
"pixicon": "t/dashboard",
"imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"
},
"divider": 1
}
]
}
}}
{{#items}}
{{#link}}
<a href="{{{url}}}" class="dropdown-item" role="menuitem">
{{#pixicon}}
{{#pix}}{{pixicon}}{{/pix}}
{{/pixicon}}
{{^pixicon}}
{{#imgsrc}}<img aria-hidden="true" src="{{imgsrc}}" alt="{{title}}"/>{{/imgsrc}}
{{/pixicon}}
{{title}}
</a>
{{/link}}
{{#submenulink}}
<a href="#" class="carousel-navigation-link dropdown-item" role="menuitem" data-carousel-target-id="carousel-item-{{submenuid}}">
{{#pixicon}}
{{#pix}}{{pixicon}}{{/pix}}
{{/pixicon}}
{{^pixicon}}
{{#imgsrc}}<img aria-hidden="true" src="{{imgsrc}}" alt="{{title}}"/>{{/imgsrc}}
{{/pixicon}}
{{title}}
</a>
{{/submenulink}}
{{#divider}}<div class="dropdown-divider"></div>{{/divider}}
{{/items}}
@@ -0,0 +1,48 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/user_action_menu_submenus
Template for the submenus in the user action menu.
Context variables required for this template:
* items - The submenu items
* link - If a link is provided render it.
* title - The title added to the link.
* text - The text to be shown for the link.
* url - The href for the link.
* isactive - (Optional) Whether the item is currently active (has been selected).
Example context (json):
{
"items": {
"link": {
"title": "Submenu item 1",
"text": "Submenu item 1",
"url": "https://example.com/",
"isactive": 0
}
}
}
}}
{{#items}}
{{#link}}
<a href="{{{url}}}" class="dropdown-item pl-5" role="menuitem" {{#isactive}}aria-current="true"{{/isactive}}>
{{text}}
</a>
{{/link}}
{{/items}}
+105
View File
@@ -0,0 +1,105 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/user_menu
Action link template.
Context variables required for this template:
* unauthenticateduseruser - (Optional) Items to be displayed if an an unautheticated user is accessing the site
* content - The content to be displayed in the header.
* url - The login url
* items - Array of user menu items used in user_action_menu_items. Required if the above not provided.
* metadata - Array of additional metadata to be displayed in the dropdown button.
* avatardata - Array of avatars to be displayed. Usually only the current user's avatar. If viewing as another user,
includes that user's avatar.
* userfullname - The name of the logged in user
* submenus - Array of submenus within the user menu.
* id - The id of the submenu.
* title - The title of the submenu.
* items - Array of the submenu items used in core/user_action_menu_submenu_items.
Example context (json):
{
"unauthenticateduser": {
"content": "You are not logged in",
"url": "https://yourmoodlesite/login/index.php"
},
"items": [],
"metadata": [],
"avatardata": [],
"userfullname": "Admin User",
"submenus": [
{
"id": "86cebd87",
"title": "Submenu title",
"items": []
}
]
}
}}
<div class="usermenu">
{{#unauthenticateduser}}
<span class="login pl-2">
{{content}}
{{#url}}
(<a href="{{url}}">{{#str}} login, core {{/str}}</a>)
{{/url}}
</span>
{{/unauthenticateduser}}
{{^unauthenticateduser}}
<div class="dropdown show">
<button type="button" id="user-menu-toggle" data-toggle="dropdown" aria-label="{{#str}}usermenu{{/str}}"
aria-haspopup="true" aria-controls="user-action-menu" class="btn dropdown-toggle">
<span class="userbutton">
{{> core/user_menu_metadata }}
</span>
</button>
<div id="user-action-menu" class="dropdown-menu dropdown-menu-right">
<div id="usermenu-carousel" class="carousel slide" data-touch="false" data-interval="false" data-keyboard="false">
<div class="carousel-inner">
<div id="carousel-item-main" class="carousel-item active" role="menu" tabindex="-1" aria-label="{{#str}}user{{/str}}">
{{> core/user_action_menu_items }}
</div>
{{#submenus}}
<div id="carousel-item-{{id}}" class="carousel-item submenu" tabindex="-1" aria-label="{{title}}">
<div class="d-flex flex-column h-100">
<div class="header">
<button type="#" class="btn btn-icon carousel-navigation-link text-decoration-none text-body" data-carousel-target-id="carousel-item-main" aria-label="{{#str}}usermenugoback{{/str}}">
<span class="dir-rtl-hide">{{#pix}}i/arrow-left{{/pix}}</span>
<span class="dir-ltr-hide">{{#pix}}i/arrow-right{{/pix}}</span>
</button>
<span class="pl-2" id="carousel-item-title-{{id}}">{{title}}</span>
</div>
<div class="dropdown-divider"></div>
<div class="items h-100 overflow-auto" role="menu" aria-labelledby="carousel-item-title-{{id}}">
{{> core/user_action_menu_submenu_items }}
</div>
</div>
</div>
{{/submenus}}
</div>
</div>
</div>
</div>
{{/unauthenticateduser}}
</div>
{{#js}}
require(['core/usermenu'], function(UserMenu) {
UserMenu.init();
});
{{/js}}
+63
View File
@@ -0,0 +1,63 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/user_menu_metadata
User menu metadata template.
Context variables required for this template:
* metadata - Array of additional metadata to be displayed in the dropdown button.
* classes - Additional classes to be applied to the content
* content - The content to be displayed. May/may not have html within it.
* avatardata - Array of avatars to be displayed. Usually only the current user's avatar. If viewing as another user,
includes that user's avatar.
* classes - Additional classes to be applied to the content
* content - The content to be displayed. May/may not have html within it.
* userfullname - The name of the logged in user
Example context (json):
{
"metadata": [
{
"classes": "loginfailures",
"content": "<span>1</span> failed login attempts"
}
],
"avatardata": [
{
"classes": "current",
"content": "<img src=\"http://localhost/stable_master/theme/image.php/boost/core/1617863836/u/f2\" class=\"userpicture defaultuserpic\">"
}
],
"userfullname": "Admin User"
}
}}
<span class="usertext mr-1">
{{userfullname}}
{{#metadata}}
<span class="meta {{classes}}">
{{{content}}}
</span>
{{/metadata}}
</span>
<span class="avatars">
{{#avatardata}}
<span class="avatar {{classes}}">
{{{content}}}
</span>
{{/avatardata}}
</span>
+130 -2
View File
@@ -41,6 +41,20 @@ use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
*/
class behat_navigation extends behat_base {
/**
* Checks whether a navigation node is active within the block navigation.
*
* @Given i should see :name is active in navigation
*
* @throws ElementNotFoundException
* @param string $element The name of the nav elemnent to look for.
* @return void
*/
public function i_should_see_is_active_in_navigation($element) {
$this->execute("behat_general::assert_element_contains_text",
[$element, '.block_navigation .active_tree_node', 'css_element']);
}
/**
* Helper function to get a navigation nodes text element given its text from within the navigation block.
*
@@ -151,7 +165,7 @@ class behat_navigation extends behat_base {
if ($this->running_javascript()) {
// The user menu must be expanded when JS is enabled.
$xpath = "//div[contains(concat(' ', @class, ' '), ' usermenu ')]//a[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]";
$xpath = "//div[contains(concat(' ', @class, ' '), ' usermenu ')]//button[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]";
$this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
}
@@ -925,7 +939,14 @@ class behat_navigation extends behat_base {
if ($parentnodes) {
$tabname = behat_context_helper::escape($parentnodes[0]);
$tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) {
$menubarxpath = '//ul[@role=\'menubar\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
$linkname = behat_context_helper::escape(get_string('moremenu'));
$menubarmorexpath = '//ul[@role=\'menubar\']/li/a[contains(normalize-space(.), ' . $linkname . ')]';
$tabnode = $this->getSession()->getPage()->find('xpath', $tabxpath);
$menunode = $this->getSession()->getPage()->find('xpath', $menubarxpath);
$menubuttons = $this->getSession()->getPage()->findAll('xpath', $menubarmorexpath);
if ($tabnode || $menunode) {
$node = is_object($tabnode) ? $tabnode : $menunode;
if ($this->running_javascript()) {
$this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
// Click on the tab and add 'active' tab to the xpath.
@@ -936,6 +957,22 @@ class behat_navigation extends behat_base {
$xpath .= '//div[@id = ' . $tabid . ']';
}
array_shift($parentnodes);
} else if (count($menubuttons) > 0) {
try {
$menubuttons[0]->isVisible();
try {
$this->execute('behat_general::i_click_on', [$menubuttons[1], 'NodeElement']);
} catch (Exception $e) {
$this->execute('behat_general::i_click_on', [$menubuttons[0], 'NodeElement']);
}
$moreitemxpath = '//ul[@data-region=\'moredropdown\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
if ($morenode = $this->getSession()->getPage()->find('xpath', $moreitemxpath)) {
$this->execute('behat_general::i_click_on', [$morenode, 'NodeElement']);
$xpath .= '//div[contains(@class,\'active\')]';
array_shift($parentnodes);
}
} catch (Exception $e) {
}
}
}
@@ -1065,4 +1102,95 @@ class behat_navigation extends behat_base {
}
$this->execute('behat_general::i_visit', [$url]);
}
/**
* First checks to see if we are on this page via the breadcrumb. If not we then attempt to follow the link name given.
*
* @param string $pagename Name of the breadcrumb item to check and follow.
*/
public function go_to_breadcrumb_location(string $pagename): void {
$link = $this->getSession()->getPage()->find(
'xpath',
"//nav[@aria-label='Navigation bar']/ol/li[last()][contains(normalize-space(.), '" . $pagename . "')]"
);
if (!$link) {
$this->execute("behat_general::click_link", $pagename);
}
}
/**
* Checks whether an item exists in the user menu.
*
* @Given :itemtext :selectortype should exist in the user menu
* @Given :itemtext :selectortype should :not exist in the user menu
*
* @throws ElementNotFoundException
* @param string $itemtext The menu item to find
* @param string $selectortype The selector type
* @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
* @return void
*/
public function should_exist_in_user_menu($itemtext, $selectortype, $not = null) {
$callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the';
$this->execute("behat_general::{$callfunction}",
[$itemtext, $selectortype, $this->get_user_menu_xpath(), 'xpath_element']);
}
/**
* Checks whether an item exists in a given user submenu.
*
* @Given :itemtext :selectortype should exist in the :submenuname user submenu
* @Given :itemtext :selectortype should :not exist in the :submenuname user submenu
*
* @throws ElementNotFoundException
* @param string $itemtext The submenu item to find
* @param string $selectortype The selector type
* @param string $submenuname The name of the submenu
* @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
* @return void
*/
public function should_exist_in_user_submenu($itemtext, $selectortype, $submenuname, $not = null) {
$callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the';
$this->execute("behat_general::{$callfunction}",
[$itemtext, $selectortype, $this->get_user_submenu_xpath($submenuname), 'xpath_element']);
}
/**
* Checks whether a given user submenu is visible.
*
* @Then /^I should see "(?P<submenu_string>[^"]*)" user submenu$/
*
* @throws ElementNotFoundException
* @throws ExpectationException
* @param string $submenuname The name of the submenu
* @return void
*/
public function i_should_see_user_submenu($submenuname) {
$this->execute('behat_general::should_be_visible',
array($this->get_user_submenu_xpath($submenuname), 'xpath_element'));
}
/**
* Return the xpath for the user menu element.
*
* @return string The xpath
*/
protected function get_user_menu_xpath() {
return "//div[contains(concat(' ', @class, ' '), ' usermenu ')]" .
"//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]" .
"//div[@id='carousel-item-main']";
}
/**
* Return the xpath for a given user submenu element.
*
* @param string $submenuname The name of the submenu
* @return string The xpath
*/
protected function get_user_submenu_xpath($submenuname) {
return "//div[contains(concat(' ', @class, ' '), ' usermenu ')]" .
"//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]" .
"//div[contains(concat(' ', @class, ' '), ' submenu ')][@aria-label='" . $submenuname . "']";
}
}
@@ -0,0 +1,413 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\navigation\output;
use ReflectionMethod;
/**
* Primary navigation renderable test
*
* @package core
* @category navigation
* @copyright 2021 onwards Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class primary_test extends \advanced_testcase {
/**
* Basic setup to make sure the nav objects gets generated without any issues.
*/
public function setUp(): void {
global $PAGE;
$this->resetAfterTest();
$pagecourse = $this->getDataGenerator()->create_course();
$assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
$cm = get_coursemodule_from_id('assign', $assign->cmid);
$contextrecord = \context_module::instance($cm->id);
$pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]);
$PAGE->set_cm($cm);
$PAGE->set_url($pageurl);
$PAGE->set_course($pagecourse);
$PAGE->set_context($contextrecord);
}
/**
* Test the primary export to confirm we are getting the nodes
*
* @dataProvider test_primary_export_provider
* @param bool $withcustom Setup with custom menu
* @param bool $withlang Setup with langs
* @param string $userloggedin The type of user ('admin' or 'guest') if creating setup with logged in user,
* otherwise consider the user as non-logged in
* @param array $expecteditems An array of nodes expected with content in them.
*/
public function test_primary_export(bool $withcustom, bool $withlang, string $userloggedin, array $expecteditems) {
global $PAGE, $CFG;
if ($withcustom) {
$CFG->custommenuitems = "Course search|/course/search.php
Google|https://google.com.au/
Netflix|https://netflix.com/au";
}
if ($userloggedin === 'admin') {
$this->setAdminUser();
} else if ($userloggedin === 'guest') {
$this->setGuestUser();
} else {
$this->setUser(0);
}
// Mimic multiple langs installed. To trigger responses 'get_list_of_translations'.
// Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this.
// We are testing whether the nodes gets generated when the lang menu is available.
if ($withlang) {
mkdir("$CFG->dataroot/lang/de", 0777, true);
mkdir("$CFG->dataroot/lang/fr", 0777, true);
// Ensure the new langs are picked up and not taken from the cache.
$stringmanager = get_string_manager();
$stringmanager->reset_caches(true);
}
$primary = new primary($PAGE);
$renderer = $PAGE->get_renderer('core');
$data = array_filter($primary->export_for_template($renderer));
// Assert that the number of returned menu items equals the expected result.
$this->assertCount(count($expecteditems), $data);
// Assert that returned menu items match the expected items.
foreach ($data as $menutype => $value) {
$this->assertTrue(in_array($menutype, $expecteditems));
}
// When the user is logged in (excluding guest access), assert that lang menu is included as a part of the
// user menu when multiple languages are installed.
if (isloggedin() && !isguestuser()) {
// Look for a language menu item within the user menu items.
$usermenulang = array_filter($data['user']['items'], function($usermenuitem) {
return $usermenuitem->title === get_string('language');
});
if ($withlang) { // If multiple languages are installed.
// Assert that the language menu exists within the user menu.
$this->assertNotEmpty($usermenulang);
} else { // If the aren't any additional installed languages.
$this->assertEmpty($usermenulang);
}
} else { // Otherwise assert that the user menu does not contain any items.
$this->assertArrayNotHasKey('items', $data['user']);
}
}
/**
* Provider for the test_primary_export function.
*
* @return array
*/
public function test_primary_export_provider(): array {
return [
"Export the menu data when: custom menu exists; multiple langs installed; user is not logged in." => [
true, true, '', ['moremenu', 'lang', 'user']
],
"Export the menu data when: custom menu exists; langs not installed; user is not logged in." => [
true, false, '', ['moremenu', 'user']
],
"Export the menu data when: custom menu exists; multiple langs installed; logged in as admin." => [
true, true, 'admin', ['moremenu', 'user']
],
"Export the menu data when: custom menu exists; langs not installed; logged in as admin." => [
true, false, 'admin', ['moremenu', 'user']
],
"Export the menu data when: custom menu exists; multiple langs installed; logged in as guest." => [
true, true, 'guest', ['moremenu', 'lang', 'user']
],
"Export the menu data when: custom menu exists; langs not installed; logged in as guest." => [
true, false, 'guest', ['moremenu', 'user']
],
"Export the menu data when: custom menu does not exist; multiple langs installed; logged in as guest." => [
false, true, 'guest', ['moremenu', 'lang', 'user']
],
"Export the menu data when: custom menu does not exist; multiple langs installed; logged in as admin." => [
false, true, 'admin', ['moremenu', 'user']
],
"Export the menu data when: custom menu does not exist; langs not installed; user is not logged in." => [
false, false, '', ['moremenu', 'user']
],
];
}
/**
* Test the get_lang_menu
*
* @dataProvider get_lang_menu_provider
* @param bool $withadditionallangs
* @param string $language
* @param array $expected
*/
public function test_get_lang_menu(bool $withadditionallangs, string $language, array $expected) {
global $CFG, $PAGE;
// Mimic multiple langs installed. To trigger responses 'get_list_of_translations'.
// Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this.
// We are testing whether the nodes gets generated when the lang menu is available.
if ($withadditionallangs) {
mkdir("$CFG->dataroot/lang/de", 0777, true);
mkdir("$CFG->dataroot/lang/fr", 0777, true);
// Ensure the new langs are picked up and not taken from the cache.
$stringmanager = get_string_manager();
$stringmanager->reset_caches(true);
}
force_current_language($language);
$output = new primary($PAGE);
$method = new ReflectionMethod('core\navigation\output\primary', 'get_lang_menu');
$method->setAccessible(true);
$renderer = $PAGE->get_renderer('core');
$response = $method->invoke($output, $renderer);
if ($withadditionallangs) { // If there are multiple languages installed.
// Assert that the title of the language menu matches the expected one.
$this->assertEquals($expected['title'], $response['title']);
// Assert that the number of language menu items matches the number of the expected items.
$this->assertEquals(count($expected['items']), count($response['items']));
foreach ($expected['items'] as $expecteditem) {
// We need to manually generate the url key and its value in the expected item array as this cannot
// be done in the data provider due to the change of the state of $PAGE.
$expecteditem['url'] = $expecteditem['isactive'] ? new \moodle_url('#') :
new \moodle_url($PAGE->url, ['lang' => $expecteditem['lang']]);
// The lang value is only used to generate the url, so this key can be removed.
unset($expecteditem['lang']);
// Assert that the given expected item exists in the returned items.
$this->assertTrue(in_array($expecteditem, $response['items']));
}
} else { // No multiple languages.
$this->assertEquals($expected, $response);
}
}
/**
* Provider for test_get_lang_menu
*
* @return array
*/
public function get_lang_menu_provider(): array {
return [
'Lang menu with only the current language' => [
false, 'en', []
],
'Lang menu with only multiple languages installed' => [
true, 'en', [
'title' => 'English (en)',
'items' => [
[
'title' => 'English (en)',
'text' => 'English (en)',
'link' => true,
'isactive' => true,
'lang' => 'en'
],
[
'title' => 'English (de)',
'text' => 'English (de)',
'link' => true,
'isactive' => false,
'lang' => 'de'
],
[
'title' => 'English (fr)',
'text' => 'English (fr)',
'link' => true,
'isactive' => false,
'lang' => 'fr'
],
],
],
],
'Lang menu with only multiple languages installed and other than EN set active.' => [
true, 'de', [
'title' => 'English (de)',
'items' => [
[
'title' => 'English (en)',
'text' => 'English (en)',
'link' => true,
'isactive' => false,
'lang' => 'en'
],
[
'title' => 'English (de)',
'text' => 'English (de)',
'link' => true,
'isactive' => true,
'lang' => 'de'
],
[
'title' => 'English (fr)',
'text' => 'English (fr)',
'link' => true,
'isactive' => false,
'lang' => 'fr'
],
],
],
],
];
}
/**
* Test the custom menu getter to confirm the nodes gets generated and are returned correctly.
*
* @dataProvider custom_menu_provider
* @param string $config
* @param array $expected
*/
public function test_get_custom_menu(string $config, array $expected) {
global $CFG, $PAGE;
$CFG->custommenuitems = $config;
$output = new primary($PAGE);
$method = new ReflectionMethod('core\navigation\output\primary', 'get_custom_menu');
$method->setAccessible(true);
$renderer = $PAGE->get_renderer('core');
$this->assertEquals($expected, $method->invoke($output, $renderer));
}
/**
* Provider for test_get_custom_menu
*
* @return array
*/
public function custom_menu_provider(): array {
return [
'Simple custom menu' => [
"Course search|/course/search.php
Google|https://google.com.au/
Netflix|https://netflix.com/au", [
(object) [
'text' => 'Course search',
'url' => 'https://www.example.com/moodle/course/search.php',
'title' => '',
'sort' => 1,
'children' => [],
'haschildren' => false,
],
(object) [
'text' => 'Google',
'url' => 'https://google.com.au/',
'title' => '',
'sort' => 2,
'children' => [],
'haschildren' => false,
],
(object) [
'text' => 'Netflix',
'url' => 'https://netflix.com/au',
'title' => '',
'sort' => 3,
'children' => [],
'haschildren' => false,
],
]
],
'Complex, nested custom menu' => [
"Moodle community|http://moodle.org
-Moodle free support|http://moodle.org/support
-Moodle development|http://moodle.org/development
--Moodle Tracker|http://tracker.moodle.org
--Moodle Docs|https://docs.moodle.org
-Moodle News|http://moodle.org/news
Moodle company
-Moodle commercial hosting|http://moodle.com/hosting
-Moodle commercial support|http://moodle.com/support", [
(object) [
'text' => 'Moodle community',
'url' => 'http://moodle.org',
'title' => '',
'sort' => 1,
'children' => [
(object) [
'text' => 'Moodle free support',
'url' => 'http://moodle.org/support',
'title' => '',
'sort' => 2,
'children' => [],
'haschildren' => false,
],
(object) [
'text' => 'Moodle development',
'url' => 'http://moodle.org/development',
'title' => '',
'sort' => 3,
'children' => [
(object) [
'text' => 'Moodle Tracker',
'url' => 'http://tracker.moodle.org',
'title' => '',
'sort' => 4,
'children' => [],
'haschildren' => false,
],
(object) [
'text' => 'Moodle Docs',
'url' => 'https://docs.moodle.org',
'title' => '',
'sort' => 5,
'children' => [],
'haschildren' => false,
],
],
'haschildren' => true,
],
(object) [
'text' => 'Moodle News',
'url' => 'http://moodle.org/news',
'title' => '',
'sort' => 6,
'children' => [],
'haschildren' => false,
],
],
'haschildren' => true,
],
(object) [
'text' => 'Moodle company',
'url' => null,
'title' => '',
'sort' => 7,
'children' => [
(object) [
'text' => 'Moodle commercial hosting',
'url' => 'http://moodle.com/hosting',
'title' => '',
'sort' => 8,
'children' => [],
'haschildren' => false,
],
(object) [
'text' => 'Moodle commercial support',
'url' => 'http://moodle.com/support',
'title' => '',
'sort' => 9,
'children' => [],
'haschildren' => false,
],
],
'haschildren' => true,
],
]
]
];
}
}
+3 -3
View File
@@ -58,9 +58,9 @@ class primary_test extends \advanced_testcase {
*/
public function test_setting_initialise_provider() {
return [
'Testing as a guest user' => ['guest', ['home', 'courses']],
'Testing as an admin' => ['admin', ['home', 'myhome', 'courses', 'siteadminnode']],
'Testing as a regular user' => ['user', ['home', 'myhome', 'courses']]
'Testing as a guest user' => ['guest', ['courses']],
'Testing as an admin' => ['admin', ['myhome', 'courses', 'siteadminnode']],
'Testing as a regular user' => ['user', ['myhome', 'courses']]
];
}
}
+126 -2
View File
@@ -106,7 +106,7 @@ class secondary_test extends \advanced_testcase {
$assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
$cm = get_coursemodule_from_id('assign', $assign->cmid);
$contextrecord = \context_module::instance($cm->id);
$pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]);
$pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->id]);
$PAGE->set_cm($cm);
break;
case 'system':
@@ -135,7 +135,131 @@ class secondary_test extends \advanced_testcase {
return [
'Testing in a course context' => ['course', 'coursehome', 'courseheader', 'Course page'],
'Testing in a module context' => ['module', 'modulepage', 'activityheader', 'Activity'],
'Testing in a site admin' => ['system', 'siteadminnode', 'menu', 'Site administration'],
'Testing in a site admin' => ['system', 'siteadminnode', 'homeheader', 'Site administration'],
];
}
/**
* Test the force_nodes_into_more_menu method.
*
* @param array $secondarynavnodesdata The array which contains the data used to generate the secondary navigation
* @param array $defaultmoremenunodes The array containing the keys of the navigation nodes which should be added
* to the "more" menu by default
* @param array $expecedmoremenunodes The array containing the keys of the expected navigation nodes which are
* forced into the "more" menu
* @dataProvider test_force_nodes_into_more_menu_provider
*/
public function test_force_nodes_into_more_menu(array $secondarynavnodesdata, array $defaultmoremenunodes,
array $expecedmoremenunodes) {
global $PAGE;
// Create a dummy secondary navigation.
$secondary = new secondary($PAGE);
foreach ($secondarynavnodesdata as $nodedata) {
$secondary->add($nodedata['text'], '#', secondary::TYPE_SETTING, null, $nodedata['key']);
}
$method = new ReflectionMethod('core\navigation\views\secondary', 'force_nodes_into_more_menu');
$method->setAccessible(true);
$method->invoke($secondary, $defaultmoremenunodes);
$actualmoremenunodes = [];
foreach ($secondary->children as $node) {
if ($node->forceintomoremenu) {
$actualmoremenunodes[] = $node->key;
}
}
// Assert that the actual nodes forced into the "more" menu matches the expected ones.
$this->assertEquals($expecedmoremenunodes, $actualmoremenunodes);
}
/**
* Data provider for the test_force_nodes_into_more_menu function.
*
* @return array
*/
public function test_force_nodes_into_more_menu_provider(): array {
return [
'The total number of navigation nodes exceeds the max display limit (5); ' .
'navnode2 and navnode4 are forced into "more" menu by default.' =>
[
[
[ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
[ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
[ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
[ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
[ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
[ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
[ 'text' => 'Navigation node 7', 'key' => 'navnode7'],
[ 'text' => 'Navigation node 8', 'key' => 'navnode8'],
[ 'text' => 'Navigation node 9', 'key' => 'navnode9'],
],
[
'navnode2',
'navnode4',
],
[
'navnode2',
'navnode4',
'navnode8',
'navnode9',
],
],
'The total number of navigation nodes does not exceed the max display limit (5); ' .
'navnode2 and navnode4 are forced into "more" menu by default.' =>
[
[
[ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
[ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
[ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
[ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
[ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
],
[
'navnode2',
'navnode4',
],
[
'navnode2',
'navnode4',
],
],
'The total number of navigation nodes exceeds the max display limit (5); ' .
'no forced navigation nodes into "more" menu by default.' =>
[
[
[ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
[ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
[ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
[ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
[ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
[ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
[ 'text' => 'Navigation node 7', 'key' => 'navnode7'],
[ 'text' => 'Navigation node 8', 'key' => 'navnode8'],
],
[],
[
'navnode6',
'navnode7',
'navnode8',
],
],
'The total number of navigation nodes does not exceed the max display limit (5); ' .
'no forced navigation nodes into "more" menu by default.' =>
[
[
[ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
[ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
[ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
[ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
[ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
[ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
],
[],
[
'navnode6',
],
],
];
}
}
+52
View File
@@ -545,6 +545,58 @@ class core_navigationlib_testcase extends advanced_testcase {
// Test it's empty again!
$this->assertEquals(0, count($navigationnodecollection->get_key_list()));
}
/**
* Test the set_force_into_more_menu method.
*
* @param bool $haschildren Whether the navigation node has children nodes
* @param bool $forceintomoremenu Whether to force the navigation node and its children into the "more" menu
* @dataProvider test_set_force_into_more_menu_provider
*/
public function test_set_force_into_more_menu(bool $haschildren, bool $forceintomoremenu) {
// Create a navigation node.
$node = new navigation_node(['text' => 'Navigation node', 'key' => 'navnode']);
// If required, add some children nodes to the navigation node.
if ($haschildren) {
for ($i = 1; $i <= 3; $i++) {
$node->add("Child navigation node {$i}");
}
}
$node->set_force_into_more_menu($forceintomoremenu);
// Assert that the expected value has been assigned to the 'forceintomoremenu' property
// in the navigation node and its children.
$this->assertEquals($forceintomoremenu, $node->forceintomoremenu);
foreach ($node->children as $child) {
$this->assertEquals($forceintomoremenu, $child->forceintomoremenu);
}
}
/**
* Data provider for the test_set_force_into_more_menu function.
*
* @return array
*/
public function test_set_force_into_more_menu_provider(): array {
return [
'Navigation node without any children nodes; Force into "more" menu => true.' =>
[
false,
true,
],
'Navigation node with children nodes; Force into "more" menu => true.' =>
[
true,
true,
],
'Navigation node with children nodes; Force into "more" menu => false.' =>
[
true,
false,
],
];
}
}
@@ -0,0 +1,44 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_assign\local\views;
use core\navigation\views\secondary as core_secondary;
/**
* Class secondary_navigation_view.
*
* Custom implementation for a plugin.
*
* @package mod_assign
* @category navigation
* @copyright 2021 onwards Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class secondary extends core_secondary {
protected function get_default_module_mapping(): array {
return [
self::TYPE_SETTING => [
'modedit' => 1,
"mod_{$this->page->activityname}_useroverrides" => 2, // Overrides are module specific.
"mod_{$this->page->activityname}_groupoverrides" => 3,
],
self::TYPE_CUSTOM => [
'advgrading' => 4,
],
];
}
}
+6 -5
View File
@@ -4564,18 +4564,19 @@ class assign {
$gradingoptionsform->set_data($gradingoptionsdata);
$actionformtext = $this->get_renderer()->render($gradingactions);
$currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
$header = new assign_header($this->get_instance(),
$this->get_context(),
false,
$this->get_course_module()->id,
get_string('grading', 'assign'),
$actionformtext);
$actionformtext,
'',
$currenturl);
$o .= $this->get_renderer()->render($header);
$currenturl = $CFG->wwwroot .
'/mod/assign/view.php?id=' .
$this->get_course_module()->id .
'&action=grading';
$o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
+5 -1
View File
@@ -671,6 +671,8 @@ class assign_header implements renderable {
public $preface = '';
/** @var string $postfix optional postfix (text to show after the intro) */
public $postfix = '';
/** @var moodle_url $subpageurl link for the subpage */
public $subpageurl = null;
/**
* Constructor
@@ -688,7 +690,8 @@ class assign_header implements renderable {
$coursemoduleid,
$subpage='',
$preface='',
$postfix='') {
$postfix='',
moodle_url $subpageurl = null) {
$this->assign = $assign;
$this->context = $context;
$this->showintro = $showintro;
@@ -696,6 +699,7 @@ class assign_header implements renderable {
$this->subpage = $subpage;
$this->preface = $preface;
$this->postfix = $postfix;
$this->subpageurl = $subpageurl;
}
}
+1 -1
View File
@@ -233,7 +233,7 @@ class mod_assign_renderer extends plugin_renderer_base {
$o = '';
if ($header->subpage) {
$this->page->navbar->add($header->subpage);
$this->page->navbar->add($header->subpage, $header->subpageurl);
$args = ['contextname' => $header->context->get_context_name(false, true), 'subpage' => $header->subpage];
$title = get_string('subpagetitle', 'assign', $args);
} else {
+1 -1
View File
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
$plugin->version = 2021052502; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2021052503; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2021052500; // Requires this Moodle version.
@@ -51,7 +51,8 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving chapters down rearranges them properly
Given I click on "Move chapter down \"1. Originally first chapter\"" "link"
When I follow "Test book"
When I am on "Course 1" course homepage
And I follow "Test book"
Then I should see "1. A great second chapter"
And I should see "#2 chapter content"
And I should see "1.1. Second chapter, subchapter 1"
@@ -61,7 +62,8 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving chapters up rearranges them properly
Given I click on "Move chapter up \"3. There aren't 2 without 3\"" "link"
When I follow "Test book"
When I am on "Course 1" course homepage
And I follow "Test book"
Then I should see "1. Originally first chapter"
And I should see "#1 chapter content"
And I should see "2. There aren't 2 without 3"
@@ -71,30 +73,26 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving subchapters down within chapter rearranges them properly
Given I click on "Move chapter down \"2.1. Second chapter, subchapter 1\"" "link"
When I follow "Test book"
Then I should see "2.1. Second chapter, subchapter 2"
And I should see "2.2. Second chapter, subchapter 1"
When I should see "2.1. Second chapter, subchapter 2"
Then I should see "2.2. Second chapter, subchapter 1"
Scenario: Moving subchapters down out of chapter rearranges them properly
Given I click on "Move chapter down \"2.2. Second chapter, subchapter 2\"" "link"
When I follow "Test book"
Then I should see "3.1. Second chapter, subchapter 2"
And I click on "Move chapter down \"3. There aren't 2 without 3\"" "link"
When I should see "3.1. Second chapter, subchapter 2"
Then I click on "Move chapter down \"3. There aren't 2 without 3\"" "link"
And I should not see "4. There aren't 2 without 3"
And I should see "3. There aren't 2 without 3"
And I should see "3.1. Second chapter, subchapter 2"
Scenario: Moving subchapters up within chapter rearranges them properly
Given I click on "Move chapter up \"2.2. Second chapter, subchapter 2\"" "link"
When I follow "Test book"
Then I should see "2.1. Second chapter, subchapter 2"
And I should see "2.2. Second chapter, subchapter 1"
When I should see "2.1. Second chapter, subchapter 2"
Then I should see "2.2. Second chapter, subchapter 1"
Scenario: Moving subchapters up out of chapter rearranges them properly
Given I click on "Move chapter up \"2.1. Second chapter, subchapter 1\"" "link"
When I follow "Test book"
Then I should see "1.1. Second chapter, subchapter 1"
And I click on "Move chapter up \"1.1. Second chapter, subchapter 1\"" "link"
When I should see "1.1. Second chapter, subchapter 1"
Then I click on "Move chapter up \"1.1. Second chapter, subchapter 1\"" "link"
And I should not see "1.1. Second chapter, subchapter 1"
And I should see "1. Second chapter, subchapter 1"
And I should see "2. Originally first chapter"
+1 -1
View File
@@ -46,7 +46,7 @@ class behat_mod_choice extends behat_base {
* @return array
*/
public function I_choose_option_from_activity($option, $choiceactivity) {
$this->execute("behat_navigation::i_am_on_page_instance", [$this->escape($choiceactivity), 'choice activity']);
$this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($choiceactivity), 'choice activity']);
$this->execute('behat_forms::i_set_the_field_to', array( $this->escape($option), 1));
+1 -3
View File
@@ -57,7 +57,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I should see "this is a multiple choice 1"
And I press "Save changes"
And I follow "Edit questions"
And I should see "this is a multiple choice 1"
# Make sure this template is not available in another course
And I am on the "Learning experience course 2" "feedback activity" page
@@ -85,7 +84,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I set the field "Append new items" to "1"
And I press "Save changes"
And I follow "Edit questions"
Then "What is your favourite subject" "text" should appear before "this is a multiple choice 1" "text"
# Import template replacing items
And I follow "Templates"
@@ -93,6 +91,7 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I set the field "Delete old items" to "1"
And I press "Save changes"
And I am on the "Another feedback in course 1" "feedback activity" page
And I follow "Edit questions"
And I should not see "What is your favourite subject"
And I should see "this is a multiple choice 1"
@@ -110,7 +109,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
Then I should see "this is a multiple choice 1"
And I press "Save changes"
And I follow "Edit questions"
And I should see "this is a multiple choice 1"
Scenario: Teacher can delete course templates but can not delete public templates
+4 -2
View File
@@ -139,8 +139,10 @@ $strpage = get_string("page");
if (!$search || $showform) {
$PAGE->navbar->add($strforums, new moodle_url('/mod/forum/index.php', array('id'=>$course->id)));
$PAGE->navbar->add(get_string('advancedsearch', 'forum'));
$url = new moodle_url('/mod/forum/index.php', array('id' => $course->id));
$PAGE->navbar->add($strforums, $url);
$url = new moodle_url('/mod/forum/search.php', array('id' => $course->id));
$PAGE->navbar->add(get_string('advancedsearch', 'forum'), $url);
$PAGE->set_title($strsearch);
$PAGE->set_heading($course->fullname);
+1 -1
View File
@@ -97,7 +97,7 @@ if (data_submitted()) {
}
$strsubscribers = get_string("subscribers", "forum");
$PAGE->navbar->add($strsubscribers);
$PAGE->navbar->add($strsubscribers, $url);
$PAGE->set_title($strsubscribers);
$PAGE->set_heading($COURSE->fullname);
if (has_capability('mod/forum:managesubscriptions', $context) && \mod_forum\subscriptions::is_forcesubscribed($forum) === false) {
+3 -3
View File
@@ -113,7 +113,7 @@ class behat_mod_forum extends behat_base {
*/
public function i_reply_post_from_forum_using_an_inpage_reply_with($postsubject, $forumname, TableNode $table) {
// Navigate to forum.
$this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $this->escape($postsubject));
$this->execute('behat_general::click_link', get_string('reply', 'forum'));
@@ -132,7 +132,7 @@ class behat_mod_forum extends behat_base {
*/
public function i_navigate_to_post_in_forum($postsubject, $forumname) {
// Navigate to forum discussion.
$this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $this->escape($postsubject));
}
@@ -493,7 +493,7 @@ class behat_mod_forum extends behat_base {
*/
protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) {
// Navigate to forum.
$this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $buttonstr);
$this->fill_new_discussion_form($table);
}
@@ -341,7 +341,6 @@ Feature: A user can control their own subscription preferences for a discussion
And I follow "You are not subscribed to this discussion. Click to subscribe"
And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row"
And I follow "Test forum name"
And I navigate to "Subscribe to this forum" in current page administration
And I should see "Student One will be notified of new posts in 'Test forum name'"
And "Unsubscribe from this forum" "link" should exist in current page administration
@@ -57,6 +57,7 @@ Feature: Teachers can edit or delete any forum post
And I should see "Edited by Teacher 1 - original submission"
Scenario: A student can't edit or delete another user's posts
When I follow "Teacher post subject"
When I follow "Test forum name"
And I follow "Teacher post subject"
Then I should not see "Edit" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
And I should not see "Delete" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
@@ -36,7 +36,8 @@ Feature: Forum posts can be replied to in private
| Reply privately | 1 |
Scenario: As a teacher I can see my own response
Given I follow "Answers to the homework"
Given I follow "Study discussions"
And I follow "Answers to the homework"
Then I should see "How about you and I have a meeting after class about plagiarism?"
Scenario: As a fellow teacher I can see the other teacher's response
@@ -61,7 +61,7 @@ class behat_mod_glossary extends behat_base {
*/
public function i_add_a_glossary_entries_category_named($categoryname) {
$this->execute("behat_general::click_link", get_string('categoryview', 'mod_glossary'));
$this->execute("behat_navigation::go_to_breadcrumb_location", get_string('categoryview', 'mod_glossary'));
$this->execute("behat_forms::press_button", get_string('editcategories', 'mod_glossary'));
@@ -43,7 +43,7 @@ Feature: A teacher can choose whether glossary entries require approval
And I follow "Waiting approval"
Then I should see "(this entry is currently hidden)"
And I follow "Approve"
And I click on "Test glossary name" "link" in the "page-header" "region"
And I am on the "Test glossary name" "glossary activity" page
Then I should see "Concept definition"
And I log out
# Check that the entry can now be viewed by students.
@@ -89,6 +89,7 @@ Feature: In a lesson activity a student should
And I should see "Second page contents"
And I press "Next page"
And I should see "Third page contents"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -98,6 +99,7 @@ Feature: In a lesson activity a student should
And I wait "1" seconds
And I press "Next page"
And I should see "Paper is made from trees."
And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -108,6 +110,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "Kermit is a frog"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -165,6 +168,7 @@ Feature: In a lesson activity a student should
And I wait "1" seconds
And I press "Next page"
And I should see "Third page contents"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
Then I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -286,6 +290,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "2+2=4"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
Then I should see "Do you want to start at the last page you saw?"
@@ -298,6 +303,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "Second content page"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -399,6 +405,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "2+2=4"
And I am on "Course 1" course homepage
And I follow "Test lesson name"
Then I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
+1 -2
View File
@@ -27,13 +27,12 @@ Feature: Restoring Moodle 2 backup restores LTI configuration
And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration
And "This tool has not yet been used" "text" should exist in the "//div[contains(@id,'tool-card-container') and contains(., 'My site tool')]" "xpath_element"
And I am on site homepage
And I follow "Course 1"
And I am on "Course 1" course homepage
And I turn editing mode on
And I add a "External tool" to section "1" and I fill the form with:
| Activity name | My LTI module |
| Preconfigured tool | My site tool |
| Launch container | Embed |
And I follow "Course 1"
And I should see "My LTI module"
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
@@ -54,6 +54,7 @@ Feature: View activity completion in the quiz activity
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
And I am on "Course 1" course homepage
And I follow "Test quiz name"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And the "Make attempts: 1" completion condition of "Test quiz name" is displayed as "done"
+1 -1
View File
@@ -46,7 +46,7 @@ require_capability('mod/workshop:allocate', $context);
$PAGE->set_title($workshop->name);
$PAGE->set_heading($course->fullname);
$PAGE->navbar->add(get_string('allocation', 'workshop'));
$PAGE->navbar->add(get_string('allocation', 'workshop'), $workshop->allocation_url($method));
$allocator = $workshop->allocator_instance($method);
$initresult = $allocator->init();
@@ -87,7 +87,7 @@ class behat_workshopallocation_manual extends behat_base {
* @param TableNode $table should have one column with title 'Reviewer' and another with title 'Participant' (or 'Reviewee')
*/
public function i_allocate_submissions_in_workshop_as($workshopname, TableNode $table) {
$this->execute("behat_general::i_click_on_in_the", [$this->escape($workshopname), 'link', 'page', 'region']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_navigation::i_navigate_to_in_current_page_administration', get_string('allocate', 'workshop'));
$rows = $table->getRows();
$reviewer = $participant = null;
@@ -52,7 +52,7 @@ class behat_mod_workshop extends behat_base {
$xpath = "//*[@class='userplan']/descendant::div[./span[contains(.,$phaseliteral)]]";
$continue = $this->escape(get_string('continue'));
$this->execute("behat_general::i_click_on_in_the", [$this->escape($workshopname), 'link', 'page', 'region']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_general::i_click_on_in_the',
array('a.action-icon', "css_element", $this->escape($xpath), "xpath_element")
@@ -73,7 +73,7 @@ class behat_mod_workshop extends behat_base {
$savechanges = $this->escape(get_string('savechanges'));
$xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ownsubmission ')]/descendant::*[@type='submit']";
$this->execute("behat_navigation::i_am_on_page_instance", [$this->escape($workshopname), 'workshop activity']);
$this->execute("behat_navigation::i_am_on_page_instance", [$workshopname, 'workshop activity']);
$this->execute("behat_general::i_click_on", array($xpath, "xpath_element"));
@@ -116,7 +116,7 @@ class behat_mod_workshop extends behat_base {
$assess = $this->escape(get_string('assess', 'workshop'));
$saveandclose = $this->escape(get_string('saveandclose', 'workshop'));
$this->execute("behat_navigation::i_am_on_page_instance", [$workshopname, 'workshop activity']);
$this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_general::i_click_on_in_the',
array($assess, "button", $xpath, "xpath_element")
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>

After

Width:  |  Height:  |  Size: 312 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

After

Width:  |  Height:  |  Size: 314 B

+16 -16
View File
@@ -35,14 +35,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I am on "Course 1" course homepage
And I reload the page
And I reload the page
And I reload the page
# We want to view this multiple times, to make sure the count is working.
And I am on "Course 1" course homepage
And I follow "URL name"
And I follow "URL name"
And I follow "URL name"
And I reload the page
And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
@@ -63,14 +63,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I reload the page
And I reload the page
And I reload the page
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "URL name"
And I follow "URL name"
And I follow "URL name"
And I reload the page
And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
@@ -93,14 +93,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I follow "Folder name"
And I reload the page
And I reload the page
And I reload the page
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "URL name"
And I follow "URL name"
And I follow "URL name"
And I reload the page
And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
+3 -2
View File
@@ -78,7 +78,8 @@ if ($ctx && ($context = context::instance_by_id($ctx, IGNORE_MISSING)) && $conte
$tagcollid = $tag->tagcollid;
$PAGE->set_url($tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec));
$pageurl = $tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec);
$PAGE->set_url($pageurl);
$PAGE->set_subpage($tag->id);
$tagnode = $PAGE->navigation->find('tags', null);
$tagnode->make_active();
@@ -97,7 +98,7 @@ if ($PAGE->user_allowed_editing()) {
$buttons .= $OUTPUT->edit_button(clone($PAGE->url));
}
$PAGE->navbar->add($tagname);
$PAGE->navbar->add($tagname, $pageurl);
$PAGE->set_title(get_string('tag', 'tag') .' - '. $tag->get_display_name());
$PAGE->set_heading($COURSE->fullname);
$PAGE->set_button($buttons);
+1 -1
View File
@@ -86,7 +86,7 @@ Feature: Managers can create and manage tag collections
And I should see "Tag3"
And I should not see "Tag1"
And I should not see "Tag2"
And I follow "Manage tags"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
# Tag "Swimming" was not standard and was moved completely.
And I should not see "Swimming"
+2
View File
@@ -61,6 +61,7 @@ Feature: Manager is able to delete tags
And I press "Yes"
And I should see "Tag(s) deleted"
And I should not see "Dog"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should not see "Dog"
And I follow "Cat"
@@ -94,6 +95,7 @@ Feature: Manager is able to delete tags
And I should see "Tag(s) deleted"
And I should not see "Dog"
And I should not see "Neverusedtag"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should not see "Dog"
And I should not see "Neverusedtag"
+3 -3
View File
@@ -123,7 +123,6 @@ Feature: Users can edit tags to add description or rename
| Related tags | Dog, Turtle,Fish |
| Standard | 0 |
And I press "Update"
Then "Default collection" "link" should exist in the ".breadcrumb" "css_element"
And I follow "Kitten"
And "Description of tag 1" "text" should exist in the ".tag-description" "css_element"
And I should see "Related tags:" in the ".tag_list" "css_element"
@@ -143,12 +142,10 @@ Feature: Users can edit tags to add description or rename
And I set the following fields to these values:
| Tag name | Kitten |
And I press "Update"
Then "Default collection" "text" should exist in the ".breadcrumb" "css_element"
And I click on "Edit this tag" "link" in the "Kitten" "table_row"
And I set the following fields to these values:
| Tag name | KITTEN |
And I press "Update"
And "Default collection" "text" should exist in the ".breadcrumb" "css_element"
And I should see "KITTEN"
And I should not see "Kitten"
@@ -161,6 +158,7 @@ Feature: Users can edit tags to add description or rename
And I set the field "Edit tag name" in the "Cat" "table_row" to "Kitten"
Then I should not see "Cat"
And "New name for tag" "field" should not exist
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Kitten"
And I should not see "Cat"
@@ -172,6 +170,7 @@ Feature: Users can edit tags to add description or rename
And I should see "Turtle"
And I should see "Dog"
And I should not see "DOG"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Turtle"
And I should see "Dog"
@@ -183,6 +182,7 @@ Feature: Users can edit tags to add description or rename
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should not see "Penguin"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Turtle"
And I should not see "Penguin"
+1
View File
@@ -78,6 +78,7 @@ Feature: Users can flag tags and manager can reset flags
And "(1)" "text" should exist in the "//tr[contains(.,'Nicetag')]//td[contains(@class,'col-flag')]" "xpath_element"
And "(" "text" should not exist in the "//tr[contains(.,'Badtag')]//td[contains(@class,'col-flag')]" "xpath_element"
And "(" "text" should not exist in the "//tr[contains(.,'Neverusedtag')]//td[contains(@class,'col-flag')]" "xpath_element"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And "Nicetag" "link" should appear before "Sweartag" "link"
And "Sweartag" "link" should appear before "Badtag" "link"
+1
View File
@@ -63,6 +63,7 @@ Feature: Manager can add standard tags and change the tag type of existing tags
And "Remove from standard tags" "link" should exist in the "Tag1" "table_row"
And "Make standard" "link" should exist in the "Tag2" "table_row"
And "Make standard" "link" should exist in the "Tag3" "table_row"
And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And "Make standard" "link" should exist in the "Tag0" "table_row"
And "Remove from standard tags" "link" should exist in the "Tag1" "table_row"
+180
View File
@@ -0,0 +1,180 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace theme_boost;
/**
* Creates a navbar for boost that allows easy control of the navbar items.
*
* @package theme_boost
* @copyright 2021 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class boostnavbar implements \renderable {
/** @var array The individual items of the navbar. */
protected $items = [];
/**
* Takes a navbar object and picks the necessary parts for display.
*
* @param \navbar $navbar The navigation bar.
*/
public function __construct(\navbar $navbar) {
foreach ($navbar->get_items() as $item) {
$this->items[] = $item;
}
$this->prepare_nodes_for_boost();
}
/**
* Prepares the navigation nodes for use with boost.
*/
protected function prepare_nodes_for_boost(): void {
// Don't display the navbar if we are in the site navigation.
if (!is_null($this->get_item('root'))) {
$this->clear_items();
return;
}
$this->remove('myhome'); // Dashboard.
$this->remove('home');
// Set the designated one path for courses.
$mycoursesnode = $this->get_item('mycourses');
if (!is_null($mycoursesnode)) {
// TODO: Once MDL-70801 lands point this to the new page.
$url = new \moodle_url('/course/');
$mycoursesnode->action = $url;
$mycoursesnode->text = get_string('courses');
}
$this->remove_no_link_items();
// Don't display the navbar if there is only one item. Apparently this is bad UX design.
if ($this->item_count() <= 1) {
$this->clear_items();
return;
}
// Make sure that the last item is not a link. Not sure if this is always a good idea.
$this->remove_last_item_action();
}
/**
* Get all the boostnavbaritem elements.
*
* @return boostnavbaritem[] Boost navbar items.
*/
public function get_items(): array {
return $this->items;
}
/**
* Removes all navigation items out of this boost navbar
*/
protected function clear_items(): void {
$this->items = [];
}
/**
* Retrieve a single navbar item.
*
* @param string|int $key The identifier of the navbar item to return.
* @return \breadcrumb_navigation_node|null The navbar item.
*/
protected function get_item($key): ?\breadcrumb_navigation_node {
foreach ($this->items as $item) {
if ($key === $item->key) {
return $item;
}
}
return null;
}
/**
* Counts all of the navbar items.
*
* @return int How many navbar items there are.
*/
protected function item_count(): int {
return count($this->items);
}
/**
* Remove a boostnavbaritem from the boost navbar.
*
* @param string|int $itemkey An identifier for the boostnavbaritem
*/
protected function remove($itemkey): void {
$itemfound = false;
foreach ($this->items as $key => $item) {
if ($item->key === $itemkey) {
unset($this->items[$key]);
$itemfound = true;
break;
}
}
if (!$itemfound) {
return;
}
$itemcount = $this->item_count();
if ($itemcount <= 0) {
return;
}
$this->items = array_values($this->items);
// Set the last item to last item if it is not.
$lastitem = $this->items[$itemcount - 1];
if (!$lastitem->is_last()) {
$lastitem->set_last(true);
}
}
/**
* Removes the action from the last item of the boostnavbaritem.
*/
protected function remove_last_item_action(): void {
$item = end($this->items);
$item->action = null;
reset($this->items);
}
/**
* Returns the second last navbar item. This is for use in the mobile view where we are showing just the second
* last item in the breadcrumb navbar.
*
* @return breakcrumb_navigation_node|null The second last navigation node.
*/
public function get_penultimate_item(): ?\breadcrumb_navigation_node {
$number = $this->item_count() - 2;
return ($number >= 0) ? $this->items[$number] : null;
}
/**
* Remove items that are categories or have no actions associated with them.
*/
protected function remove_no_link_items(): void {
foreach ($this->items as $key => $value) {
if (!$value->has_action() || $value->type == \navigation_node::TYPE_SECTION) {
unset($this->items[$key]);
}
}
$this->items = array_values($this->items);
}
}
@@ -0,0 +1,239 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace theme_boost\output;
use moodle_url;
use get_string;
defined('MOODLE_INTERNAL') || die;
/**
* The context header for the boost theme.
*
* @package theme_boost
* @copyright 2021 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class context_header implements \renderable, \templatable {
/** \moodle_page This current page information. */
protected $page;
/** array Header data. */
protected $headerinfo;
/** int What level the header should be i.e. 1 = <h1>. */
protected $headinglevel;
/**
* Constructs a new instance.
*
* @param \moodle_page $page This current page information.
* @param array $headerinfo Header data.
* @param int $headinglevel What level the header should be i.e. 1 = <h1>.
*/
public function __construct(\moodle_page $page, array $headerinfo = null, int $headinglevel = 1) {
$this->page = $page;
$this->headerinfo = $headerinfo;
$this->headinglevel = $headinglevel;
}
/**
* Adds an array element for a formatted image.
*
* @param array $buttons Buttons to format.
* @return array The formatted buttons.
*/
protected function format_button_images(array $buttons): array {
foreach ($buttons as $key => $button) {
// If no image is provided then just use the title.
if (!isset($button['image'])) {
$buttons[$key]['formattedimage'] = $button['title'];
} else {
// Check to see if this is an internal Moodle icon.
$internalimage = $this->page->theme->resolve_image_location('t/' . $button['image'], 'moodle');
if ($internalimage) {
$buttons[$key]['formattedimage'] = 't/' . $button['image'];
} else {
// Treat as an external image.
$buttons[$key]['formattedimage'] = $button['image'];
}
}
if (isset($button['linkattributes']['class'])) {
$class = $button['linkattributes']['class'] . ' btn';
} else {
$class = 'btn';
}
// Add the bootstrap 'btn' class for formatting.
$buttons[$key]['linkattributes'] = array_merge($button['linkattributes'], ['class' => $class]);
$temp = '';
foreach ($buttons[$key]['linkattributes'] as $index => $value) {
$temp .= "{$index}=\"{$value}\" ";
}
$buttons[$key]['linkattributes'] = $temp;
}
return $buttons;
}
/**
* Gets the logo data.
*
* @param \renderer_base $output Renderer output data.
* @return array Data to display a logo.
*/
protected function get_logo_data(\renderer_base $output): array {
global $SITE;
if (!$output->should_display_main_logo($this->headinglevel)) {
return [];
}
$sitename = format_string($SITE->fullname, true, ['context' => \context_course::instance(SITEID)]);
if (!isset($heading)) {
$heading = $output->heading($this->page->heading, $this->headinglevel, 'sr-only');
} else {
$heading = $output->heading($heading, $this->headinglevel, 'sr-only');
}
return [
'heading' => $heading,
'image' => [
'src' => $output->get_logo_url(null, 150),
'alt' => get_string('logoof', '', $sitename)
]
];
}
/**
* Organises all of the relevant data to display the context header.
*
* @param \renderer_base $output Renderer output data.
* @return array Data to display the context header.
*/
public function export_for_template(\renderer_base $output): array {
global $DB, $USER, $CFG;
$page = $this->page;
$context = $page->context;
$heading = null;
$imagedata = null;
$userbuttons = null;
// Make sure to use the heading if it has been set.
if (isset($this->headerinfo['heading'])) {
$heading = $this->headerinfo['heading'];
} else {
$heading = $page->heading;
}
// The user context currently has images and buttons. Other contexts may follow.
if (isset($this->headerinfo['user']) || $context->contextlevel == CONTEXT_USER) {
if (isset($this->headerinfo['user'])) {
$user = $this->headerinfo['user'];
} else {
// Look up the user information if it is not supplied.
$user = $DB->get_record('user', ['id' => $context->instanceid]);
}
// If the user context is set, then use that for capability checks.
if (isset($this->headerinfo['usercontext'])) {
$context = $this->headerinfo['usercontext'];
}
// Only provide user information if the user is the current user, or a user which the current user can view.
// When checking user_can_view_profile(), either:
// If the page context is course, check the course context (from the page object) or;
// If page context is NOT course, then check across all courses.
$course = ($context->contextlevel == CONTEXT_COURSE) ? $page->course : null;
if (user_can_view_profile($user, $course)) {
// Use the user's full name if the heading isn't set.
if (empty($heading)) {
$heading = fullname($user);
}
$imagedata = $output->user_picture($user, ['size' => 100]);
// Check to see if we should be displaying a message button.
if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
$userbuttons[] = [
'buttontype' => 'message',
'title' => get_string('message', 'message'),
'url' => new moodle_url('/message/index.php', ['id' => $user->id]),
'image' => 'message',
'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
'page' => (isset($page))
];
\core_message\helper::togglecontact_requirejs();
if ($USER->id != $user->id) {
$iscontact = \core_message\api::is_contact($USER->id, $user->id);
$contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
$contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
$contactimage = $iscontact ? 'removecontact' : 'addcontact';
$userbuttons[] = [
'buttontype' => 'togglecontact',
'title' => get_string($contacttitle, 'message'),
'url' => new moodle_url('/message/index.php', [
'user1' => $USER->id,
'user2' => $user->id,
$contacturlaction => $user->id,
'sesskey' => sesskey()
]),
'image' => $contactimage,
'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
'page' => (isset($page))
];
\core_message\helper::messageuser_requirejs();
}
$page->requires->string_for_js('changesmadereallygoaway', 'moodle');
$userbuttons = $this->format_button_images($userbuttons);
}
} else {
$heading = null;
}
}
$prefix = null;
if ($context->contextlevel == CONTEXT_MODULE) {
$heading = $page->cm->name;
$imagedata = $output->pix_icon('icon', '', $page->activityname);
$prefix = get_string('modulename', $page->activityname);
}
$logo = $this->get_logo_data($output);
if (empty($logo)) {
$logo = '';
$headingdisplay = $this->headinglevel + 1;
$headingdisplay = 'h' . $headingdisplay;
if (!isset($heading)) {
$heading = $output->heading($page->heading, $this->headinglevel, $headingdisplay);
} else {
$heading = $output->heading($heading, $this->headinglevel, $headingdisplay);
}
}
return [
'logo' => $logo,
'heading' => $heading,
'imagedata' => $imagedata,
'userbuttons' => $userbuttons,
'prefix' => $prefix
];
}
}
@@ -17,6 +17,8 @@
namespace theme_boost\output;
use moodle_url;
use html_writer;
use get_string;
defined('MOODLE_INTERNAL') || die;
@@ -42,4 +44,204 @@ class core_renderer extends \core_renderer {
return $this->render_single_button($button);
}
/**
* Renders the "breadcrumb" for all pages in boost.
*
* @return string the HTML for the navbar.
*/
public function navbar(): string {
$newnav = new \theme_boost\boostnavbar($this->page->navbar);
return $this->render_from_template('core/navbar', $newnav);
}
/**
* Renders the context header for the page.
*
* @param array $headerinfo Heading information.
* @param int $headinglevel What 'h' level to make the heading.
* @return string A rendered context header.
*/
public function context_header($headerinfo = null, $headinglevel = 1): string {
global $DB, $USER, $CFG, $SITE;
require_once($CFG->dirroot . '/user/lib.php');
$context = $this->page->context;
$heading = null;
$imagedata = null;
$subheader = null;
$userbuttons = null;
// Make sure to use the heading if it has been set.
if (isset($headerinfo['heading'])) {
$heading = $headerinfo['heading'];
} else {
$heading = $this->page->heading;
}
// The user context currently has images and buttons. Other contexts may follow.
if (isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) {
if (isset($headerinfo['user'])) {
$user = $headerinfo['user'];
} else {
// Look up the user information if it is not supplied.
$user = $DB->get_record('user', array('id' => $context->instanceid));
}
// If the user context is set, then use that for capability checks.
if (isset($headerinfo['usercontext'])) {
$context = $headerinfo['usercontext'];
}
// Only provide user information if the user is the current user, or a user which the current user can view.
// When checking user_can_view_profile(), either:
// If the page context is course, check the course context (from the page object) or;
// If page context is NOT course, then check across all courses.
$course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null;
if (user_can_view_profile($user, $course)) {
// Use the user's full name if the heading isn't set.
if (empty($heading)) {
$heading = fullname($user);
}
$imagedata = $this->user_picture($user, array('size' => 100));
// Check to see if we should be displaying a message button.
if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
$userbuttons = array(
'messages' => array(
'buttontype' => 'message',
'title' => get_string('message', 'message'),
'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
'image' => 'message',
'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
'page' => $this->page
)
);
if ($USER->id != $user->id) {
$iscontact = \core_message\api::is_contact($USER->id, $user->id);
$contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
$contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
$contactimage = $iscontact ? 'removecontact' : 'addcontact';
$userbuttons['togglecontact'] = array(
'buttontype' => 'togglecontact',
'title' => get_string($contacttitle, 'message'),
'url' => new moodle_url('/message/index.php', array(
'user1' => $USER->id,
'user2' => $user->id,
$contacturlaction => $user->id,
'sesskey' => sesskey())
),
'image' => $contactimage,
'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
'page' => $this->page
);
}
$this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
}
} else {
$heading = null;
}
}
$prefix = null;
if ($context->contextlevel == CONTEXT_MODULE) {
$heading = $this->page->cm->name;
$imagedata = $this->pix_icon('icon', '', $this->page->activityname);
$prefix = get_string('modulename', $this->page->activityname);
}
if ($this->should_display_main_logo($headinglevel)) {
$sitename = format_string($SITE->fullname, true, ['context' => \context_course::instance(SITEID)]);
// Logo.
$html = html_writer::div(
html_writer::empty_tag('img', [
'src' => $this->get_logo_url(null, 150),
'alt' => get_string('logoof', '', $sitename),
'class' => 'img-fluid'
]),
'logo'
);
// Heading.
if (!isset($heading)) {
$html .= $this->heading($this->page->heading, $headinglevel, 'sr-only');
} else {
$html .= $this->heading($heading, $headinglevel, 'sr-only');
}
return $html;
}
$contextheader = new \context_header($heading, $headinglevel, $imagedata, $userbuttons, $prefix);
return $this->render_context_header($contextheader);
}
/**
* Renders the header bar.
*
* @param context_header $contextheader Header bar object.
* @return string HTML for the header bar.
*/
protected function render_context_header(\context_header $contextheader) {
// Generate the heading first and before everything else as we might have to do an early return.
if (!isset($contextheader->heading)) {
$heading = $this->heading($this->page->heading, $contextheader->headinglevel, 'h2');
} else {
$heading = $this->heading($contextheader->heading, $contextheader->headinglevel, 'h2');
}
$showheader = empty($this->page->layout_options['nocontextheader']);
if (!$showheader) {
// Return the heading wrapped in an sr-only element so it is only visible to screen-readers.
return html_writer::div($heading, 'sr-only');
}
// All the html stuff goes here.
$html = html_writer::start_div('page-context-header');
// Image data.
if (isset($contextheader->imagedata)) {
// Header specific image.
$html .= html_writer::div($contextheader->imagedata, 'page-header-image icon-size-6');
}
// Headings.
if (isset($contextheader->prefix)) {
$prefix = html_writer::div($contextheader->prefix, 'text-muted text-uppercase');
$heading = $prefix . $heading;
}
$html .= html_writer::tag('div', $heading, array('class' => 'page-header-headings'));
// Buttons.
if (isset($contextheader->additionalbuttons)) {
$html .= html_writer::start_div('btn-group header-button-group');
foreach ($contextheader->additionalbuttons as $button) {
if (!isset($button->page)) {
// Include js for messaging.
if ($button['buttontype'] === 'togglecontact') {
\core_message\helper::togglecontact_requirejs();
}
if ($button['buttontype'] === 'message') {
\core_message\helper::messageuser_requirejs();
}
$image = $this->pix_icon($button['formattedimage'], $button['title'], 'moodle', array(
'class' => 'iconsmall',
'role' => 'presentation'
));
$image .= html_writer::span($button['title'], 'header-button-title');
} else {
$image = html_writer::empty_tag('img', array(
'src' => $button['formattedimage'],
'role' => 'presentation'
));
}
$html .= html_writer::link($button['url'], html_writer::tag('span', $image), $button['linkattributes']);
}
$html .= html_writer::end_div();
}
$html .= html_writer::end_div();
return $html;
}
}
+19 -3
View File
@@ -42,6 +42,20 @@ $hasblocks = strpos($blockshtml, 'data-block=') !== false;
$buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_actions();
// If the settings menu will be included in the header then don't add it here.
$regionmainsettingsmenu = $buildregionmainsettings ? $OUTPUT->region_main_settings_menu() : false;
$secondarynavigation = false;
if (!defined('BEHAT_SITE_RUNNING')) {
$buildsecondarynavigation = $PAGE->has_secondary_navigation();
if ($buildsecondarynavigation) {
$moremenu = new \core\navigation\output\more_menu($PAGE->secondarynav, 'nav-tabs');
$secondarynavigation = $moremenu->export_for_template($OUTPUT);
}
}
$primary = new core\navigation\output\primary($PAGE);
$renderer = $PAGE->get_renderer('core');
$primarymenu = $primary->export_for_template($renderer);
$templatecontext = [
'sitename' => format_string($SITE->shortname, true, ['context' => context_course::instance(SITEID), "escape" => false]),
'output' => $OUTPUT,
@@ -50,11 +64,13 @@ $templatecontext = [
'bodyattributes' => $bodyattributes,
'navdraweropen' => $navdraweropen,
'regionmainsettingsmenu' => $regionmainsettingsmenu,
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu)
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu),
'primarymoremenu' => $primarymenu['moremenu'],
'secondarymoremenu' => $secondarynavigation,
'usermenu' => $primarymenu['user'],
'langmenu' => $primarymenu['lang'],
];
$nav = $PAGE->flatnav;
$templatecontext['flatnavigation'] = $nav;
$templatecontext['firstcollectionlabel'] = $nav->get_collectionlabel();
echo $OUTPUT->render_from_template('theme_boost/columns2', $templatecontext);
+18 -1
View File
@@ -69,6 +69,19 @@ $buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_action
// If the settings menu will be included in the header then don't add it here.
$regionmainsettingsmenu = $buildregionmainsettings ? $OUTPUT->region_main_settings_menu() : false;
$secondarynavigation = false;
if (!defined('BEHAT_SITE_RUNNING')) {
$buildsecondarynavigation = $PAGE->has_secondary_navigation();
if ($buildsecondarynavigation) {
$moremenu = new \core\navigation\output\more_menu($PAGE->secondarynav, 'nav-tabs');
$secondarynavigation = $moremenu->export_for_template($OUTPUT);
}
}
$primary = new core\navigation\output\primary($PAGE);
$renderer = $PAGE->get_renderer('core');
$primarymenu = $primary->export_for_template($renderer);
$templatecontext = [
'sitename' => format_string($SITE->shortname, true, ['context' => context_course::instance(SITEID), "escape" => false]),
'output' => $OUTPUT,
@@ -80,7 +93,11 @@ $templatecontext = [
'blockdraweropen' => $blockdraweropen,
'regionmainsettingsmenu' => $regionmainsettingsmenu,
'courseindex' => $courseindex,
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu)
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu),
'primarymoremenu' => $primarymenu['moremenu'],
'secondarymoremenu' => $secondarynavigation,
'usermenu' => $primarymenu['user'],
'langmenu' => $primarymenu['lang'],
];
$nav = $PAGE->flatnav;
+3
View File
@@ -45,3 +45,6 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/navbar";
@import "moodle/reportbuilder";
@import "moodle/courseindex";
@import "moodle/moremenu";
@import "moodle/primarynavigation";
@import "moodle/secondarynavigation";
+5 -2
View File
@@ -379,10 +379,12 @@ img.resize {
.action-menu .dropdown-toggle {
text-decoration: none;
display: inline-block;
}
.action-menu {
white-space: nowrap;
display: inline;
}
.block img.resize {
@@ -2076,7 +2078,8 @@ nav.navbar .logo img {
.page-header-image {
margin-right: 1em;
margin-bottom: 1em;
margin-bottom: 0.5em;
margin-top: 0.7em;
& > a {
display: inline-block;
}
@@ -2706,7 +2709,7 @@ $picker-emojis-per-row: 7 !default;
}
}
}
.nav-tabs,
.nav-tabs:not(.more-nav),
.nav-pills {
margin: 0;
border: 0;
+39
View File
@@ -0,0 +1,39 @@
.moremenu {
opacity: 0;
height: $moremenu-height;
&.observed {
opacity: 1;
}
.nav-link {
height: $moremenu-height;
display: flex;
align-items: center;
}
.nav-tabs {
margin-left: 0;
}
// Styling for dropdown menus inside the MoreButton.
.dropdownmoremenu > .dropdown-menu {
& > .dropdown-item {
padding: 0;
}
.dropdown-menu {
position: static;
padding: 0;
border: 0;
&.show {
display: block;
}
.dropdown-item {
background-color: $gray-100;
@include hover-focus() {
color: $dropdown-link-hover-color;
@include gradient-bg($dropdown-link-active-bg);
}
}
.dropdown-divider {
display: none;
}
}
}
}
+78
View File
@@ -33,6 +33,57 @@
.dropdown-toggle::after {
display: none;
}
.dropdown-menu {
min-width: 235px;
.carousel-navigation-link {
> * {
pointer-events: none;
}
}
.dropdown-item {
padding: .25rem 1.75rem .25rem .75rem;
&.carousel-navigation-link::after {
font-family: FontAwesome;
content: $fa-var-caret-right;
font-size: 1rem;
right: .75rem;
position: absolute;
}
}
.submenu {
.header {
padding: .25rem .75rem;
font-size: .975rem;
.icon {
font-size: 20px;
height: 20px;
width: 20px;
margin: 0;
}
}
.items {
.dropdown-item {
&[aria-current="true"]::before {
font-family: FontAwesome;
content: $fa-var-check;
font-size: 0.75rem;
padding-left: .25rem;
}
}
}
}
}
}
.langmenu {
.dropdown-menu {
.dropdown-item {
&[aria-current="true"]::before {
content: "\f00c";
font-size: 0.75rem;
padding-left: .25rem;
}
}
}
}
.moodle-actionmenu .menubar,
.action-menu-trigger .dropdown {
@@ -40,6 +91,33 @@
display: flex;
}
}
.dir-rtl .navbar.fixed-top {
.usermenu {
.dropdown-menu {
.dropdown-item {
&.carousel-navigation-link::after {
content: $fa-var-caret-left;
}
}
.carousel {
.carousel-inner {
.carousel-item-prev.carousel-item-right,
.carousel-item-next.carousel-item-left {
transform: translateX(0);
}
.carousel-item-next,
.carousel-item-right.active {
transform: translateX(-100%);
}
.carousel-item-prev,
.carousel-item-left.active {
transform: translateX(100%);
}
}
}
}
}
}
#page {
margin-top: $navbar-height;
}
@@ -0,0 +1,8 @@
.navbar.fixed-top {
.moremenu {
height: $navbar-height;
.nav-link {
height: $navbar-height;
}
}
}

Some files were not shown because too many files have changed in this diff Show More