Merge branch 'MDL-72352-master-v2' of git://github.com/peterRd/moodle

This commit is contained in:
Jun Pataleta 2021-10-20 15:45:22 +08:00
commit 56c55f996a
5 changed files with 621 additions and 4 deletions

View File

@ -57,6 +57,7 @@ class secondary extends view {
'gradebooksetup' => 2.1,
'outcomes' => 2.2,
'coursecompletion' => 6,
'filtermanagement' => 9,
],
];
$nodes['navigation'] = [
@ -197,13 +198,129 @@ class secondary extends view {
$this->initialised = true;
}
/**
* Recursively goes and gets all children nodes.
*
* @param navigation_node $node The node to get the children of.
* @return array The additional child nodes.
*/
protected function get_additional_child_nodes(navigation_node $node): array {
$nodes = [];
foreach ($node->children as $child) {
if ($child->has_action()) {
$nodes[$child->action->out()] = $child->text;
}
if ($child->has_children()) {
$childnodes = $this->get_additional_child_nodes($child);
$nodes = array_merge($nodes, $childnodes);
}
}
return $nodes;
}
/**
* Returns an array of sections, actions, and text for a url select menu.
*
* @param navigation_node $node The node to use for a url select menu.
* @return array The menu array.
*/
protected function get_menu_array(navigation_node $node): array {
$urldata = [];
// Check that children have children.
$additionalchildren = false;
$initialchildren = [];
if ($node->has_action()) {
$initialchildren[$node->action->out()] = $node->text;
}
foreach ($node->children as $child) {
$additionalnode = [];
if ($child->has_action()) {
$additionalnode[$child->action->out()] = $child->text;
}
if ($child->has_children()) {
$additionalchildren = true;
$urldata[][$child->text] = $additionalnode + $this->get_additional_child_nodes($child);
} else {
$initialchildren += $additionalnode;
}
}
if ($additionalchildren) {
$urldata[][$node->text] = $initialchildren;
} else {
$urldata = $initialchildren;
}
return $urldata;
}
/**
* Returns a node with the action being from the first found child node that has an action (Recursive).
*
* @param navigation_node $node The part of the node tree we are checking.
* @param navigation_node $basenode The very first node to be used for the return.
* @return navigation_node|null
*/
protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {
$newnode = null;
if (!$node->has_children()) {
return null;
}
// Find the first child with an action and update the main node.
foreach ($node->children as $child) {
if ($child->has_action()) {
$newnode = $basenode;
$newnode->action = $child->action;
return $newnode;
}
}
if (is_null($newnode)) {
// Check for children and go again.
foreach ($node->children as $child) {
if ($child->has_children()) {
$newnode = $this->get_node_with_first_action($child, $basenode);
if (!is_null($newnode)) {
return $newnode;
}
}
}
}
return null;
}
/**
* Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have
* an action then a search is done through the children looking for the first node that has an action. This action is then given
* to the parent node that is initially provided as a parameter.
*
* @param navigation_node $node The navigation node that we want to ensure has an action tied to it.
* @return navigation_node The node intact with an action to use.
*/
protected function get_first_action_for_node(navigation_node $node): ?navigation_node {
// If the node does not have children OR has an action no further processing needed.
$newnode = null;
if ($node->has_children()) {
if (!$node->has_action()) {
// We want to find the first child with an action.
// We want to check all children on this level before going further down.
// Note that new node gets changed here.
$newnode = $this->get_node_with_first_action($node, $node);
} else {
$newnode = $node;
}
}
return $newnode;
}
/**
* Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do
* the relevant checks, we don't do it again here.
*/
protected function load_course_navigation(): void {
$course = $this->page->course;
// Initialise the main navigation and settings nav.
// It is important that this is done before we try anything.
$settingsnav = $this->page->settingsnav;
@ -224,6 +341,74 @@ class secondary extends view {
$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));
}
// Try to get any custom nodes defined by a user which may include containers.
$expectedcourseadmin = ['editsettings', 'coursecompletion', 'users', 'coursereports', 'gradebooksetup', 'coursebadges',
'backup', 'restore', 'import', 'copy', 'reset', 'questionbank'];
foreach ($settingsnav->children as $value) {
if ($value->key == 'courseadmin') {
foreach ($value->children as $other) {
if (array_search($other->key, $expectedcourseadmin) === false) {
$othernode = $this->get_first_action_for_node($other);
// Get the first node and check whether it's been added already.
if ($othernode && !$this->get($othernode->key)) {
$this->add_node($othernode);
}
}
}
}
}
}
/**
* Recursively looks for a match to the current page url.
*
* @param navigation_node $node The node to look through.
* @return navigation_node|null The node that matches this page's url.
*/
protected function nodes_match_current_url(navigation_node $node): ?navigation_node {
$pagenode = $this->page->url;
if ($node->has_action()) {
// Check this node first.
if ($node->action->compare($pagenode)) {
return $node;
}
}
if ($node->has_children()) {
foreach ($node->children as $child) {
$result = $this->nodes_match_current_url($child);
if ($result) {
return $result;
}
}
}
return null;
}
/**
* Returns a url_select object with overflow navigation nodes.
*
* @return \url_select|null The overflow menu data.
*/
public function get_overflow_menu_data(): ?\url_select {
$activenode = $this->find_active_node();
if ($activenode && $activenode->has_action() && $activenode->has_children() && $activenode->key != 'coursehome') {
// This needs to be expanded to does the active node have children and does the page url match any of the children.
$menunode = $this->page->settingsnav->find($activenode->key, null);
if ($menunode instanceof navigation_node) {
// Loop through all children and try and find a match to the current url.
$matchednode = $this->nodes_match_current_url($menunode);
if (is_null($matchednode)) {
return null;
}
if (!isset($menunode) || !$menunode->has_children()) {
return null;
}
$selectdata = $this->get_menu_array($menunode);
return new \url_select($selectdata, $matchednode->action->out(), null);
}
}
return null;
}
/**
@ -342,7 +527,15 @@ class secondary extends view {
$leftover = array_diff($existingkeys, $populatedkeys);
foreach ($leftover as $key) {
if (!in_array($key, $flattenednodes) && $leftovernode = $completenode->get($key)) {
$this->add_node($leftovernode);
// Check for nodes with children and potentially no action to direct to.
if ($leftovernode->has_children()) {
$leftovernode = $this->get_first_action_for_node($leftovernode);
}
// Confirm we have a valid object to add.
if ($leftovernode) {
$this->add_node($leftovernode);
}
}
}
}

View File

@ -4516,7 +4516,8 @@ class settings_navigation extends navigation_node {
// Manage filters
if ($adminoptions->filters) {
$url = new moodle_url('/filter/manage.php', array('contextid'=>$coursecontext->id));
$coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/filter', ''));
$coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING,
null, 'filtermanagement', new pix_icon('i/filter', ''));
}
// View course reports.

View File

@ -404,4 +404,415 @@ class secondary_test extends \advanced_testcase {
],
];
}
/**
* Recursive call to generate a navigation node given an array definition.
*
* @param array $structure
* @param string $parentkey
* @return navigation_node
*/
private function generate_node_tree_construct(array $structure, string $parentkey): navigation_node {
$node = navigation_node::create($parentkey, null, navigation_node::TYPE_CUSTOM, '', $parentkey);
foreach ($structure as $key => $value) {
if (is_array($value)) {
$children = $value['children'] ?? $value;
$child = $this->generate_node_tree_construct($children, $key);
if (isset($value['action'])) {
$child->action = new \moodle_url($value['action']);
}
$node->add_node($child);
} else {
$node->add($key, $value, navigation_node::TYPE_CUSTOM, '', $key);
}
}
return $node;
}
/**
* Test the nodes_match_current_url function.
*
* @param string $selectedurl
* @param string $expectednode
* @dataProvider test_nodes_match_current_url_provider
*/
public function test_nodes_match_current_url(string $selectedurl, string $expectednode) {
global $PAGE;
$structure = [
'parentnode1' => [
'child1' => '/my',
'child2' => [
'child2.1' => '/view/course.php',
'child2.2' => '/view/admin.php',
]
]
];
$node = $this->generate_node_tree_construct($structure, 'primarynode');
$node->action = new \moodle_url('/');
$PAGE->set_url($selectedurl);
$secondary = new secondary($PAGE);
$method = new ReflectionMethod('core\navigation\views\secondary', 'nodes_match_current_url');
$method->setAccessible(true);
$response = $method->invoke($secondary, $node);
$this->assertSame($response->key ?? null, $expectednode);
}
/**
* Provider for test_nodes_match_current_url
*
* @return \string[][]
*/
public function test_nodes_match_current_url_provider(): array {
return [
"Match url to a node that is a deep nested" => [
'/view/course.php',
'child2.1',
],
"Match url to a parent node with children" => [
'/', 'primarynode'
],
"Match url to a child node" => [
'/my', 'child1'
],
];
}
/**
* Test the get_menu_array function
*
* @param string $selected
* @param array $expected
* @dataProvider test_get_menu_array_provider
*/
public function test_get_menu_array(string $selected, array $expected) {
global $PAGE;
// Custom nodes - mimicing nodes added via 3rd party plugins.
$structure = [
'parentnode1' => [
'child1' => '/my',
'child2' => [
'action' => '/test.php',
'children' => [
'child2.1' => '/view/course.php?child=2',
'child2.2' => '/view/admin.php?child=2',
'child2.3' => '/test.php',
]
],
'child3' => [
'child3.1' => '/view/course.php?child=3',
'child3.2' => '/view/admin.php?child=3',
]
],
'parentnode2' => "/view/module.php"
];
$secondary = new secondary($PAGE);
$secondary->add_node($this->generate_node_tree_construct($structure, 'primarynode'));
$selectednode = $secondary->find($selected, null);
$method = new ReflectionMethod('core\navigation\views\secondary', 'get_menu_array');
$method->setAccessible(true);
$response = $method->invoke($secondary, $selectednode);
$this->assertSame($expected, $response);
}
/**
* Provider for test_get_menu_array
*
* @return array[]
*/
public function test_get_menu_array_provider(): array {
return [
"Fetch information from a node with action and no children" => [
'child1',
[
'https://www.example.com/moodle/my' => 'child1'
],
],
"Fetch information from a node with no action and children" => [
'child3',
[
'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2'
],
],
"Fetch information from a node with children" => [
'child2',
[
'https://www.example.com/moodle/test.php' => 'child2',
'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2'
],
],
"Fetch information from a node with an action and no children" => [
'parentnode2',
['https://www.example.com/moodle/view/module.php' => 'parentnode2'],
],
"Fetch information from a node with an action and multiple nested children" => [
'parentnode1',
[
[
'child2' => [
'https://www.example.com/moodle/test.php' => 'child2',
'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2',
]
],
[
'child3' => [
'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2'
]
],
[
'parentnode1' => [
'https://www.example.com/moodle/my' => 'child1'
]
]
],
],
];
}
/**
* Test the get_node_with_first_action function
*
* @param string $selectedkey
* @param string|null $expectedkey
* @dataProvider test_get_node_with_first_action_provider
*/
public function test_get_node_with_first_action(string $selectedkey, ?string $expectedkey) {
global $PAGE;
$structure = [
'parentnode1' => [
'child1' => [
'child1.1' => null
],
'child2' => [
'child2.1' => [
'child2.1.1' => [
'action' => '/test.php',
'children' => [
'child2.1.1.1' => '/view/course.php?child=2',
'child2.1.1.2' => '/view/admin.php?child=2',
]
]
]
],
'child3' => [
'child3.1' => '/view/course.php?child=3',
'child3.2' => '/view/admin.php?child=3',
]
],
'parentnode2' => "/view/module.php"
];
$nodes = $this->generate_node_tree_construct($structure, 'primarynode');
$selectednode = $nodes->find($selectedkey, null);
$expected = null;
// Expected response will be the parent node with the action updated.
if ($expectedkey) {
$expectedbasenode = clone $selectednode;
$actionfromnode = $nodes->find($expectedkey, null);
$expectedbasenode->action = $actionfromnode->action;
$expected = $expectedbasenode;
}
$secondary = new secondary($PAGE);
$method = new ReflectionMethod('core\navigation\views\secondary', 'get_node_with_first_action');
$method->setAccessible(true);
$response = $method->invoke($secondary, $selectednode, $selectednode);
$this->assertEquals($expected, $response);
}
/**
* Provider for test_get_node_with_first_action
*
* @return array
*/
public function test_get_node_with_first_action_provider(): array {
return [
"Search for action when parent has no action and multiple children with actions" => [
"child3",
"child3.1",
],
"Search for action when parent child is deeply nested." => [
"child2",
"child2.1.1"
],
"No navigation node returned when node has no children" => [
"parentnode2",
null
],
"No navigation node returned when node has children but no actions available." => [
"child1",
null
],
];
}
/**
* Test for get_additional_child_nodes
*
* @param string $selectedkey
* @param array $expected
* @dataProvider test_get_additional_child_nodes_provider
*/
public function test_get_additional_child_nodes(string $selectedkey, array $expected) {
global $PAGE;
$structure = [
'parentnode1' => [
'child1' => '/my',
'child2' => [
'action' => '/test.php',
'children' => [
'child2.1' => '/view/course.php?child=2',
'child2.2' => '/view/admin.php?child=2',
]
],
'child3' => [
'child3.1' => '/view/course.php?child=3',
'child3.2' => '/view/admin.php?child=3',
]
],
'parentnode2' => "/view/module.php"
];
$secondary = new secondary($PAGE);
$nodes = $this->generate_node_tree_construct($structure, 'primarynode');
$selectednode = $nodes->find($selectedkey, null);
$method = new ReflectionMethod('core\navigation\views\secondary', 'get_additional_child_nodes');
$method->setAccessible(true);
$response = $method->invoke($secondary, $selectednode);
$this->assertSame($expected, $response);
}
/**
* Provider for test_get_additional_child_nodes
*
* @return array[]
*/
public function test_get_additional_child_nodes_provider(): array {
return [
"Get nodes with deep nested children" => [
"parentnode1",
[
'https://www.example.com/moodle/my' => 'child1',
'https://www.example.com/moodle/test.php' => 'child2',
'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2',
'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2',
]
],
"Get children from parent without action " => [
"child3",
[
'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2'
]
],
"Get children from parent with action " => [
"child2",
[
'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2'
]
],
];
}
/**
* Test the get_overflow_menu_data function
*
* @param string $selectedurl
* @param bool $expectednull
* @param bool $emptynode
* @dataProvider test_get_overflow_menu_data_provider
*/
public function test_get_overflow_menu_data(string $selectedurl, bool $expectednull, bool $emptynode = false) {
global $PAGE;
// Custom nodes - mimicing nodes added via 3rd party plugins.
$structure = [
'parentnode1' => [
'child1' => '/my',
'child2' => [
'action' => '/test.php',
'children' => [
'child2.1' => '/view/course.php',
'child2.2' => '/view/admin.php',
]
]
],
'parentnode2' => "/view/module.php"
];
$PAGE->set_url($selectedurl);
navigation_node::override_active_url(new \moodle_url($selectedurl));
$node = $this->generate_node_tree_construct($structure, 'primarynode');
$node->action = new \moodle_url('/');
$secondary = new secondary($PAGE);
$secondary->add_node($node);
$PAGE->settingsnav->add_node(clone $node);
$secondary->add('Course home', '/coursehome.php', navigation_node::TYPE_CUSTOM, '', 'coursehome');
$secondary->add('Course settings', '/course/settings.php', navigation_node::TYPE_CUSTOM, '', 'coursesettings');
// Test for an empty node without children and action.
if ($emptynode) {
$node = $secondary->add('Course management', null, navigation_node::TYPE_CUSTOM, '', 'course');
$node->make_active();
} else {
// Set the correct node as active.
$method = new ReflectionMethod('core\navigation\views\secondary', 'scan_for_active_node');
$method->setAccessible(true);
$method->invoke($secondary, $secondary);
}
$method = new ReflectionMethod('core\navigation\views\secondary', 'get_overflow_menu_data');
$method->setAccessible(true);
$response = $method->invoke($secondary);
if ($expectednull) {
$this->assertNull($response);
} else {
$this->assertIsObject($response);
$this->assertInstanceOf('url_select', $response);
}
}
/**
* Data provider for test_get_overflow_menu_data
*
* @return string[]
*/
public function test_get_overflow_menu_data_provider(): array {
return [
"Active node is the course home node" => [
'/coursehome.php',
true
],
"Active node is one with an action and no children" => [
'/view/module.php',
false
],
"Active node is one with an action and children" => [
'/',
false
],
"Active node is one without an action and children" => [
'/',
true,
true,
],
"Active node is one with an action and children but is NOT in settingsnav" => [
'/course/settings.php',
true
],
];
}
}

