Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\navigation\views;
18
 
19
use navigation_node;
20
use url_select;
21
use settings_navigation;
22
 
23
/**
24
 * Class secondary_navigation_view.
25
 *
26
 * The secondary navigation view is a stripped down tweaked version of the
27
 * settings_navigation/navigation
28
 *
29
 * @package     core
30
 * @category    navigation
31
 * @copyright   2021 onwards Peter Dias
32
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class secondary extends view {
35
    /** @var string $headertitle The header for this particular menu*/
36
    public $headertitle;
37
 
38
    /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
39
    const MAX_DISPLAYED_NAV_NODES = 5;
40
 
41
    /** @var navigation_node The course overflow node. */
42
    protected $courseoverflownode = null;
43
 
44
    /** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */
45
    protected $overflowselected = null;
46
 
47
    /**
48
     * Defines the default structure for the secondary nav in a course context.
49
     *
50
     * In a course context, we are curating nodes from the settingsnav and navigation objects.
51
     * The following mapping construct specifies which object we are fetching it from, the type of the node, the key
52
     * and in what order we want the node - defined as per the mockups.
53
     *
54
     * @return array
55
     */
56
    protected function get_default_course_mapping(): array {
57
        $nodes = [];
58
        $nodes['settings'] = [
59
            self::TYPE_CONTAINER => [
1441 ariadna 60
                'coursereports' => 4,
61
                'questionbank' => 5,
1 efrain 62
            ],
63
            self::TYPE_SETTING => [
64
                'editsettings' => 0,
65
                'review' => 1.1,
66
                'manageinstances' => 1.2,
67
                'groups' => 1.3,
68
                'override' => 1.4,
69
                'roles' => 1.5,
70
                'permissions' => 1.6,
71
                'otherusers' => 1.7,
72
                'gradebooksetup' => 2.1,
73
                'outcomes' => 2.2,
1441 ariadna 74
                'coursecompletion' => 7,
75
                'coursebadges' => 8.1,
76
                'newbadge' => 8.2,
77
                'filtermanagement' => 10,
78
                'unenrolself' => 11,
79
                'coursetags' => 12,
80
                'download' => 13,
81
                'contextlocking' => 14,
1 efrain 82
            ],
83
        ];
84
        $nodes['navigation'] = [
85
            self::TYPE_CONTAINER => [
86
                'participants' => 1,
1441 ariadna 87
                'courseoverview' => 3,
1 efrain 88
            ],
89
            self::TYPE_SETTING => [
90
                'grades' => 2,
1441 ariadna 91
                'badgesview' => 8,
92
                'competencies' => 9,
93
                'communication' => 15,
1 efrain 94
            ],
95
            self::TYPE_CUSTOM => [
1441 ariadna 96
                'contentbank' => 6,
1 efrain 97
                'participants' => 1, // In site home, 'participants' is classified differently.
98
            ],
99
        ];
100
 
101
        return $nodes;
102
    }
103
 
104
    /**
105
     * Defines the default structure for the secondary nav in a module context.
106
     *
107
     * In a module context, we are curating nodes from the settingsnav object.
108
     * The following mapping construct specifies the type of the node, the key
109
     * and in what order we want the node - defined as per the mockups.
110
     *
111
     * @return array
112
     */
113
    protected function get_default_module_mapping(): array {
114
        return [
115
            self::TYPE_SETTING => [
116
                'modedit' => 1,
117
                "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
118
                "mod_{$this->page->activityname}_groupoverrides" => 4,
119
                'roleassign' => 7.2,
120
                'filtermanage' => 6,
121
                'roleoverride' => 7,
122
                'rolecheck' => 7.1,
123
                'logreport' => 8,
124
                'backup' => 9,
125
                'restore' => 10,
126
                'competencybreakdown' => 11,
127
                'sendtomoodlenet' => 16,
128
            ],
129
            self::TYPE_CUSTOM => [
130
                'advgrading' => 2,
131
                'contentbank' => 12,
132
            ],
133
        ];
134
    }
135
 
136
    /**
137
     * Defines the default structure for the secondary nav in a category context.
138
     *
139
     * In a category context, we are curating nodes from the settingsnav object.
140
     * The following mapping construct specifies the type of the node, the key
141
     * and in what order we want the node - defined as per the mockups.
142
     *
143
     * @return array
144
     */
145
    protected function get_default_category_mapping(): array {
146
        return [
147
            self::TYPE_SETTING => [
148
                'edit' => 1,
149
                'permissions' => 2,
150
                'roles' => 2.1,
151
                'rolecheck' => 2.2,
152
            ]
153
        ];
154
    }
155
 
156
    /**
157
     * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
158
     *
159
     * @return array
160
     */
161
    protected function get_default_category_more_menu_nodes(): array {
162
        return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse'];
163
    }
