ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç primary.php000064400000020757152267530440006763 0ustar00. namespace core\navigation\views; use navigation_node; /** * Class primary. * * The primary navigation view is a combination of few components - navigation, output->navbar, * * @package core * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class primary extends view { /** * Initialise the primary navigation node */ public function initialise(): void { global $CFG; if (during_initial_install() || $this->initialised) { return; } $this->id = 'primary_navigation'; $showhomenode = empty($this->page->theme->removedprimarynavitems) || !in_array('home', $this->page->theme->removedprimarynavitems); // We do not need to change the text for the home/dashboard depending on the set homepage. if ($showhomenode) { $sitehome = $this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM, null, 'home', new \pix_icon('i/home', '')); } if (isloggedin() && !isguestuser()) { $homepage = get_home_page(); if ($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES) { // We need to stop automatic redirection. if ($showhomenode) { $sitehome->action->param('redirect', '0'); } } // Add the dashboard link. $showmyhomenode = !empty($CFG->enabledashboard) && (empty($this->page->theme->removedprimarynavitems) || !in_array('myhome', $this->page->theme->removedprimarynavitems)); if ($showmyhomenode) { $this->add(get_string('myhome'), new \moodle_url('/my/'), self::TYPE_SETTING, null, 'myhome', new \pix_icon('i/dashboard', '')); } // Add the mycourses link. $showcoursesnode = empty($this->page->theme->removedprimarynavitems) || !in_array('courses', $this->page->theme->removedprimarynavitems); if ($showcoursesnode) { $this->add(get_string('mycourses'), new \moodle_url('/my/courses.php'), self::TYPE_ROOTNODE, null, 'mycourses'); } } // Add the calendar link only for guest users. if (isguestuser()) { $this->add(get_string('calendar', 'calendar'), new \moodle_url('/calendar/view.php?view=month'), self::TYPE_ROOTNODE, null, 'calendar'); } $showsiteadminnode = empty($this->page->theme->removedprimarynavitems) || !in_array('siteadminnode', $this->page->theme->removedprimarynavitems); if ($showsiteadminnode && $node = $this->get_site_admin_node()) { // We don't need everything from the node just the initial link. $this->add($node->text, $node->action(), self::TYPE_SITE_ADMIN, null, 'siteadminnode', $node->icon); } // Allow plugins to add nodes to the primary navigation. $hook = new \core\hook\navigation\primary_extend($this); \core\di::get(\core\hook\manager::class)->dispatch($hook); // Search and set the active node. $this->set_active_node(); $this->initialised = true; } /** * Get the site admin node if available. * * @return navigation_node|null */ private function get_site_admin_node(): ?navigation_node { // Add the site admin node. We are using the settingsnav so as to avoid rechecking permissions again. $settingsnav = $this->page->settingsnav; $node = $settingsnav->find('siteadministration', self::TYPE_SITE_ADMIN); if (!$node) { // Try again. This node can exist with 2 different keys. $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN); } return $node ?: null; } /** * Find and set the active node. Initially searches based on URL/explicitly set active node. * If nothing is found, it checks the following: * - If the node is a site page, set 'Home' as active * - If within a course context, set 'My courses' as active * - If within a course category context, set 'Site Admin' (if available) else set 'Home' * - Else if available set site admin as active * - Fallback, set 'Home' as active */ private function set_active_node(): void { global $SITE; $activenode = $this->search_and_set_active_node($this); // If we haven't found an active node based on the standard search. Follow the criteria above. if (!$activenode) { $children = $this->get_children_key_list(); $navactivenode = $this->page->navigation->find_active_node(); $activekey = 'home'; if (isset($navactivenode->parent) && $navactivenode->parent->text == get_string('sitepages')) { $activekey = 'home'; } else if (in_array($this->context->contextlevel, [CONTEXT_COURSE, CONTEXT_MODULE])) { if ($this->page->course->id != $SITE->id) { $activekey = 'courses'; } } else if (in_array('siteadminnode', $children) && $node = $this->get_site_admin_node()) { if ($this->context->contextlevel == CONTEXT_COURSECAT || $node->search_for_active_node(URL_MATCH_EXACT)) { $activekey = 'siteadminnode'; } } else if (in_array('calendar', $children) && $node = $this->find('calendar', self::TYPE_ROOTNODE)) { if ($node->search_for_active_node(URL_MATCH_BASE)) { $activekey = 'calendar'; } } if ($activekey && $activenode = $this->find($activekey, null)) { $activenode->make_active(); } } } /** * Searches all children for the matching active node * * This method recursively traverse through the node tree to * find the node to activate/highlight: * 1. If the user had set primary node key to highlight, it * tries to match this key with the node(s). Hence it would * travel all the nodes. * 2. If no primary key is provided by the dev, then it would * check for the active node set in the tree. * * @param navigation_node $node * @param array $actionnodes navigation nodes array to set active and inactive. * @return navigation_node|null */ private function search_and_set_active_node(navigation_node $node, array &$actionnodes = []): ?navigation_node { global $PAGE; $activekey = $PAGE->get_primary_activate_tab(); if ($activekey) { if ($node->key && ($activekey === $node->key)) { return $node; } } else if ($node->check_if_active()) { return $node; } foreach ($node->children as $child) { $outcome = $this->search_and_set_active_node($child, $actionnodes); if ($outcome !== null) { $outcome->make_active(); $actionnodes['active'] = $outcome; if ($activekey === null) { return $actionnodes['active']; } } else { // If the child is active then make it inactive. if ($child->isactive) { $actionnodes['set_inactive'][] = $child; } } } // If we have successfully found an active node then reset any other nodes to inactive. if (isset($actionnodes['set_inactive']) && isset($actionnodes['active'])) { foreach ($actionnodes['set_inactive'] as $inactivenode) { $inactivenode->make_inactive(); } $actionnodes['set_inactive'] = []; } return ($actionnodes['active'] ?? null); } } view.php000064400000012502152267530440006237 0ustar00. 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; /** @var navigation_node $activenode A string identifier for the active node*/ public $activenode; /** * 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; $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"] = $nodesordered["$location"] ?? $node; } } } return $nodesordered; } /** * Scan the given node for the active node. It starts first with a strict search and then switches to a base search if * required. * * @param navigation_node $node The node to scan. * @return navigation_node|null The active node or null. */ protected function scan_for_active_node(navigation_node $node): ?navigation_node { $result = $this->active_node_scan($node); if (!is_null($result)) { return $result; } else { return $this->active_node_scan($node, URL_MATCH_BASE); } } /** * 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 adjust the strictness * 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 * @param int $strictness How stict to be with the scan for the active node. * @return navigation_node|null */ protected function active_node_scan(navigation_node $node, int $strictness = URL_MATCH_EXACT): ?navigation_node { $result = null; $activekey = $this->page->get_secondary_active_tab(); if ($activekey) { if ($node->key && $activekey === $node->key) { return $node; } } else if ($node->check_if_active($strictness)) { return $node; // No need to continue, exit function. } foreach ($node->children as $child) { if ($this->active_node_scan($child, $strictness)) { // If node is one of the new views then set the active node to the child. if (!$node instanceof view) { $node->make_active(); $result = $node; } else { $child->make_active(); $this->activenode = $child; $result = $child; } // If the secondary active tab not set then just return the result (fallback). if ($activekey === null) { return $result; } } else { // Make sure to reset the active state. $child->make_inactive(); } } return $result; } } secondary.php000064400000144572152267530440007271 0ustar00. namespace core\navigation\views; use navigation_node; use url_select; use settings_navigation; /** * 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 { /** @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; /** @var navigation_node The course overflow node. */ protected $courseoverflownode = null; /** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */ protected $overflowselected = null; /** * 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' => 4, 'questionbank' => 5, ], self::TYPE_SETTING => [ 'editsettings' => 0, 'review' => 1.1, 'manageinstances' => 1.2, 'groups' => 1.3, 'override' => 1.4, 'roles' => 1.5, 'permissions' => 1.6, 'otherusers' => 1.7, 'gradebooksetup' => 2.1, 'outcomes' => 2.2, 'coursecompletion' => 7, 'coursebadges' => 8.1, 'newbadge' => 8.2, 'filtermanagement' => 10, 'unenrolself' => 11, 'coursetags' => 12, 'download' => 13, 'contextlocking' => 14, ], ]; $nodes['navigation'] = [ self::TYPE_CONTAINER => [ 'participants' => 1, 'courseoverview' => 3, ], self::TYPE_SETTING => [ 'grades' => 2, 'badgesview' => 8, 'competencies' => 9, 'communication' => 15, ], self::TYPE_CUSTOM => [ 'contentbank' => 6, 'participants' => 1, // In site home, 'participants' is classified differently. ], ]; 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, "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific. "mod_{$this->page->activityname}_groupoverrides" => 4, 'roleassign' => 7.2, 'filtermanage' => 6, 'roleoverride' => 7, 'rolecheck' => 7.1, 'logreport' => 8, 'backup' => 9, 'restore' => 10, 'competencybreakdown' => 11, 'sendtomoodlenet' => 16, ], self::TYPE_CUSTOM => [ 'advgrading' => 2, 'contentbank' => 12, ], ]; } /** * Defines the default structure for the secondary nav in a category context. * * In a category 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_category_mapping(): array { return [ self::TYPE_SETTING => [ 'edit' => 1, 'permissions' => 2, 'roles' => 2.1, 'rolecheck' => 2.2, ] ]; } /** * 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_category_more_menu_nodes(): array { return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse']; } /** * 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', "mod_{$this->page->activityname}_useroverrides", "mod_{$this->page->activityname}_groupoverrides"]; } /** * 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. * * 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; $this->headertitle = get_string('menu'); $defaultmoremenunodes = []; $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES; switch ($context->contextlevel) { case CONTEXT_COURSE: $this->headertitle = get_string('courseheader'); if ($this->page->course->format === 'singleactivity') { $this->load_single_activity_course_navigation(); } else { $this->load_course_navigation(); $defaultmoremenunodes = $this->get_default_course_more_menu_nodes(); } break; case CONTEXT_MODULE: $this->headertitle = get_string('activityheader'); if ($this->page->course->format === 'singleactivity') { $this->load_single_activity_course_navigation(); } else { $this->load_module_navigation($this->page->settingsnav); $defaultmoremenunodes = $this->get_default_module_more_menu_nodes(); } break; case CONTEXT_COURSECAT: $this->headertitle = get_string('categoryheader'); $this->load_category_navigation(); $defaultmoremenunodes = $this->get_default_category_more_menu_nodes(); break; case CONTEXT_SYSTEM: $this->headertitle = get_string('homeheader'); $this->load_admin_navigation(); // If the site administration navigation was generated after load_admin_navigation(). if ($this->has_children()) { // Do not explicitly limit the number of navigation nodes displayed in the site administration // navigation menu. $maxdisplayednodes = null; } $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes(); break; } $this->remove_unwanted_nodes($this); // Don't need to show anything if only the view node is available. Remove it. if ($this->children->count() == 1) { $this->children->remove('modulepage'); } // Force certain navigation nodes to be displayed in the "more" menu. $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes); // Search and set the active node. $this->scan_for_active_node($this); $this->initialised = true; } /** * 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 and has no action then no further processing is needed. $newnode = null; if ($node->has_children() && !$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 if ($node->has_action()) { $newnode = $node; } return $newnode; } /** * Recursive call to add all custom navigation nodes to secondary * * @param navigation_node $node The node which should be added to secondary * @param navigation_node $basenode The original parent node * @param navigation_node|null $root The parent node nodes are to be added/removed to. * @param bool $forceadd Whether or not to bypass the external action check and force add all nodes */ protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode, ?navigation_node $root = null, bool $forceadd = false) { $root = $root ?? $this; // Add the first node. if ($node->has_action() && !$this->get($node->key)) { $root->add_node(clone $node); } // If the node has an external action add all children to the secondary navigation. if (!$node->has_internal_action() || $forceadd) { if ($node->has_children()) { foreach ($node->children as $child) { if ($child->has_children()) { $this->add_external_nodes_to_secondary($child, $basenode, $root, true); } else if ($child->has_action() && !$this->get($child->key)) { // Check whether the basenode matches a child's url. // This would have happened in get_first_action_for_node. // In these cases, we prefer the specific child content. if ($basenode->has_action() && $basenode->action()->compare($child->action())) { $root->children->remove($basenode->key, $basenode->type); } $root->add_node(clone $child); } } } } } /** * Returns a list of all expected nodes in the course administration. * * @return array An array of keys for navigation nodes in the course administration. */ protected function get_expected_course_admin_nodes(): array { $expectednodes = []; foreach ($this->get_default_course_mapping()['settings'] as $value) { foreach ($value as $nodekey => $notused) { $expectednodes[] = $nodekey; } } foreach ($this->get_default_course_mapping()['navigation'] as $value) { foreach ($value as $nodekey => $notused) { $expectednodes[] = $nodekey; } } $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges']; $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset']; $expectednodes = array_merge($expectednodes, $othernodes); $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes); return $expectednodes; } /** * 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. * * @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children. * If not explicitly defined, the nodes will be added to the secondary root * node by default. */ protected function load_course_navigation(?navigation_node $rootnode = null): void { global $SITE; $rootnode = $rootnode ?? $this; $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; if ($course->id == $SITE->id) { $firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'. $frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node. $settingsnav = $frontpage ?: $settingsnav; $courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node. } else { $firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'. $courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'. } // Add the known nodes from settings and navigation. $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, $rootnode); // Try to get any custom nodes defined by plugins, which may include containers. if ($courseadminnode) { $expectedcourseadmin = $this->get_expected_course_admin_nodes(); foreach ($courseadminnode->children as $other) { if (array_search($other->key, $expectedcourseadmin, true) === false) { $othernode = $this->get_first_action_for_node($other); $recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other; // Get the first node and check whether it's been added already. // Also check if the first node is an external link. If it is, add all children. $this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode); } } } // Add the respective first node, provided there are other nodes included. if (!empty($nodekeys = $rootnode->children->get_key_list())) { $rootnode->add_node( navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]), self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys) ); } // Allow plugins to add nodes to the secondary navigation. $hook = new \core\hook\navigation\secondary_extend($this); \core\di::get(\core\hook\manager::class)->dispatch($hook); } /** * Gets the overflow navigation nodes for the course administration category. * * @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained. * If not explicitly defined, the nodes will be obtained from the secondary root * node by default. * @return navigation_node The course overflow nodes. */ protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node { global $SITE; $rootnode = $rootnode ?? $this; // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being // present the second time this is called. if (isset($this->courseoverflownode)) { return $this->courseoverflownode; } // Start with getting the base node for the front page or the course. $node = null; if ($this->page->course->id == $SITE->id) { $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING); } else { $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); } $coursesettings = $node ? $node->get_children_key_list() : []; $thissettings = $rootnode->get_children_key_list(); $diff = array_diff($coursesettings, $thissettings); // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup, // grades - outcomes). $shortdiff = array_filter($diff, function($value) { return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' || $value == 'outcomes'); }); // Permissions may be in play here that ultimately will show no overflow. if (empty($shortdiff)) { return null; } $firstitem = array_shift($shortdiff); $navnode = $node->get($firstitem); foreach ($shortdiff as $key) { $courseadminnodes = $node->get($key); if ($courseadminnodes) { if ($courseadminnodes->parent->key == $node->key) { $navnode->add_node($courseadminnodes); } } } $this->courseoverflownode = $navnode; return $navnode; } /** * 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; } /** * Recursively search a node and its children for a node matching the key string $key. * * @param navigation_node $node the navigation node to check. * @param string $key the key of the node to match. * @return navigation_node|null node if found, otherwise null. */ protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node { if ($node->has_action()) { // Check this node first. if ($node->key == $key) { return $node; } } if ($node->has_children()) { foreach ($node->children as $child) { $result = $this->node_matches_key_string($child, $key); if ($result) { return $result; } } } return null; } /** * Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key. * * Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php' * will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match. * * This method allows a page to choose a specific node to match, which is useful in cases where the page knows its URL won't * match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page * knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node". * * E.g. * - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore) * - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the * main restore landing page). * - The backup/restore.php page calls: * $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore'); * and when the page is loaded, the 'Restore' node be presented as the selected node. * * @param string $nodekey The string key of the overflow node to match. */ public function set_overflow_selected_node(string $nodekey): void { $this->overflowselected = $nodekey; } /** * Returns a url_select object with overflow navigation nodes. * This looks to see if the current page is within the course administration, or some other page that requires an overflow * select object. * * @return url_select|null The overflow menu data. */ public function get_overflow_menu_data(): ?url_select { if (!$this->page->get_navigation_overflow_state()) { return null; } $issingleactivitycourse = $this->page->course->format === 'singleactivity'; $rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this; $activenode = $this->find_active_node(); $incourseadmin = false; $activeleafnode = $this->page->settingsnav->find_active_node(); $parentnode = $activeleafnode->parent ?? null; if ($issingleactivitycourse && $parentnode && $parentnode->key === 'quiz_report') { $activenode = $parentnode; } if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) { // Could be in the course admin section. $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); if (!$courseadmin) { return null; } $activenode = $courseadmin->find_active_node(); if (!$activenode) { return null; } $incourseadmin = true; } if ($activenode->key === 'coursereuse' || $incourseadmin) { $courseoverflownode = $this->get_course_overflow_nodes($rootnode); if (is_null($courseoverflownode)) { return null; } if ($incourseadmin) { // Validate whether the active node is part of the expected course overflow nodes. if (($activenode->key !== $courseoverflownode->key) && !$courseoverflownode->find($activenode->key, $activenode->type)) { return null; } } $menuarray = static::create_menu_element([$courseoverflownode]); if ($activenode->key != 'coursereuse') { $inmenu = false; foreach ($menuarray as $key => $value) { if ($this->page->url->out(false) == $key) { $inmenu = true; } } if (!$inmenu) { return null; } } // If the page has explicitly set the overflow node it would like selected, find and use that node. if ($this->overflowselected) { $selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected); $selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null; } $menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null); $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'visually-hidden']); return $menuselect; } else { return $this->get_other_overflow_menu_data($activenode); } } /** * Gets overflow menu data for third party plugin settings. * * @param navigation_node $activenode The node to gather the children for to put into the overflow menu. * @return url_select|null The overflow menu in a url_select object. */ protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select { if (!$activenode->has_action()) { return null; } if (!$activenode->has_children()) { return null; } // If the setting is extending the course navigation then the page being redirected to should be in the course context. // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages // that are redirected to, be in the course context or module context depending on which callback was used. // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found. // The request of third party developers is to keep their settings within the specified context. if ($this->page->context->contextlevel != CONTEXT_COURSE && $this->page->context->contextlevel != CONTEXT_MODULE && $this->page->context->contextlevel != CONTEXT_COURSECAT) { return null; } // These areas have their own code to retrieve added plugin navigation nodes. if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') { return null; } $menunode = $this->page->settingsnav->find($activenode->key, null); if (!$menunode instanceof navigation_node) { return null; } // 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 = static::create_menu_element([$menunode], false); $urlselect = new url_select($selectdata, $matchednode->action->out(false), null); $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'visually-hidden']); return $urlselect; } /** * 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. * * @param settings_navigation $settingsnav The settings navigation object related to the module page * @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children. * If not explicitly defined, the nodes will be added to the secondary root * node by default. */ protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void { $rootnode = $rootnode ?? $this; $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING); $nodes = $this->get_default_module_mapping(); if ($mainnode) { $url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php', ['id' => $settingsnav->get_page()->cm->id]); $setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE); $node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $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, $rootnode); // We have finished inserting the initial structure. // Populate the menu with the rest of the nodes available. $this->load_remaining_nodes($mainnode, $nodes, $rootnode); } } /** * Load the course category navigation. */ protected function load_category_navigation(): void { $settingsnav = $this->page->settingsnav; $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER); $nodes = $this->get_default_category_mapping(); if ($mainnode) { $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]); $this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain'); // 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 { global $PAGE, $SITE; $settingsnav = $this->page->settingsnav; $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN); // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding // tab. Note this value refers to being present on the page itself, before a search has been performed. $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS); if ($node) { $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode'); if ($isadminsearchpage) { $siteadminnode->action = false; $siteadminnode->tab = "#link$node->key"; } else { $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key"); } foreach ($node->children as $child) { if ($child->display && !$child->is_short_branch()) { // Mimic the current boost behaviour and pass down anchors for the tabs. if ($isadminsearchpage) { $child->action = false; $child->tab = "#link$child->key"; } else { $child->action = new \moodle_url("/admin/search.php", [], "link$child->key"); } $this->add_node(clone $child); } else { $siteadminnode->add_node(clone $child); } } } } /** * Adds the indexed nodes to the current view or a given node. 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. * @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly * defined, the nodes will be added to the secondary root node by default. */ protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void { $rootnode = $rootnode ?? $this; 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(clone $node); } } else { $rootnode->add_node(clone $node); } } } /** * Find the remaining nodes that need to be loaded into secondary based on the current context or a given node. * * @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 * @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not * explicitly defined, the nodes will be added to the secondary root node by * default. */ protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap, ?navigation_node $rootnode = null): void { $flattenednodes = []; $rootnode = $rootnode ?? $this; 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, true) && $leftovernode = $completenode->get($key)) { // Check for nodes with children and potentially no action to direct to. if ($leftovernode->has_children()) { $leftovernode = $this->get_first_action_for_node($leftovernode); } // We have found the first node with an action. if ($leftovernode) { $this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode); } } } } /** * 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 * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation */ protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) { // 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 defined). if (in_array($child->key, $defaultmoremenunodes) || (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) { // Force the node and its children into the "more" menu. $child->set_force_into_more_menu(true); continue; } $displayednodescount++; } } /** * Recursively remove navigation nodes that should not be displayed in the secondary navigation. * * @param navigation_node $node The starting navigation node. */ protected function remove_unwanted_nodes(navigation_node $node) { foreach ($node->children as $child) { if (!$child->showinsecondarynavigation) { $child->remove(); continue; } if (!empty($child->children)) { $this->remove_unwanted_nodes($child); } } } /** * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a * url_select element. * * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu. * @param bool $forceheadings Whether the returned array should be forced to use headings. * @return array|null A url select element for navigating through the navigation nodes. */ public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array { if (empty($navigationnodes)) { return null; } // If one item, do we put this into a url_select? if (count($navigationnodes) < 2) { // Check if there are children. $navnode = array_shift($navigationnodes); $menudata = []; if (!$navnode->has_children()) { // Just one item. if (!$navnode->has_action()) { return null; } $menudata[$navnode->action->out(false)] = static::format_node_text($navnode); } else { if (static::does_menu_need_headings($navnode) || $forceheadings) { // Let's do headings. $menudata = static::get_headings_nav_array($navnode); } else { // Simple flat nav. $menudata = static::get_flat_nav_array($navnode); } } return $menudata; } else { // We have more than one navigation node to handle. Put each node in it's own heading. $menudata = []; $titledata = []; foreach ($navigationnodes as $navigationnode) { if ($navigationnode->has_children()) { $menuarray = []; // Add a heading and flatten out everything else. if ($navigationnode->has_action()) { $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] = static::format_node_text($navigationnode); $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode); } else { $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode); } $titledata += $menuarray; } else { // Add with no heading. if (!$navigationnode->has_action()) { return null; } $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode); } } $menudata += [$titledata]; return $menudata; } } /** * Recursively goes through the provided navigation node and returns a flat version. * * @param navigation_node $navigationnode The navigationnode. * @return array The whole tree flat. */ protected static function get_whole_tree_flat(navigation_node $navigationnode): array { $nodes = []; foreach ($navigationnode->children as $child) { if ($child->has_action()) { $nodes[$child->action->out()] = $child->text; } if ($child->has_children()) { $childnodes = static::get_whole_tree_flat($child); $nodes = array_merge($nodes, $childnodes); } } return $nodes; } /** * Checks to see if the provided navigation node has children and determines if we want headings for a url select element. * * @param navigation_node $navigationnode The navigation node we are checking. * @return bool Whether we want headings or not. */ protected static function does_menu_need_headings(navigation_node $navigationnode): bool { if (!$navigationnode->has_children()) { return false; } foreach ($navigationnode->children as $child) { if ($child->has_children()) { return true; } } return false; } /** * Takes the navigation node and returns it in a flat fashion. This is not recursive. * * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure. * @return array The flat navigation array. */ protected static function get_flat_nav_array(navigation_node $navigationnode): array { $menuarray = []; if ($navigationnode->has_action()) { $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode); } foreach ($navigationnode->children as $child) { if ($child->has_action()) { $menuarray[$child->action->out(false)] = static::format_node_text($child); } } return $menuarray; } /** * For any navigation node that we have determined needs headings we return a more tree like array structure. * * @param navigation_node $navigationnode The navigation node to use for the formatted array structure. * @return array The headings navigation array structure. */ protected static function get_headings_nav_array(navigation_node $navigationnode): array { $menublock = []; // We know that this single node has headings, so grab this for the first heading. $firstheading = []; if ($navigationnode->has_action()) { $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] = static::format_node_text($navigationnode); $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock); } else { $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock); } return [$firstheading + $menublock]; } /** * Recursively goes and gets all children nodes. * * @param navigation_node $node The node to get the children of. * @param array $menublock Used to put all child nodes in its own container. * @return array The additional child nodes. */ protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array { $nodes = []; foreach ($node->children as $child) { if (!$child->has_children()) { if (!$child->has_action()) { continue; } $nodes[$child->action->out(false)] = static::format_node_text($child); } else { $newarray = []; if ($child->has_action()) { $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child); $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock); } else { $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock); } $menublock += $newarray; } } return $nodes; } /** * Returns the navigation node text in a string. * * @param navigation_node $navigationnode The navigationnode to return the text string of. * @return string The navigation node text string. */ protected static function format_node_text(navigation_node $navigationnode): string { return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text; } /** * Load the single activity course secondary navigation. */ protected function load_single_activity_course_navigation(): void { $page = $this->page; $course = $page->course; // Create 'Course' navigation node. $coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course'); $this->load_course_navigation($coursesecondarynode); // Remove the unnecessary 'Course' child node generated in load_course_navigation(). $coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE); if (!empty($coursehomenode)) { $coursehomenode->remove(); } // Add the 'Course' node to the secondary navigation only if this node has children nodes. if (count($coursesecondarynode->children) > 0) { $this->add_node($coursesecondarynode); // Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu' // property to true. This is required to force the template to output these items within a dropdown menu. $coursesecondarynode->showchildreninsubmenu = true; } // Create 'Activity' navigation node. $activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity'); // We should display the module related navigation in the course context as well. Therefore, we need to // re-initialize the page object and manually set the course module to the one that it is currently visible in // the course in order to obtain the required module settings navigation. if ($page->context instanceof \context_course) { $this->page->set_secondary_active_tab($coursesecondarynode->key); // Get the currently used module in the single activity course. $module = current(array_filter(get_course_mods($course->id), function ($module) { return $module->visible == 1; })); // If the default module for the single course format has not been set yet, skip displaying the module // related navigation in the secondary navigation. if (!$module) { return; } $page = new \moodle_page(); $page->set_cm($module, $course); $page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id])); } $this->load_module_navigation($page->settingsnav, $activitysecondarynode); // Add the 'Activity' node to the secondary navigation only if this node has more that one child node. if (count($activitysecondarynode->children) > 1) { // Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within // a dropdown menu. $activitysecondarynode->showchildreninsubmenu = true; $this->add_node($activitysecondarynode); if ($this->context instanceof \context_module) { $this->page->set_secondary_active_tab($activitysecondarynode->key); } } else { // Otherwise, add the 'View activity' node to the secondary navigation. $viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]); $this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage'); if ($this->context instanceof \context_module) { $this->page->set_secondary_active_tab('modulepage'); } } } } primary_test.php000064400000015517152267621250010020 0ustar00. namespace core\navigation\views; use navigation_node; use ReflectionMethod; /** * Class core_primary_testcase * * Unit test for the primary nav view. * * @package core * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class primary_test extends \advanced_testcase { /** * Test the initialise in different contexts * * @param string $usertype The user to setup for - admin, guest, regular user * @param string $expected The expected nodes * @dataProvider setting_initialise_provider */ public function test_setting_initialise($usertype, $expected): void { global $PAGE; $PAGE->set_url("/"); $this->resetAfterTest(); if ($usertype == 'admin') { $this->setAdminUser(); } else if ($usertype == 'guest') { $this->setGuestUser(); } else { $user = $this->getDataGenerator()->create_user(); $this->setUser($user); } $node = new primary($PAGE); $node->initialise(); $children = $node->get_children_key_list(); $this->assertEquals($expected, $children); } /** * Data provider for the test_setting_initialise function */ public static function setting_initialise_provider(): array { return [ 'Testing as a guest user' => ['guest', ['home', 'calendar']], 'Testing as an admin' => ['admin', ['home', 'myhome', 'mycourses', 'siteadminnode']], 'Testing as a regular user' => ['user', ['home', 'myhome', 'mycourses']] ]; } /** * Get the nav tree initialised to test search_and_set_active_node. * * @param string|null $seturl The url set for $PAGE. * @return navigation_node The initialised nav tree. */ private function get_tree_initilised_to_set_activate(?string $seturl = null): navigation_node { $node = new navigation_node('My test node'); $node->type = navigation_node::TYPE_SYSTEM; $node->add('first child', null, navigation_node::TYPE_CUSTOM, 'firstchld', 'firstchild'); $child2 = $node->add('second child', null, navigation_node::TYPE_COURSE, 'secondchld', 'secondchild'); $child3 = $node->add('third child', null, navigation_node::TYPE_CONTAINER, 'thirdchld', 'thirdchild'); $node->add('fourth child', null, navigation_node::TYPE_ACTIVITY, 'fourthchld', 'fourthchld'); $node->add('fifth child', '/my', navigation_node::TYPE_CATEGORY, 'fifthchld', 'fifthchild'); // If seturl is null then set actionurl of child6 to '/'. if ($seturl === null) { $child6actionurl = new \moodle_url('/'); } else { // If seturl is provided then set actionurl of child6 to '/foo'. $child6actionurl = new \moodle_url('/foo'); } $child6 = $child2->add('sixth child', $child6actionurl, navigation_node::TYPE_COURSE, 'sixthchld', 'sixthchild'); // Activate the sixthchild node. $child6->make_active(); $child2->add('seventh child', null, navigation_node::TYPE_COURSE, 'seventhchld', 'seventhchild'); $child8 = $child2->add('eighth child', null, navigation_node::TYPE_CUSTOM, 'eighthchld', 'eighthchild'); $child8->add('nineth child', null, navigation_node::TYPE_CUSTOM, 'ninethchld', 'ninethchild'); $child3->add('tenth child', null, navigation_node::TYPE_CUSTOM, 'tenthchld', 'tenthchild'); return $node; } /** * Testing search_and_set_active_node. * * @param string $expectedkey Expected key of the node, if set. * @param string|null $key The key of the node to activate. * @param string|null $seturl Set the url for $PAGE. * @return void * @dataProvider search_and_set_active_node_provider */ public function test_search_and_set_active_node(string $expectedkey, ?string $key = null, ?string $seturl = null): void { global $PAGE; if ($seturl !== null) { navigation_node::override_active_url(new \moodle_url($seturl)); } else { $PAGE->set_url('/'); navigation_node::override_active_url(new \moodle_url('/')); } if ($key !== null) { $PAGE->set_primary_active_tab($key); } $node = $this->get_tree_initilised_to_set_activate($seturl); $primary = new primary($PAGE); $method = new ReflectionMethod('core\navigation\views\primary', 'search_and_set_active_node'); $result = $method->invoke($primary, $node); $sixthchildnode = $node->find('sixthchild', navigation_node::TYPE_COURSE); if ($expectedkey !== '') { $this->assertInstanceOf('navigation_node', $result); $this->assertEquals($result->isactive, true); $this->assertEquals($result->key, $expectedkey); // Test the state of sixthchild, based on $expectedkey. if ($expectedkey !== 'sixthchild') { $this->assertFalse($sixthchildnode->isactive); } else { $this->assertTrue($sixthchildnode->isactive); } } else { $this->assertNull($result); $this->assertTrue($sixthchildnode->isactive); } } /** * Data provider for test_search_and_set_active_node * * @return array */ public static function search_and_set_active_node_provider(): array { return [ 'Test by activating node which is part of the tree' => ['tenthchild', 'tenthchild'], 'Do not change the state of any nodes of the tree' => ['sixthchild'], 'Test by setting an empty string as node key to activate' => ['sixthchild', ''], 'Activate a node which does not exist in the tree' => ['', 'foobar'], 'Activate the leaf node of the tree' => ['ninethchild', 'ninethchild'], 'Test by changing the $PAGE url which is different from action url of child6' => ['', null, '/foobar'], 'Test by having $PAGE url and child6 action url same' => ['sixthchild', null, '/foo'], ]; } } secondary_test.php000064400000116107152267621250010321 0ustar00. namespace core\navigation\views; use booktool_print\output\renderer; use navigation_node; use ReflectionMethod; use moodle_url; /** * 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 */ final class secondary_test 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): void { 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'); $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 static 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 * @param string $header The expected string * @param string $activenode The expected active node * @param string $courseformat The used course format (only applicable in the course and module context). * @return void * @dataProvider setting_initialise_provider */ public function test_setting_initialise(string $context, string $expectedfirstnode, string $header, string $activenode, string $courseformat = 'topics'): void { global $PAGE, $SITE; $this->resetAfterTest(); $this->setAdminUser(); $pagecourse = $SITE; $pageurl = '/'; switch ($context) { case 'course': $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]); $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST); if ($courseformat === 'singleactivity') { $pageurl = new \moodle_url('/course/edit.php', ['id' => $pagecourse->id]); } else { $pageurl = new \moodle_url('/course/view.php', ['id' => $pagecourse->id]); } break; case 'module': $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]); $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->id]); $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); navigation_node::override_active_url($pageurl); $PAGE->set_course($pagecourse); $PAGE->set_context($contextrecord); $node = new secondary($PAGE); $node->initialise(); $children = $node->get_children_key_list(); $this->assertEquals($expectedfirstnode, $children[0]); $this->assertEquals(get_string($header), $node->headertitle); $this->assertEquals($activenode, $node->activenode->text); } /** * Data provider for the test_setting_initialise function * @return array */ public static function setting_initialise_provider(): array { return [ 'Testing in a course context' => ['course', 'coursehome', 'courseheader', 'Course'], 'Testing in a course context using a single activity course format' => ['course', 'course', 'courseheader', 'Course', 'singleactivity'], 'Testing in a module context' => ['module', 'modulepage', 'activityheader', 'Assignment'], 'Testing in a module context using a single activity course format' => ['module', 'course', 'activityheader', 'Activity', 'singleactivity'], 'Testing in a site admin' => ['system', 'siteadminnode', 'homeheader', 'General'], ]; } /** * Get the nav tree initialised to test active_node_scan. * * This is to test the secondary nav with navigation_node instance. * * @param string|null $seturl The url set for $PAGE. * @return navigation_node The initialised nav tree. */ private function get_tree_initilised_to_set_activate(?string $seturl = null): navigation_node { global $PAGE; $node = new secondary($PAGE); $node->key = 'mytestnode'; $node->type = navigation_node::TYPE_SYSTEM; $node->add('first child', null, navigation_node::TYPE_CUSTOM, 'firstchld', 'firstchild'); $child2 = $node->add('second child', null, navigation_node::TYPE_COURSE, 'secondchld', 'secondchild'); $child3 = $node->add('third child', null, navigation_node::TYPE_CONTAINER, 'thirdchld', 'thirdchild'); $node->add('fourth child', null, navigation_node::TYPE_ACTIVITY, 'fourthchld', 'fourthchld'); $node->add('fifth child', '/my', navigation_node::TYPE_CATEGORY, 'fifthchld', 'fifthchild'); // If seturl is null then set actionurl of child6 to '/'. if ($seturl === null) { $child6actionurl = new \moodle_url('/'); } else { // If seturl is provided then set actionurl of child6 to '/foo'. $child6actionurl = new \moodle_url('/foo'); } $child6 = $child2->add('sixth child', $child6actionurl, navigation_node::TYPE_COURSE, 'sixthchld', 'sixthchild'); // Activate the sixthchild node. $child6->make_active(); $child2->add('seventh child', null, navigation_node::TYPE_COURSE, 'seventhchld', 'seventhchild'); $child8 = $child2->add('eighth child', null, navigation_node::TYPE_CUSTOM, 'eighthchld', 'eighthchild'); $child8->add('nineth child', null, navigation_node::TYPE_CUSTOM, 'ninethchld', 'ninethchild'); $child3->add('tenth child', null, navigation_node::TYPE_CUSTOM, 'tenthchld', 'tenthchild'); return $node; } /** * Testing active_node_scan on navigation_node instance. * * @param string $expectedkey The expected node key. * @param string|null $key The key set by user using set_secondary_active_tab. * @param string|null $seturl The url set by user. * @return void * @dataProvider active_node_scan_provider */ public function test_active_node_scan(string $expectedkey, ?string $key = null, ?string $seturl = null): void { global $PAGE; if ($seturl !== null) { navigation_node::override_active_url(new \moodle_url($seturl)); } else { $PAGE->set_url('/'); navigation_node::override_active_url(new \moodle_url('/')); } if ($key !== null) { $PAGE->set_secondary_active_tab($key); } $node = $this->get_tree_initilised_to_set_activate($seturl); $secondary = new secondary($PAGE); $method = new ReflectionMethod('core\navigation\views\secondary', 'active_node_scan'); $result = $method->invoke($secondary, $node); if ($expectedkey !== '') { $this->assertInstanceOf('navigation_node', $result); $this->assertEquals($expectedkey, $result->key); } else { $this->assertNull($result); } } /** * Data provider for the active_node_scan_provider * * @return array */ public static function active_node_scan_provider(): array { return [ 'Test by activating node adjacent to root node' => ['firstchild', 'firstchild'], 'Activate a grand child node of the root' => ['thirdchild', 'tenthchild'], 'When child node is activated the parent node is activated and returned' => ['secondchild', null], 'Test by setting an empty string as node key to activate' => ['secondchild', ''], 'Activate a node which does not exist in the tree' => ['', 'foobar'], 'Activate the leaf node of the tree' => ['secondchild', 'ninethchild', null, true], 'Changing the $PAGE url which is different from action url of child6 and not setting active tab manually' => ['', null, '/foobar'], 'Having $PAGE url and child6 action url same and not setting active tab manually' => ['secondchild', null, '/foo'], ]; } /** * 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 int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation * @param array $expecedmoremenunodes The array containing the keys of the expected navigation nodes which are * forced into the "more" menu * @dataProvider force_nodes_into_more_menu_provider */ public function test_force_nodes_into_more_menu(array $secondarynavnodesdata, array $defaultmoremenunodes, ?int $maxdisplayednodes, array $expecedmoremenunodes): void { 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->invoke($secondary, $defaultmoremenunodes, $maxdisplayednodes); $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 static function 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', ], 5, [ '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', ], 5, [ 'navnode2', 'navnode4', ], ], 'The max display limit of navigation nodes is not defined; ' . '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', ], null, [ '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'], ], [], 5, [ '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'], ], [], 5, [ 'navnode6', ], ], 'The max display limit of navigation nodes is not defined; ' . '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'], ], [], null, [], ], ]; } /** * 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 nodes_match_current_url_provider */ public function test_nodes_match_current_url(string $selectedurl, string $expectednode): void { 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'); $response = $method->invoke($secondary, $node); $this->assertSame($response->key ?? null, $expectednode); } /** * Provider for test_nodes_match_current_url * * @return \string[][] */ public static function 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 get_menu_array_provider */ public function test_get_menu_array(string $selected, array $expected): void { 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); $response = \core\navigation\views\secondary::create_menu_element([$selectednode]); $this->assertSame($expected, $response); } /** * Provider for test_get_menu_array * * @return array[] */ public static function 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.3', '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', [ [ 'parentnode1' => [ 'https://www.example.com/moodle/my' => 'child1' ], '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' ] ] ], ], ]; } /** * Test the get_node_with_first_action function * * @param string $selectedkey * @param string|null $expectedkey * @dataProvider get_node_with_first_action_provider */ public function test_get_node_with_first_action(string $selectedkey, ?string $expectedkey): void { 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'); $response = $method->invoke($secondary, $selectednode, $selectednode); $this->assertEquals($expected, $response); } /** * Provider for test_get_node_with_first_action * * @return array */ public static function 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 the add_external_nodes_to_secondary function. * * @param array $structure The structure of the navigation node tree to setup with. * @param array $expectednodes The expected nodes added to the secondary navigation * @param bool $separatenode Whether or not to create a separate node to add nodes to. * @dataProvider add_external_nodes_to_secondary_provider */ public function test_add_external_nodes_to_secondary(array $structure, array $expectednodes, bool $separatenode = false): void { global $PAGE; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $PAGE->set_context($context); $PAGE->set_url('/'); $node = $this->generate_node_tree_construct($structure, 'parentnode'); $secondary = new secondary($PAGE); $secondary->add_node($node); $firstnode = $node->get('parentnode1'); $customparent = null; if ($separatenode) { $customparent = navigation_node::create('Custom parent'); } $method = new ReflectionMethod('core\navigation\views\secondary', 'add_external_nodes_to_secondary'); $method->invoke($secondary, $firstnode, $firstnode, $customparent); $actualnodes = $separatenode ? $customparent->get_children_key_list() : $secondary->get_children_key_list(); $this->assertEquals($expectednodes, $actualnodes); } /** * Provider for the add_external_nodes_to_secondary function. * * @return array */ public static function add_external_nodes_to_secondary_provider(): array { return [ "Container node with internal action and external children" => [ [ 'parentnode1' => [ 'action' => '/test.php', 'children' => [ 'child2.1' => 'https://example.org', 'child2.2' => 'https://example.net', ] ] ], ['parentnode', 'parentnode1'] ], "Container node with external action and external children" => [ [ 'parentnode1' => [ 'action' => 'https://example.com', 'children' => [ 'child2.1' => 'https://example.org', 'child2.2' => 'https://example.net', ] ] ], ['parentnode', 'parentnode1', 'child2.1', 'child2.2'] ], "Container node with external action and internal children" => [ [ 'parentnode1' => [ 'action' => 'https://example.org', 'children' => [ 'child2.1' => '/view/course.php', 'child2.2' => '/view/admin.php', ] ] ], ['parentnode', 'parentnode1', 'child2.1', 'child2.2'] ], "Container node with internal actions and internal children" => [ [ 'parentnode1' => [ 'action' => '/test.php', 'children' => [ 'child2.1' => '/course.php', 'child2.2' => '/admin.php', ] ] ], ['parentnode', 'parentnode1'] ], "Container node with internal action and external children adding to custom node" => [ [ 'parentnode1' => [ 'action' => '/test.php', 'children' => [ 'child2.1' => 'https://example.org', 'child2.2' => 'https://example.net', ] ] ], ['parentnode1'], true ], "Container node with external action and external children adding to custom node" => [ [ 'parentnode1' => [ 'action' => 'https://example.com', 'children' => [ 'child2.1' => 'https://example.org', 'child2.2' => 'https://example.net', ] ] ], ['parentnode1', 'child2.1', 'child2.2'], true ], "Container node with external action and internal children adding to custom node" => [ [ 'parentnode1' => [ 'action' => 'https://example.org', 'children' => [ 'child2.1' => '/view/course.php', 'child2.2' => '/view/admin.php', ] ] ], ['parentnode1', 'child2.1', 'child2.2'], true ], "Container node with internal actions and internal children adding to custom node" => [ [ 'parentnode1' => [ 'action' => '/test.php', 'children' => [ 'child2.1' => '/course.php', 'child2.2' => '/admin.php', ] ] ], ['parentnode1'], true ], ]; } /** * Test the get_overflow_menu_data function * * @param string $selectedurl * @param bool $expectednull * @param bool $emptynode * @dataProvider get_overflow_menu_data_provider */ public function test_get_overflow_menu_data(string $selectedurl, bool $expectednull, bool $emptynode = false): void { global $PAGE; $this->resetAfterTest(); // 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" ]; $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $PAGE->set_context($context); $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->invoke($secondary, $secondary); } $method = new ReflectionMethod('core\navigation\views\secondary', 'get_overflow_menu_data'); $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 static function 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 ], ]; } /** * Test the course administration settings return an overflow menu. * * @dataProvider get_overflow_menu_data_course_admin_provider * @param string $url Url of the page we are testing. * @param string $contextidentifier id or contextid or something similar. * @param bool $expected The expected return. True to return the overflow menu otherwise false for nothing. */ public function test_get_overflow_menu_data_course_admin(string $url, string $contextidentifier, bool $expected): void { global $PAGE; $this->resetAfterTest(); $this->setAdminUser(); $pagecourse = $this->getDataGenerator()->create_course(); $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST); $id = ($contextidentifier == 'contextid') ? $contextrecord->id : $pagecourse->id; $pageurl = new \moodle_url($url, [$contextidentifier => $id]); $PAGE->set_url($pageurl); navigation_node::override_active_url($pageurl); $PAGE->set_context($contextrecord); $node = new secondary($PAGE); $node->initialise(); $result = $node->get_overflow_menu_data(); if ($expected) { $this->assertInstanceOf('url_select', $result); $this->assertTrue($pageurl->compare($result->selected)); } else { $this->assertNull($result); } } /** * Data provider for the other half of the method thing * * @return array Provider information. */ public static function get_overflow_menu_data_course_admin_provider(): array { return [ "Backup page returns overflow" => [ '/backup/backup.php', 'id', false, ], "Restore course page returns overflow" => [ '/backup/restorefile.php', 'contextid', false, ], "Import course page returns overflow" => [ '/backup/import.php', 'id', false, ], "Course copy page returns overflow" => [ '/backup/copy.php', 'id', false, ], "Course reset page returns overflow" => [ '/course/reset.php', 'id', false, ], // The following pages should not return the overflow menu. "Course page returns nothing" => [ '/course/view.php', 'id', false ], "Question bank should return nothing" => [ '/question/edit.php', 'cmid', false ], "Reports should return nothing" => [ '/report/log/index.php', 'id', false ], "Participants page should return nothing" => [ '/user/index.php', 'id', false ] ]; } }