View File

@ -71,11 +71,19 @@ $buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_action
$regionmainsettingsmenu = $buildregionmainsettings ? $OUTPUT->region_main_settings_menu() : false;
$secondarynavigation = false;
$overflow = false;
if (!defined('BEHAT_SITE_RUNNING')) {
$buildsecondarynavigation = $PAGE->has_secondary_navigation();
if ($buildsecondarynavigation) {
$moremenu = new \core\navigation\output\more_menu($PAGE->secondarynav, 'nav-tabs');
$secondary = $PAGE->secondarynav;
$moremenu = new \core\navigation\output\more_menu($secondary, 'nav-tabs');
$secondarynavigation = $moremenu->export_for_template($OUTPUT);
// Get the pre-content stuff.
$overflowdata = $secondary->get_overflow_menu_data();
if (!is_null($overflowdata)) {
$overflow = $overflowdata->export_for_template($OUTPUT);
}
}
}
@ -100,6 +108,7 @@ $templatecontext = [
'usermenu' => $primarymenu['user'],
'langmenu' => $primarymenu['lang'],
'forceblockdraweropen' => $forceblockdraweropen,
'overflow' => $overflow
];
$nav = $PAGE->flatnav;

View File

@ -129,6 +129,9 @@
<div class="region_main_settings_menu_proxy"></div>
{{/hasregionmainsettingsmenu}}
{{{ output.course_content_header }}}
{{#overflow}}
{{> core/url_select}}
{{/overflow}}
{{{ output.main_content }}}
{{{ output.activity_navigation }}}
{{{ output.course_content_footer }}}