164
    /**
165
     * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
166
     *
167
     * @return array
168
     */
169
    protected function get_default_course_more_menu_nodes(): array {
170
        return [];
171
    }
172
 
173
    /**
174
     * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
175
     *
176
     * @return array
177
     */
178
    protected function get_default_module_more_menu_nodes(): array {
179
        return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
180
            'competencybreakdown', "mod_{$this->page->activityname}_useroverrides",
181
            "mod_{$this->page->activityname}_groupoverrides"];
182
    }
183
 
184
    /**
185
     * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
186
     *
187
     * @return array
188
     */
189
    protected function get_default_admin_more_menu_nodes(): array {
190
        return [];
191
    }
192
 
193
    /**
194
     * Initialise the view based navigation based on the current context.
195
     *
196
     * As part of the initial restructure, the secondary nav is only considered for the following pages:
197
     * 1 - Site admin settings
198
     * 2 - Course page - Does not include front_page which has the same context.
199
     * 3 - Module page
200
     */
201
    public function initialise(): void {
202
        global $SITE;
203
 
204
        if (during_initial_install() || $this->initialised) {
205
            return;
206
        }
207
        $this->id = 'secondary_navigation';
208
        $context = $this->context;
209
        $this->headertitle = get_string('menu');
210
        $defaultmoremenunodes = [];
211
        $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES;
212
 
213
        switch ($context->contextlevel) {
214
            case CONTEXT_COURSE:
215
                $this->headertitle = get_string('courseheader');
216
                if ($this->page->course->format === 'singleactivity') {
217
                    $this->load_single_activity_course_navigation();
218
                } else {
219
                    $this->load_course_navigation();
220
                    $defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
221
                }
222
                break;
223
            case CONTEXT_MODULE:
224
                $this->headertitle = get_string('activityheader');
225
                if ($this->page->course->format === 'singleactivity') {
226
                    $this->load_single_activity_course_navigation();
227
                } else {
228
                    $this->load_module_navigation($this->page->settingsnav);
229
                    $defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
230
                }
231
                break;
232
            case CONTEXT_COURSECAT:
233
                $this->headertitle = get_string('categoryheader');
234
                $this->load_category_navigation();
235
                $defaultmoremenunodes = $this->get_default_category_more_menu_nodes();
236
                break;
237
            case CONTEXT_SYSTEM:
238
                $this->headertitle = get_string('homeheader');
239
                $this->load_admin_navigation();
240
                // If the site administration navigation was generated after load_admin_navigation().
241
                if ($this->has_children()) {
242
                    // Do not explicitly limit the number of navigation nodes displayed in the site administration
243
                    // navigation menu.
244
                    $maxdisplayednodes = null;
245
                }
246
                $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
247
                break;
248
        }
249
 
250
        $this->remove_unwanted_nodes($this);
251
 
252
        // Don't need to show anything if only the view node is available. Remove it.
253
        if ($this->children->count() == 1) {
254
            $this->children->remove('modulepage');
255
        }
256
        // Force certain navigation nodes to be displayed in the "more" menu.
257
        $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes);
258
        // Search and set the active node.
259
        $this->scan_for_active_node($this);
260
        $this->initialised = true;
261
    }
262
 
263
    /**
264
     * Returns a node with the action being from the first found child node that has an action (Recursive).
265
     *
266
     * @param navigation_node $node The part of the node tree we are checking.
267
     * @param navigation_node $basenode  The very first node to be used for the return.
268
     * @return navigation_node|null
269
     */
270
    protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {
271
        $newnode = null;
272
        if (!$node->has_children()) {
273
            return null;
274
        }
275
 
276
        // Find the first child with an action and update the main node.
277
        foreach ($node->children as $child) {
278
            if ($child->has_action()) {
279
                $newnode = $basenode;
280
                $newnode->action = $child->action;
281
                return $newnode;
282
            }
283
        }
284
        if (is_null($newnode)) {
285
            // Check for children and go again.
286
            foreach ($node->children as $child) {
287
                if ($child->has_children()) {
288
                    $newnode = $this->get_node_with_first_action($child, $basenode);
289
 
290
                    if (!is_null($newnode)) {
291
                        return $newnode;
292
                    }
293
                }
294
            }
295
        }
296
        return null;
297
    }
298
 
299
    /**
300
     * Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have
301
     * an action then a search is done through the children looking for the first node that has an action. This action is then given
302
     * to the parent node that is initially provided as a parameter.
303
     *
304
     * @param navigation_node $node The navigation node that we want to ensure has an action tied to it.
305
     * @return navigation_node The node intact with an action to use.
306
     */
