diff --git a/grade/grading/lib.php b/grade/grading/lib.php index a75dd20a564..7af2811ea0f 100644 --- a/grade/grading/lib.php +++ b/grade/grading/lib.php @@ -460,7 +460,7 @@ class grading_manager { $this->set_area($areaname); $method = $this->get_active_method(); $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'), - $this->get_management_url(), settings_navigation::TYPE_CUSTOM); + $this->get_management_url(), settings_navigation::TYPE_CUSTOM, null, 'advgrading'); if ($method) { $controller = $this->get_controller($method); $controller->extend_settings_navigation($settingsnav, $managementnode); @@ -469,7 +469,7 @@ class grading_manager { } else { // make management screen node for each area $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'), - null, settings_navigation::TYPE_CUSTOM); + null, settings_navigation::TYPE_CUSTOM, null, 'advgrading'); foreach ($areas as $areaname => $areatitle) { $this->set_area($areaname); $method = $this->get_active_method(); diff --git a/lang/en/admin.php b/lang/en/admin.php index e697dee502f..8f0cda14793 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -419,6 +419,7 @@ $string['courselistshortnames'] = 'Display extended course names'; $string['courselistshortnames_desc'] = 'If enabled, course short names will be displayed in addition to full names in course lists. If required, extended course names may be customised by editing the \'courseextendednamedisplay\' language string using the language customisation feature.'; $string['coursemgmt'] = 'Manage courses and categories'; $string['courseoverview'] = 'Course overview'; +$string['coursepage'] = 'Course Page'; $string['courserequestnotify'] = 'Course request notification'; $string['courserequestnotifyemail'] = 'User {$a->user} requested a new course at {$a->link}'; $string['courserequests'] = 'Course requests'; diff --git a/lib/classes/navigation/views/secondary.php b/lib/classes/navigation/views/secondary.php new file mode 100644 index 00000000000..9c965c68a23 --- /dev/null +++ b/lib/classes/navigation/views/secondary.php @@ -0,0 +1,249 @@ +. + +namespace core\navigation\views; + +use navigation_node; + +/** + * Class secondary_navigation_view. + * + * The secondary navigation view is a stripped down tweaked version of the + * settings_navigation/navigation + * + * @package core + * @category navigation + * @copyright 2021 onwards Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class secondary extends view { + /** + * Defines the default structure for the secondary nav in a course context. + * + * In a course context, we are curating nodes from the settingsnav and navigation objects. + * The following mapping construct specifies which object we are fetching it from, the type of the node, the key + * and in what order we want the node - defined as per the mockups. + * + * @return array + */ + protected function get_default_course_mapping(): array { + $nodes = []; + $nodes['settings'] = [ + self::TYPE_CONTAINER => [ + 'coursereports' => 3, + 'questionbank' => 4, + ], + self::TYPE_SETTING => [ + 'editsettings' => 0, + 'gradebooksetup' => 2.1, + 'outcomes' => 2.2, + 'coursecompletion' => 6, + ], + ]; + $nodes['navigation'] = [ + self::TYPE_CONTAINER => [ + 'participants' => 1, + ], + self::TYPE_SETTING => [ + 'grades' => 2, + 'badgesview' => 7, + 'competencies' => 8, + ], + self::TYPE_CUSTOM => [ + 'contentbank' => 5, + ], + ]; + + return $nodes; + } + + /** + * Defines the default structure for the secondary nav in a module context. + * + * In a module context, we are curating nodes from the settingsnav object. + * The following mapping construct specifies the type of the node, the key + * and in what order we want the node - defined as per the mockups. + * + * @return array + */ + protected function get_default_module_mapping(): array { + 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, + 'backup' => 9, + 'restore' => 10, + 'competencybreakdown' => 11, + ], + self::TYPE_CUSTOM => [ + 'advgrading' => 2, + ], + ]; + } + + /** + * Initialise the view based navigation based on the current context. + * + * As part of the initial restructure, the secondary nav is only considered for the following pages: + * 1 - Site admin settings + * 2 - Course page - Does not include front_page which has the same context. + * 3 - Module page + */ + public function initialise(): void { + global $SITE; + + if (during_initial_install() || $this->initialised) { + return; + } + $this->id = 'secondary_navigation'; + $context = $this->context; + + switch ($context->contextlevel) { + case CONTEXT_COURSE: + if ($this->page->course->id != $SITE->id) { + $this->load_course_navigation(); + } + break; + case CONTEXT_MODULE: + $this->load_module_navigation(); + break; + case CONTEXT_SYSTEM: + $this->load_admin_navigation(); + break; + } + + // Search and set the active node. + $this->scan_for_active_node($this); + $this->initialised = true; + } + + /** + * 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; + $navigation = $this->page->navigation; + + $url = new \moodle_url('/course/view.php', ['id' => $course->id, 'sesskey' => sesskey()]); + $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']); + $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)); + } + + /** + * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via + * '_extend_settings_navigation'. + * It populates the tree based on the nav mockup + * + * If nodes change, we will have to explicitly call the callback again. + */ + protected function load_module_navigation(): void { + $settingsnav = $this->page->settingsnav; + $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING); + $nodes = $this->get_default_module_mapping(); + + if ($mainnode) { + $this->add(get_string('module', 'course'), $this->page->url, null, null, 'modulepage'); + // Add the initial nodes. + $nodesordered = $this->get_leaf_nodes($mainnode, $nodes); + $this->add_ordered_nodes($nodesordered); + + // We have finished inserting the initial structure. + // Populate the menu with the rest of the nodes available. + $this->load_remaining_nodes($mainnode, $nodes); + } + } + + /** + * Load the site admin navigation + */ + protected function load_admin_navigation(): void { + $settingsnav = $this->page->settingsnav; + $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN); + if ($node) { + $siteadminnode = $this->add($node->text, "#link$node->key", null, null, 'siteadminnode'); + foreach ($node->children as $child) { + if ($child->display && !$child->is_short_branch()) { + $this->add_node($child); + } else { + $siteadminnode->add_node($child); + } + } + } + } + + /** + * Adds the indexed nodes to the current view. The key should indicate it's position in the tree. Any sub nodes + * needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed under #3 node. + * + * @param array $nodes An array of navigation nodes to be added. + */ + protected function add_ordered_nodes(array $nodes): void { + ksort($nodes); + foreach ($nodes as $key => $node) { + // If the key is a string then we are assuming this is a nested element. + if (is_string($key)) { + $parentnode = $nodes[floor($key)] ?? null; + if ($parentnode) { + $parentnode->add_node($node); + } + } else { + $this->add_node($node); + } + } + } + + /** + * Find the remaining nodes that need to be loaded into secondary based on the current context + * + * @param navigation_node $completenode The original node that we are sourcing information from + * @param array $nodesmap The map used to populate secondary nav in the given context + */ + protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap): void { + $flattenednodes = []; + foreach ($nodesmap as $nodecontainer) { + $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes); + } + + $populatedkeys = $this->get_children_key_list(); + $existingkeys = $completenode->get_children_key_list(); + $leftover = array_diff($existingkeys, $populatedkeys); + foreach ($leftover as $key) { + if (!in_array($key, $flattenednodes) && $leftovernode = $completenode->get($key)) { + $this->add_node($leftovernode); + } + } + } +} diff --git a/lib/classes/navigation/views/view.php b/lib/classes/navigation/views/view.php new file mode 100644 index 00000000000..8f62f3445e1 --- /dev/null +++ b/lib/classes/navigation/views/view.php @@ -0,0 +1,127 @@ +. + +namespace core\navigation\views; + +use navigation_node; +use navigation_node_collection; + +/** + * Class view. + * + * The base view class which expands on the navigation_node, + * + * @package core + * @category navigation + * @copyright 2021 onwards Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class view extends navigation_node { + /** @var stdClass $context the current context */ + protected $context; + /** @var moodle_page $page the moodle page that the navigation belongs to */ + protected $page; + /** @var bool $initialised A switch to see if the navigation node is initialised */ + protected $initialised = false; + + /** + * Function to initialise the respective view + * @return void + */ + abstract public function initialise(): void; + + /** + * navigation constructor. + * @param \moodle_page $page + */ + public function __construct(\moodle_page $page) { + global $FULLME; + + if (during_initial_install()) { + return false; + } + + $this->page = $page; + $this->context = $this->page->context; + + // Not all pages override the active url. Do it now. + if ($this->page->has_set_url()) { + self::override_active_url(new \moodle_url($this->page->url)); + } else { + self::override_active_url(new \moodle_url($FULLME)); + } + + $this->children = new navigation_node_collection(); + } + + /** + * Get the leaf nodes for the nav view + * + * @param navigation_node $source The settingsnav OR navigation object + * @param array $nodes An array of nodes to fetch from the source which specifies the node type and final order + * @return array $nodesordered The fetched nodes ordered based on final specification. + */ + protected function get_leaf_nodes(navigation_node $source, array $nodes): array { + $nodesordered = []; + foreach ($nodes as $type => $leaves) { + foreach ($leaves as $leaf => $location) { + if ($node = $source->find($leaf, $type)) { + $nodesordered["$location"] = $node; + } + } + } + + return $nodesordered; + } + + /** + * This function recursively scans nodes until it finds the active node or there + * are no more nodes. We are using a custom implementation here to be more strict with the comparison + * and also because we need the parent node and not the specific child node in the new views. + * e.g. Structure for site admin, + * SecondaryNav + * - Site Admin + * - Users + * - User policies + * - Courses + * In the above example, if we are on the 'User Policies' page, the active node should be 'Users' + * + * @param navigation_node $node + * @return navigation_node|null + */ + protected function scan_for_active_node(navigation_node $node): ?navigation_node { + if ($node->check_if_active()) { + return $node; // No need to continue, exit function. + } + + if ($node->children->count() > 0) { + foreach ($node->children as $child) { + if ($this->scan_for_active_node($child)) { + // If node is one of the new views then set the active node to the child. + if (!$node instanceof view) { + $node->make_active(); + $child->make_inactive(); + } else { + $child->make_active(); + } + return $node; // We have found the active node, set the parent status, no need to continue. + } + } + } + + return null; + } +} diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 4af9f550eca..c424375d21c 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -4470,7 +4470,7 @@ class settings_navigation extends navigation_node { if ($adminoptions->editcompletion) { // Add the course completion settings link $url = new moodle_url('/course/completion.php', array('id' => $course->id)); - $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, null, + $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, 'coursecompletion', new pix_icon('i/settings', '')); } diff --git a/lib/pagelib.php b/lib/pagelib.php index 6e5f3aa3798..481eb2e5814 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -26,6 +26,7 @@ */ defined('MOODLE_INTERNAL') || die(); +use core\navigation\views\secondary; /** * $PAGE is a central store of information about the current page we are @@ -79,6 +80,8 @@ defined('MOODLE_INTERNAL') || die(); * @property-read array $layout_options An arrays with options for the layout file. * @property-read array $legacythemeinuse True if the legacy browser theme is in use. * @property-read navbar $navbar The navbar object used to display the navbar + * @property-read secondary $secondarynav The secondary navigation object + * used to display the secondarynav 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. @@ -294,6 +297,12 @@ class moodle_page { */ protected $_flatnav = null; + /** + * @var secondary Contains the nav nodes that will appear + * in the secondary navigation. + */ + protected $_secondarynav = null; + /** * @var navbar Contains the navbar structure. */ @@ -783,6 +792,18 @@ class moodle_page { return $this->_flatnav; } + /** + * Returns the secondary navigation object + * @return secondary + */ + protected function magic_get_secondarynav() { + if ($this->_secondarynav === null) { + $this->_secondarynav = new secondary($this); + $this->_secondarynav->initialise(); + } + return $this->_secondarynav; + } + /** * Returns request IP address. * diff --git a/lib/tests/secondary_test.php b/lib/tests/secondary_test.php new file mode 100644 index 00000000000..e54cfc37acd --- /dev/null +++ b/lib/tests/secondary_test.php @@ -0,0 +1,133 @@ +. + +use core\navigation\views\secondary; + +/** + * Class core_secondary_testcase + * + * Unit test for the secondary nav view. + * + * @package core + * @category navigation + * @copyright 2021 onwards Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_secondary_testcase extends advanced_testcase { + /** + * Test the get_leaf_nodes function + * @param float $siteorder The order for the siteadmin node + * @param float $courseorder The order for the course node + * @param float $moduleorder The order for the module node + * @dataProvider leaf_nodes_order_provider + */ + public function test_get_leaf_nodes(float $siteorder, float $courseorder, float $moduleorder) { + global $PAGE; + + // Create a secondary navigation and populate with some dummy nodes. + $secondary = new secondary($PAGE); + $secondary->add('Site Admin', '#', secondary::TYPE_SETTING, null, 'siteadmin'); + $secondary->add('Course Admin', '#', secondary::TYPE_CUSTOM, null, 'courseadmin'); + $secondary->add('Module Admin', '#', secondary::TYPE_SETTING, null, 'moduleadmin'); + $nodes = [ + navigation_node::TYPE_SETTING => [ + 'siteadmin' => $siteorder, + 'moduleadmin' => $courseorder, + ], + navigation_node::TYPE_CUSTOM => [ + 'courseadmin' => $moduleorder, + ] + ]; + $expectednodes = [ + "$siteorder" => 'siteadmin', + "$courseorder" => 'moduleadmin', + "$moduleorder" => 'courseadmin', + ]; + + $method = new ReflectionMethod('core\navigation\views\secondary', 'get_leaf_nodes'); + $method->setAccessible(true); + $sortednodes = $method->invoke($secondary, $secondary, $nodes); + foreach ($sortednodes as $order => $node) { + $this->assertEquals($expectednodes[$order], $node->key); + } + } + + /** + * Data provider for test_get_leaf_nodes + * @return array + */ + public function leaf_nodes_order_provider(): array { + return [ + 'Initialise the order with whole numbers' => [3, 2, 1], + 'Initialise the order with a mix of whole and float numbers' => [2.1, 2, 1], + ]; + } + + /** + * Test the initialise in different contexts + * + * @param string $context The context to setup for - course, module, system + * @param string $expectedfirstnode The expected first node + * @dataProvider test_setting_initialise_provider + */ + public function test_setting_initialise(string $context, string $expectedfirstnode) { + global $PAGE, $SITE; + $this->resetAfterTest(); + $this->setAdminUser(); + $pagecourse = $SITE; + $pageurl = '/'; + switch ($context) { + case 'course': + $pagecourse = $this->getDataGenerator()->create_course(); + $contextrecord = context_course::instance($pagecourse->id, MUST_EXIST); + $pageurl = new moodle_url('/course/view.php', ['id' => $pagecourse->id]); + break; + case 'module': + $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); + break; + case 'system': + $contextrecord = context_system::instance(); + $PAGE->set_pagelayout('admin'); + $pageurl = new moodle_url('/admin/index.php'); + + } + $PAGE->set_url($pageurl); + $PAGE->set_course($pagecourse); + $PAGE->set_context($contextrecord); + + $node = new secondary($PAGE); + $node->initialise(); + $children = $node->get_children_key_list(); + $this->assertEquals($children[0], $expectedfirstnode); + } + + /** + * Data provider for the test_setting_initialise function + * @return array + */ + public function test_setting_initialise_provider(): array { + return [ + 'Testing in a course context' => ['course', 'coursehome'], + 'Testing in a module context' => ['module', 'modulepage'], + 'Testing in a site admin' => ['system', 'siteadminnode'], + ]; + } +} diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 3033d267e30..1f937f6bc17 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -1,6 +1,12 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. +=== 4.0 === +* New navigation classes to mimic the new navigation project. The existing navigation callbacks are still available and + will be called. The following behaviour will be the new standard for nodes added via callbacks: + - Module nodes added will be appended to the end and will appear within the hamburger option. + - Course nodes added will also be appended and appear within the 'More' option/page. + === 3.11 === * PHPUnit has been upgraded to 9.5 (see MDL-71036 for details). That comes with a few changes: diff --git a/report/competency/lib.php b/report/competency/lib.php index 497c91b0772..9d1a927478d 100644 --- a/report/competency/lib.php +++ b/report/competency/lib.php @@ -60,6 +60,6 @@ function report_competency_extend_navigation_module($navigation, $cm) { context_course::instance($cm->course))) { $url = new moodle_url('/report/competency/index.php', array('id' => $cm->course, 'mod' => $cm->id)); $name = get_string('pluginname', 'report_competency'); - $navigation->add($name, $url, navigation_node::TYPE_SETTING, null, null); + $navigation->add($name, $url, navigation_node::TYPE_SETTING, null, 'competencybreakdown'); } }