307
    protected function get_first_action_for_node(navigation_node $node): ?navigation_node {
308
        // If the node does not have children and has no action then no further processing is needed.
309
        $newnode = null;
310
        if ($node->has_children() && !$node->has_action()) {
311
            // We want to find the first child with an action.
312
            // We want to check all children on this level before going further down.
313
            // Note that new node gets changed here.
314
            $newnode = $this->get_node_with_first_action($node, $node);
315
        } else if ($node->has_action()) {
316
            $newnode = $node;
317
        }
318
        return $newnode;
319
    }
320
 
321
    /**
322
     * Recursive call to add all custom navigation nodes to secondary
323
     *
324
     * @param navigation_node $node The node which should be added to secondary
325
     * @param navigation_node $basenode The original parent node
326
     * @param navigation_node|null $root The parent node nodes are to be added/removed to.
327
     * @param bool $forceadd Whether or not to bypass the external action check and force add all nodes
328
     */
329
    protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode,
330
           ?navigation_node $root = null, bool $forceadd = false) {
331
        $root = $root ?? $this;
332
        // Add the first node.
333
        if ($node->has_action() && !$this->get($node->key)) {
334
            $root->add_node(clone $node);
335
        }
336
 
337
        // If the node has an external action add all children to the secondary navigation.
338
        if (!$node->has_internal_action() || $forceadd) {
339
            if ($node->has_children()) {
340
                foreach ($node->children as $child) {
341
                    if ($child->has_children()) {
342
                        $this->add_external_nodes_to_secondary($child, $basenode, $root, true);
343
                    } else if ($child->has_action() && !$this->get($child->key)) {
344
                        // Check whether the basenode matches a child's url.
345
                        // This would have happened in get_first_action_for_node.
346
                        // In these cases, we prefer the specific child content.
347
                        if ($basenode->has_action() && $basenode->action()->compare($child->action())) {
348
                            $root->children->remove($basenode->key, $basenode->type);
349
                        }
350
                        $root->add_node(clone $child);
351
                    }
352
                }
353
            }
354
        }
355
    }
356
 
357
    /**
358
     * Returns a list of all expected nodes in the course administration.
359
     *
360
     * @return array An array of keys for navigation nodes in the course administration.
361
     */
362
    protected function get_expected_course_admin_nodes(): array {
363
        $expectednodes = [];
364
        foreach ($this->get_default_course_mapping()['settings'] as $value) {
365
            foreach ($value as $nodekey => $notused) {
366
                $expectednodes[] = $nodekey;
367
            }
368
        }
369
        foreach ($this->get_default_course_mapping()['navigation'] as $value) {
370
            foreach ($value as $nodekey => $notused) {
371
                $expectednodes[] = $nodekey;
372
            }
373
        }
374
        $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges'];
375
        $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset'];
376
        $expectednodes = array_merge($expectednodes, $othernodes);
377
        $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes);
378
        return $expectednodes;
379
    }
380
 
381
    /**
382
     * Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do
383
     * the relevant checks, we don't do it again here.
384
     *
385
     * @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children.
386
     *                                       If not explicitly defined, the nodes will be added to the secondary root
387
     *                                       node by default.
388
     */
389
    protected function load_course_navigation(?navigation_node $rootnode = null): void {
390
        global $SITE;
391
 
392
        $rootnode = $rootnode ?? $this;
393
        $course = $this->page->course;
394
        // Initialise the main navigation and settings nav.
395
        // It is important that this is done before we try anything.
396
        $settingsnav = $this->page->settingsnav;
397
        $navigation = $this->page->navigation;
398
 
399
        if ($course->id == $SITE->id) {
400
            $firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'.
401
            $frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node.
402
            $settingsnav = $frontpage ?: $settingsnav;
403
            $courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node.
404
        } else {
405
            $firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'.
406
            $courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'.
407
        }
408
 
409
        // Add the known nodes from settings and navigation.
410
        $nodes = $this->get_default_course_mapping();
411
        $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
412
        $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
413
        $this->add_ordered_nodes($nodesordered, $rootnode);
414
 
415
        // Try to get any custom nodes defined by plugins, which may include containers.
416
        if ($courseadminnode) {
417
            $expectedcourseadmin = $this->get_expected_course_admin_nodes();
418
            foreach ($courseadminnode->children as $other) {
419
                if (array_search($other->key, $expectedcourseadmin, true) === false) {
420
                    $othernode = $this->get_first_action_for_node($other);
421
                    $recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other;
422
                    // Get the first node and check whether it's been added already.
423
                    // Also check if the first node is an external link. If it is, add all children.
424
                    $this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode);
425
                }
426
            }
427
        }
428
 
429
        // Add the respective first node, provided there are other nodes included.
430
        if (!empty($nodekeys = $rootnode->children->get_key_list())) {
431
            $rootnode->add_node(
432
                navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]),
433
                    self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys)
434
            );
435
        }
1441 ariadna 436
 
437
        // Allow plugins to add nodes to the secondary navigation.
438
        $hook = new \core\hook\navigation\secondary_extend($this);
439
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
1 efrain 440
    }
441
 
442
    /**
443
     * Gets the overflow navigation nodes for the course administration category.
444
     *
445
     * @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained.
446
     *                                       If not explicitly defined, the nodes will be obtained from the secondary root
447
     *                                       node by default.
448
     * @return navigation_node  The course overflow nodes.
449
     */
450
    protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node {
451
        global $SITE;
452
 
453
        $rootnode = $rootnode ?? $this;
454
        // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being
455
        // present the second time this is called.
456
        if (isset($this->courseoverflownode)) {
457
            return $this->courseoverflownode;
458
        }
459
 
460
        // Start with getting the base node for the front page or the course.
461
        $node = null;
462
        if ($this->page->course->id == $SITE->id) {
463
            $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
464
        } else {
465
            $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
466
        }
467
        $coursesettings = $node ? $node->get_children_key_list() : [];
468
        $thissettings = $rootnode->get_children_key_list();
469
        $diff = array_diff($coursesettings, $thissettings);
470
 
471
        // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup,
472
        // grades - outcomes).
473
        $shortdiff = array_filter($diff, function($value) {
474
            return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' ||
475
                $value == 'outcomes');
476
        });
477
 
478
        // Permissions may be in play here that ultimately will show no overflow.
479
        if (empty($shortdiff)) {
480
            return null;
481
        }
482
 
483
        $firstitem = array_shift($shortdiff);
484
        $navnode = $node->get($firstitem);
485
        foreach ($shortdiff as $key) {
486
            $courseadminnodes = $node->get($key);
487
            if ($courseadminnodes) {
488
                if ($courseadminnodes->parent->key == $node->key) {
489
                    $navnode->add_node($courseadminnodes);
490
                }
491
            }
492
        }
493
        $this->courseoverflownode = $navnode;
494
        return $navnode;
495
 
496
    }
497
 
498
    /**
499
     * Recursively looks for a match to the current page url.
500
     *
501
     * @param navigation_node $node The node to look through.
502
     * @return navigation_node|null The node that matches this page's url.
503
     */
504
    protected function nodes_match_current_url(navigation_node $node): ?navigation_node {
505
        $pagenode = $this->page->url;
506
        if ($node->has_action()) {
507
            // Check this node first.
508
            if ($node->action->compare($pagenode)) {
509
                return $node;
510
            }
511
        }
512
        if ($node->has_children()) {
513
            foreach ($node->children as $child) {
514
                $result = $this->nodes_match_current_url($child);
515
                if ($result) {
516
                    return $result;
517
                }
518
            }
519
        }
520
        return null;
521
    }
522
 
523
    /**
524
     * Recursively search a node and its children for a node matching the key string $key.
525
     *
526
     * @param navigation_node $node the navigation node to check.
527
     * @param string $key the key of the node to match.
528
     * @return navigation_node|null node if found, otherwise null.
529
     */
530
    protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node {
531
        if ($node->has_action()) {
532
            // Check this node first.
533
            if ($node->key == $key) {
534
                return $node;
535
            }
536
        }
537
        if ($node->has_children()) {
538
            foreach ($node->children as $child) {
539
                $result = $this->node_matches_key_string($child, $key);
540
                if ($result) {
541
                    return $result;
542
                }
543
            }
544
        }
545
        return null;
546
    }
547
 
548
    /**
549
     * Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key.
550
     *
551
     * Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php'
552
     * will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match.
553
     *
554
     * 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
555
     * match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page
556
     * knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node".
557
     *
558
     * E.g.
559
     * - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore)
560
     * - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the
561
     * main restore landing page).
562
     * - The backup/restore.php page calls:
563
     * $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore');
564
     * and when the page is loaded, the 'Restore' node be presented as the selected node.
565
     *
566
     * @param string $nodekey The string key of the overflow node to match.
567
     */
568
    public function set_overflow_selected_node(string $nodekey): void {
569
        $this->overflowselected = $nodekey;
570
    }
571
 
572
    /**
573
     * Returns a url_select object with overflow navigation nodes.
574
     * This looks to see if the current page is within the course administration, or some other page that requires an overflow
575
     * select object.
576
     *
577
     * @return url_select|null The overflow menu data.
578
     */
579
    public function get_overflow_menu_data(): ?url_select {
580
 
581
        if (!$this->page->get_navigation_overflow_state()) {
582
            return null;
583
        }
584
 
585
        $issingleactivitycourse = $this->page->course->format === 'singleactivity';
586
        $rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this;
587
        $activenode = $this->find_active_node();
588
        $incourseadmin = false;
589
 
1441 ariadna 590
        $activeleafnode = $this->page->settingsnav->find_active_node();
591
        $parentnode = $activeleafnode->parent ?? null;
592
        if ($issingleactivitycourse && $parentnode && $parentnode->key === 'quiz_report') {
593
            $activenode = $parentnode;
594
        }
595
 
1 efrain 596
        if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) {
597
            // Could be in the course admin section.
598
            $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
599
            if (!$courseadmin) {
600
                return null;
601
            }
602
 
603
            $activenode = $courseadmin->find_active_node();
604
            if (!$activenode) {
605
                return null;
606
            }
607
            $incourseadmin = true;
608
        }
609
 
610
        if ($activenode->key === 'coursereuse' || $incourseadmin) {
611
            $courseoverflownode = $this->get_course_overflow_nodes($rootnode);
612
            if (is_null($courseoverflownode)) {
613
                return null;
614
            }
615
            if ($incourseadmin) {
616
                // Validate whether the active node is part of the expected course overflow nodes.
617
                if (($activenode->key !== $courseoverflownode->key) &&
618
                    !$courseoverflownode->find($activenode->key, $activenode->type)) {
619
                    return null;
620
                }
621
            }
622
            $menuarray = static::create_menu_element([$courseoverflownode]);
623
            if ($activenode->key != 'coursereuse') {
624
                $inmenu = false;
625
                foreach ($menuarray as $key => $value) {
626
                    if ($this->page->url->out(false) == $key) {
627
                        $inmenu = true;
628
                    }
629
                }
630
                if (!$inmenu) {
631
                    return null;
632
                }
633
            }
634
            // If the page has explicitly set the overflow node it would like selected, find and use that node.
635
            if ($this->overflowselected) {
636
                $selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected);
637
                $selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null;
638
            }
639
 
640
            $menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null);
1441 ariadna 641
            $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'visually-hidden']);
1 efrain 642
            return $menuselect;
643
        } else {
644
            return $this->get_other_overflow_menu_data($activenode);
645
        }
646
    }
647
 
648
    /**
649
     * Gets overflow menu data for third party plugin settings.
650
     *
651
     * @param navigation_node $activenode The node to gather the children for to put into the overflow menu.
652
     * @return url_select|null The overflow menu in a url_select object.
653
     */
654
    protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select {
655
        if (!$activenode->has_action()) {
656
            return null;
657
        }
658
 
659
        if (!$activenode->has_children()) {
660
            return null;
661
        }
662
 
663
        // If the setting is extending the course navigation then the page being redirected to should be in the course context.
664
        // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages
665
        // that are redirected to, be in the course context or module context depending on which callback was used.
666
        // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found.
667
        // The request of third party developers is to keep their settings within the specified context.
668
        if ($this->page->context->contextlevel != CONTEXT_COURSE
669
                && $this->page->context->contextlevel != CONTEXT_MODULE
670
                && $this->page->context->contextlevel != CONTEXT_COURSECAT) {
671
            return null;
672
        }
673
 
674
        // These areas have their own code to retrieve added plugin navigation nodes.
675
        if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') {
676
            return null;
677
        }
678
 
679
        $menunode = $this->page->settingsnav->find($activenode->key, null);
680
 
681
        if (!$menunode instanceof navigation_node) {
682
            return null;
683
        }
684
        // Loop through all children and try and find a match to the current url.
685
        $matchednode = $this->nodes_match_current_url($menunode);
686
        if (is_null($matchednode)) {
687
            return null;
688
        }
689
        if (!isset($menunode) || !$menunode->has_children()) {
690
            return null;
691
        }
692
        $selectdata = static::create_menu_element([$menunode], false);
693
        $urlselect = new url_select($selectdata, $matchednode->action->out(false), null);
1441 ariadna 694
        $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'visually-hidden']);
1 efrain 695
        return $urlselect;
696
    }
697
 
698
    /**
699
     * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via
700
     * '_extend_settings_navigation'.
701
     * It populates the tree based on the nav mockup
702
     *
703
     * If nodes change, we will have to explicitly call the callback again.
704
     *
705
     * @param settings_navigation $settingsnav The settings navigation object related to the module page
706
     * @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children.
707
     *                                       If not explicitly defined, the nodes will be added to the secondary root
708
     *                                       node by default.
709
     */
710
    protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void {
711
        $rootnode = $rootnode ?? $this;
712
        $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING);
713
        $nodes = $this->get_default_module_mapping();
714
 
715
        if ($mainnode) {
716
            $url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php',
717
                ['id' => $settingsnav->get_page()->cm->id]);
718
            $setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE);
719
            $node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $url,
720
                null, null, 'modulepage');
721
            if ($setactive) {
722
                $node->make_active();
723
            }
724
            // Add the initial nodes.
725
            $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
726
            $this->add_ordered_nodes($nodesordered, $rootnode);
727
 
728
            // We have finished inserting the initial structure.
729
            // Populate the menu with the rest of the nodes available.
730
            $this->load_remaining_nodes($mainnode, $nodes, $rootnode);
731
        }
732
    }
733
 
734
    /**
735
     * Load the course category navigation.
736
     */
737
    protected function load_category_navigation(): void {
738
        $settingsnav = $this->page->settingsnav;
739
        $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER);
740
        $nodes = $this->get_default_category_mapping();
741
 
742
        if ($mainnode) {
743
            $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]);
744
            $this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain');
745
 
746
            // Add the initial nodes.
747
            $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
748
            $this->add_ordered_nodes($nodesordered);
749
 
750
            // We have finished inserting the initial structure.
751
            // Populate the menu with the rest of the nodes available.
752
            $this->load_remaining_nodes($mainnode, $nodes);
753
        }
754
    }
755
 
756
    /**
757
     * Load the site admin navigation
758
     */
759
    protected function load_admin_navigation(): void {
760
        global $PAGE, $SITE;
761
 
762
        $settingsnav = $this->page->settingsnav;
763
        $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN);
764
        // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via
765
        // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to
766
        // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding
767
        // tab. Note this value refers to being present on the page itself, before a search has been performed.
768
        $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS);
769
        if ($node) {
770
            $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode');
771
            if ($isadminsearchpage) {
772
                $siteadminnode->action = false;
773
                $siteadminnode->tab = "#link$node->key";
774
            } else {
775
                $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key");
776
            }
777
            foreach ($node->children as $child) {
778
                if ($child->display && !$child->is_short_branch()) {
779
                    // Mimic the current boost behaviour and pass down anchors for the tabs.
780
                    if ($isadminsearchpage) {
781
                        $child->action = false;
782
                        $child->tab = "#link$child->key";
783
                    } else {
784
                        $child->action = new \moodle_url("/admin/search.php", [], "link$child->key");
785
                    }
786
                    $this->add_node(clone $child);
787
                } else {
788
                    $siteadminnode->add_node(clone $child);
789
                }
790
            }
791
        }
792
    }
793
 
794
    /**
795
     * Adds the indexed nodes to the current view or a given node. The key should indicate it's position in the tree.
796
     * Any sub nodes needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed  under #3 node.
797
     *
798
     * @param array $nodes An array of navigation nodes to be added.
799
     * @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly
800
     *                                       defined, the nodes will be added to the secondary root node by default.
801
     */
802
    protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void {
803
        $rootnode = $rootnode ?? $this;
804
        ksort($nodes);
805
        foreach ($nodes as $key => $node) {
806
            // If the key is a string then we are assuming this is a nested element.
807
            if (is_string($key)) {
808
                $parentnode = $nodes[floor($key)] ?? null;
809
                if ($parentnode) {
810
                    $parentnode->add_node(clone $node);
811
                }
812
            } else {
813
                $rootnode->add_node(clone $node);
814
            }
815
        }
816
    }
817
 
818
    /**
819
     * Find the remaining nodes that need to be loaded into secondary based on the current context or a given node.
820
     *
821
     * @param navigation_node $completenode The original node that we are sourcing information from
822
     * @param array           $nodesmap The map used to populate secondary nav in the given context
823
     * @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not
824
     *                                       explicitly defined, the nodes will be added to the secondary root node by
825
     *                                       default.
826
     */
827
    protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap,
828
            ?navigation_node $rootnode = null): void {
829
        $flattenednodes = [];
830
        $rootnode = $rootnode ?? $this;
831
        foreach ($nodesmap as $nodecontainer) {
832
            $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes);
833
        }
834
 
835
        $populatedkeys = $this->get_children_key_list();
836
        $existingkeys = $completenode->get_children_key_list();
837
        $leftover = array_diff($existingkeys, $populatedkeys);
838
        foreach ($leftover as $key) {
839
            if (!in_array($key, $flattenednodes, true) && $leftovernode = $completenode->get($key)) {
840
                // Check for nodes with children and potentially no action to direct to.
841
                if ($leftovernode->has_children()) {
842
                    $leftovernode = $this->get_first_action_for_node($leftovernode);
843
                }
844
 
845
                // We have found the first node with an action.
846
                if ($leftovernode) {
847
                    $this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode);
848
                }
849
            }
850
        }
851
    }
852
 
853
    /**
854
     * Force certain secondary navigation nodes to be displayed in the "more" menu.
855
     *
856
     * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
857
     *                                    should be added into the "more" menu by default
858
     * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation
859
     */
860
    protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) {
861
        // Counter of the navigation nodes that are initially displayed in the secondary nav
862
        // (excludes the nodes from the "more" menu).
863
        $displayednodescount = 0;
864
        foreach ($this->children as $child) {
865
            // Skip if the navigation node has been already forced into the "more" menu.
866
            if ($child->forceintomoremenu) {
867
                continue;
868
            }
869
            // If the navigation node is in the pre-defined list of nodes that should be added by default in the
870
            // "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined).
871
            if (in_array($child->key, $defaultmoremenunodes) ||
872
                    (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) {
873
                // Force the node and its children into the "more" menu.
874
                $child->set_force_into_more_menu(true);
875
                continue;
876
            }
877
            $displayednodescount++;
878
        }
879
    }
880
 
881
    /**
882
     * Recursively remove navigation nodes that should not be displayed in the secondary navigation.
883
     *
884
     * @param navigation_node $node The starting navigation node.
885
     */
886
    protected function remove_unwanted_nodes(navigation_node $node) {
887
        foreach ($node->children as $child) {
888
            if (!$child->showinsecondarynavigation) {
889
                $child->remove();
890
                continue;
891
            }
892
            if (!empty($child->children)) {
893
                $this->remove_unwanted_nodes($child);
894
            }
895
        }
896
    }
897
 
898
    /**
899
     * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a
900
     * url_select element.
901
     *
902
     * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu.
903
     * @param bool $forceheadings Whether the returned array should be forced to use headings.
904
     * @return array|null A url select element for navigating through the navigation nodes.
905
     */
906
    public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array {
907
        if (empty($navigationnodes)) {
908
            return null;
909
        }
910
 
911
        // If one item, do we put this into a url_select?
912
        if (count($navigationnodes) < 2) {
913
            // Check if there are children.
914
            $navnode = array_shift($navigationnodes);
915
            $menudata = [];
916
            if (!$navnode->has_children()) {
917
                // Just one item.
918
                if (!$navnode->has_action()) {
919
                    return null;
920
                }
921
                $menudata[$navnode->action->out(false)] = static::format_node_text($navnode);
922
            } else {
923
                if (static::does_menu_need_headings($navnode) || $forceheadings) {
924
                    // Let's do headings.
925
                    $menudata = static::get_headings_nav_array($navnode);
926
                } else {
927
                    // Simple flat nav.
928
                    $menudata = static::get_flat_nav_array($navnode);
929
                }
930
            }
931
            return $menudata;
932
        } else {
933
            // We have more than one navigation node to handle. Put each node in it's own heading.
934
            $menudata = [];
935
            $titledata = [];
936
            foreach ($navigationnodes as $navigationnode) {
937
                if ($navigationnode->has_children()) {
938
                    $menuarray = [];
939
                    // Add a heading and flatten out everything else.
940
                    if ($navigationnode->has_action()) {
941
                        $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
942
                            static::format_node_text($navigationnode);
943
                        $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode);
944
                    } else {
945
                        $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode);
946
                    }
947
 
948
                    $titledata += $menuarray;
949
                } else {
950
                    // Add with no heading.
951
                    if (!$navigationnode->has_action()) {
952
                        return null;
953
                    }
954
                    $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
955
                }
956
            }
957
            $menudata += [$titledata];
958
            return $menudata;
959
        }
960
    }
961
 
962
    /**
963
     * Recursively goes through the provided navigation node and returns a flat version.
964
     *
965
     * @param navigation_node $navigationnode The navigationnode.
966
     * @return array The whole tree flat.
967
     */
968
    protected static function get_whole_tree_flat(navigation_node $navigationnode): array {
969
        $nodes = [];
970
        foreach ($navigationnode->children as $child) {
971
            if ($child->has_action()) {
972
                $nodes[$child->action->out()] = $child->text;
973
            }
974
            if ($child->has_children()) {
975
                $childnodes = static::get_whole_tree_flat($child);
976
                $nodes = array_merge($nodes, $childnodes);
977
            }
978
        }
979
        return $nodes;
980
    }
981
 
982
    /**
983
     * Checks to see if the provided navigation node has children and determines if we want headings for a url select element.
984
     *
985
     * @param navigation_node  $navigationnode  The navigation node we are checking.
986
     * @return bool Whether we want headings or not.
987
     */
988
    protected static function does_menu_need_headings(navigation_node $navigationnode): bool {
989
        if (!$navigationnode->has_children()) {
990
            return false;
991
        }
992
        foreach ($navigationnode->children as $child) {
993
            if ($child->has_children()) {
994
                return true;
995
            }
996
        }
997
        return false;
998
    }
999
 
1000
    /**
1001
     * Takes the navigation node and returns it in a flat fashion. This is not recursive.
1002
     *
1003
     * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure.
1004
     * @return array The flat navigation array.
1005
     */
1006
    protected static function get_flat_nav_array(navigation_node $navigationnode): array {
1007
        $menuarray = [];
1008
        if ($navigationnode->has_action()) {
1009
            $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
1010
        }
1011
 
1012
        foreach ($navigationnode->children as $child) {
1013
            if ($child->has_action()) {
1014
                $menuarray[$child->action->out(false)] = static::format_node_text($child);
1015
            }
1016
        }
1017
        return $menuarray;
1018
    }
1019
 
1020
    /**
1021
     * For any navigation node that we have determined needs headings we return a more tree like array structure.
1022
     *
1023
     * @param navigation_node $navigationnode The navigation node to use for the formatted array structure.
1024
     * @return array The headings navigation array structure.
1025
     */
1026
    protected static function get_headings_nav_array(navigation_node $navigationnode): array {
1027
        $menublock = [];
1028
        // We know that this single node has headings, so grab this for the first heading.
1029
        $firstheading = [];
1030
        if ($navigationnode->has_action()) {
1031
            $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
1032
                static::format_node_text($navigationnode);
1033
            $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock);
1034
        } else {
1035
            $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock);
1036
        }
1037
         return [$firstheading + $menublock];
1038
    }
1039
 
1040
    /**
1041
     * Recursively goes and gets all children nodes.
1042
     *
1043
     * @param navigation_node $node The node to get the children of.
1044
     * @param array $menublock Used to put all child nodes in its own container.
1045
     * @return array The additional child nodes.
1046
     */
1047
    protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array {
1048
        $nodes = [];
1049
        foreach ($node->children as $child) {
1050
            if (!$child->has_children()) {
1051
                if (!$child->has_action()) {
1052
                    continue;
1053
                }
1054
                $nodes[$child->action->out(false)] = static::format_node_text($child);
1055
            } else {
1056
                $newarray = [];
1057
                if ($child->has_action()) {
1058
                    $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child);
1059
                    $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock);
1060
                } else {
1061
                    $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock);
1062
                }
1063
                $menublock += $newarray;
1064
            }
1065
        }
1066
        return $nodes;
1067
    }
1068
 
1069
    /**
1070
     * Returns the navigation node text in a string.
1071
     *
1072
     * @param navigation_node $navigationnode The navigationnode to return the text string of.
1073
     * @return string The navigation node text string.
1074
     */
1075
    protected static function format_node_text(navigation_node $navigationnode): string {
1076
        return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text;
1077
    }
1078
 
1079
    /**
1080
     * Load the single activity course secondary navigation.
1081
     */
1082
    protected function load_single_activity_course_navigation(): void {
1083
        $page = $this->page;
1084
        $course = $page->course;
1085
 
1086
        // Create 'Course' navigation node.
1087
        $coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course');
1088
        $this->load_course_navigation($coursesecondarynode);
1089
        // Remove the unnecessary 'Course' child node generated in load_course_navigation().
1090
        $coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE);
1091
        if (!empty($coursehomenode)) {
1092
            $coursehomenode->remove();
1093
        }
1094
 
1095
        // Add the 'Course' node to the secondary navigation only if this node has children nodes.
1096
        if (count($coursesecondarynode->children) > 0) {
1097
            $this->add_node($coursesecondarynode);
1098
            // Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu'
1099
            // property to true. This is required to force the template to output these items within a dropdown menu.
1100
            $coursesecondarynode->showchildreninsubmenu = true;
1101
        }
1102
 
1103
        // Create 'Activity' navigation node.
1104
        $activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity');
1105
 
1106
        // We should display the module related navigation in the course context as well. Therefore, we need to
1107
        // re-initialize the page object and manually set the course module to the one that it is currently visible in
1108
        // the course in order to obtain the required module settings navigation.
1109
        if ($page->context instanceof \context_course) {
1110
            $this->page->set_secondary_active_tab($coursesecondarynode->key);
1111
            // Get the currently used module in the single activity course.
1112
            $module = current(array_filter(get_course_mods($course->id), function ($module) {
1113
                return $module->visible == 1;
1114
            }));
1115
            // If the default module for the single course format has not been set yet, skip displaying the module
1116
            // related navigation in the secondary navigation.
1117
            if (!$module) {
1118
                return;
1119
            }
1120
            $page = new \moodle_page();
1121
            $page->set_cm($module, $course);
1122
            $page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]));
1123
        }
1124
 
1125
        $this->load_module_navigation($page->settingsnav, $activitysecondarynode);
1126
 
1127
        // Add the 'Activity' node to the secondary navigation only if this node has more that one child node.
1128
        if (count($activitysecondarynode->children) > 1) {
1129
            // Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within
1130
            // a dropdown menu.
1131
            $activitysecondarynode->showchildreninsubmenu = true;
1132
            $this->add_node($activitysecondarynode);
1133
            if ($this->context instanceof \context_module) {
1134
                $this->page->set_secondary_active_tab($activitysecondarynode->key);
1135
            }
1136
        } else { // Otherwise, add the 'View activity' node to the secondary navigation.
1137
            $viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]);
1138
            $this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage');
1139
            if ($this->context instanceof \context_module) {
1140
                $this->page->set_secondary_active_tab('modulepage');
1141
            }
1142
        }
1143
    }
1144
}