Skip to content

Commit

Permalink
MDL-83100 navigation: Add experimental navigation node debugging
Browse files Browse the repository at this point in the history
  • Loading branch information
cameron1729 committed Sep 14, 2024
1 parent 09e56f2 commit 97bc4ef
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 0 deletions.
5 changes: 5 additions & 0 deletions admin/settings/development.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
new lang_string('enablecommunicationsubsystem', 'core_admin'),
new lang_string('enablecommunicationsubsystem_desc', 'core_admin'), 0));

// Navigation node debugging.
$temp->add(new admin_setting_configcheckbox('enablenavigationnodedebugging',
new lang_string('enablenavigationnodedebugging', 'core_admin'),
new lang_string('enablenavigationnodedebugging_desc', 'core_admin'), 0));

$ADMIN->add('experimental', $temp);

// "debugging" settingpage
Expand Down
2 changes: 2 additions & 0 deletions lang/en/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,8 @@
$string['enablecomments'] = 'Enable comments';
$string['enablecommunicationsubsystem'] = 'Enable communication providers';
$string['enablecommunicationsubsystem_desc'] = 'Allow integration with communication providers such as Matrix so teachers and students can communicate more easily. You can manage these integrations in <a href="settings.php?section=managecommunicationproviders">Plugins</a>.';
$string['enablenavigationnodedebugging'] = 'Enable navigation node debugging';
$string['enablenavigationnodedebugging_desc'] = 'When enabled, you can append debugnav=1 as a query parameter to any page and the state of various navigation trees at diffrent points in their lifecycle will be dumped in a folder with the current timestamp as its name, located in $CFG->tempdir/navdebug. To view these files in a meaningful way, you may want to consider downloading the play_tree_frames script available from <a href="https://github.com/cameron1729/navigation-node-player">this Git repository</a>.';
$string['enablecourserelativedates'] = 'Enable course relative dates';
$string['enablecourserelativedates_desc'] = 'Allow courses to be set up to display dates relative to the user\'s start date in the course.';
$string['enablecourserequests'] = 'Enable course requests';
Expand Down
5 changes: 5 additions & 0 deletions lib/classes/navigation/views/secondary.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ protected function get_default_admin_more_menu_nodes(): array {
public function initialise(): void {
global $SITE;

$this->debug("Initialising view");

if (during_initial_install() || $this->initialised) {
return;
}
Expand Down Expand Up @@ -254,8 +256,11 @@ public function initialise(): void {
}
// 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->debug("Initiate node scan");
$this->scan_for_active_node($this);
$this->debug("Node scan complete. Secondary initialisation finished.");
$this->initialised = true;
}

Expand Down
12 changes: 12 additions & 0 deletions lib/classes/navigation/views/view.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,32 @@ protected function active_node_scan(navigation_node $node,

$result = null;
$activekey = $this->page->get_secondary_active_tab();

if ($activekey) {
if ($node->key && $activekey === $node->key) {
$this->debug("$node matches forced active key $activekey, returning it.");
return $node;
}
} else if ($node->check_if_active($strictness)) {
$this->debug("$node is active, returning it.");
return $node; // No need to continue, exit function.
}

$this->debug("Inspect children of $node");
foreach ($node->children as $child) {
$this->debug("Inspecting child $child for active nodes");
if ($this->active_node_scan($child, $strictness)) {
$this->debug("$child should be made active");
// If node is one of the new views then set the active node to the child.
if (!$node instanceof view) {
$this->debug("$node is NOT a view, marking $node active");
$node->make_active();
$result = $node;
} else {
$this->debug("$node IS a view, marking $child active");
$child->make_active();

// Does this even do anything???
$this->activenode = $child;
$result = $child;
}
Expand All @@ -144,9 +154,11 @@ protected function active_node_scan(navigation_node $node,
}
} else {
// Make sure to reset the active state.
$this->debug("$child should not be active. Marking inactive");
$child->make_inactive();
}
}
$this->debug("Finish inspect children of $node");

return $result;
}
Expand Down
101 changes: 101 additions & 0 deletions lib/navigationlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,32 @@ class navigation_node implements renderable {
/** @var bool node that have children. */
public $haschildren;

private static ?string $debugdir = null;
private static ?array $placeholders = null;

public static function get_placeholders() {
$mkplaceholder = fn(string $key): navigation_node => new navigation_node(['key' => $key . '_placeholder', 'text' => '']);
$trees = ['settingsnav', 'navigation', 'secondarynav'];

if (!self::$placeholders) {
self::$placeholders = array_map($mkplaceholder, array_combine($trees, $trees));
}

return self::$placeholders;
}

/**
* Constructs a new navigation_node
*
* @param array|string $properties Either an array of properties or a string to use
* as the text for the node
*/
public function __construct($properties) {

if (count(self::$placeholders ?? []) === 3) {
$this->debug("Constructing node " . ($properties['key'] ?? 'nokey'));
}

if (is_array($properties)) {
// Check the array for each property that we allow to set at construction.
// text - The main content for the node
Expand Down Expand Up @@ -242,6 +261,7 @@ public function __construct($properties) {
*/
public function check_if_active($strength=URL_MATCH_EXACT) {
global $FULLME, $PAGE;

// Set fullmeurl if it hasn't already been set
if (self::$fullmeurl == null) {
if ($PAGE->has_set_url()) {
Expand Down Expand Up @@ -397,6 +417,7 @@ public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=nul
* @return navigation_node The added node
*/
public function add_node(navigation_node $childnode, $beforekey=null) {
$this->debug("Adding $childnode to $this");
// First convert the nodetype for this node to a branch as it will now have children
if ($this->nodetype !== self::NODETYPE_BRANCH) {
$this->nodetype = self::NODETYPE_BRANCH;
Expand Down Expand Up @@ -494,6 +515,7 @@ public function get($key, $type=null) {
* @return bool
*/
public function remove() {
$this->debug("Removing $this from " . $this->parent);
return $this->parent->children->remove($this->key, $this->type);
}

Expand All @@ -515,6 +537,7 @@ public function has_children() {
* rather than having to locate and manually mark a node active.
*/
public function make_active() {
$this->debug("Marking $this as active");
$this->isactive = true;
$this->add_class('active_tree_node');
$this->force_open();
Expand All @@ -528,6 +551,7 @@ public function make_active() {
* doing the same to all parents.
*/
public function make_inactive() {
$this->debug("Marking $this as inactive");
$this->isactive = false;
$this->remove_class('active_tree_node');
if ($this->parent !== null) {
Expand Down Expand Up @@ -592,6 +616,81 @@ public function remove_class($class) {
return false;
}

public function __toString(): string {
return ($this->key ?? '?' . get_class($this)) . ($this->isactive ? '*' : '') . " (" .
"id: " . spl_object_id($this) .
" type: " . $this->type .
" parent: " . ($this->parent ? spl_object_id($this->parent) : "none") .
" ncid: " . (is_object($this->children) ? spl_object_id($this->children) : '?') .
")";
}

public function print_tree(string $pre = ''): string {
$node ??= $this;
$children = iterator_to_array($node->children);
$islast = fn(self $node): bool => $children[array_key_last($children)]->key === $node->key;
$nodeline = fn(self $node): string => $pre . ($islast($node) ? '' : '') . $node;
$childline = fn(self $node): string => $node->has_children() ? $node->print_tree($pre . ($islast($node) ? ' ' : '')) : '';
$merge = fn(string $nodes, self $node): string => $nodes . $nodeline($node) . "\n" . $childline($node);
return array_reduce($children, $merge, empty($pre) ? ($node . "\n") : '');
}

private static function get_debug_frame_base() {
global $CFG;

if (!self::$debugdir) {
self::$debugdir = (new DateTime)->format(DateTime::ATOM);
make_temp_directory('navdebug/' . self::$debugdir);
}

$count = count(glob($CFG->tempdir . '/navdebug/' . self::$debugdir . "/*.trees.txt"));
return $CFG->tempdir . '/navdebug/' . self::$debugdir . "/" . ($count + 1);
}

public static function debug(string $message): void {
global $CFG;

$navdebugenabled = $CFG->enablenavigationnodedebugging === "1" ? true : false;
if(!$navdebugenabled || !optional_param('debugnav', false, PARAM_BOOL)) {
return;
}

global $PAGE;
$base = self::get_debug_frame_base();
$fields = ['function', 'line', 'file', 'class'];
$getfields = fn(array $frame): array => array_intersect_key($frame, array_flip($fields));
$rawbacktrace = array_map($getfields, debug_backtrace());
['function' => $function, 'file' => $file, 'class' => $class] = $rawbacktrace[1];

$pad = fn(int $width): callable => fn(?string $str): string => $str . str_pad(" ", $width - ($str ? mb_strlen($str) : 0));
$distribute = fn(?string ...$strings): string => implode("", array_map($pad(70), $strings));
[$lines, $unlines] = [partial(implode(...), "\n"), partial(explode(...), "\n")];
$align = fn(string ...$texts): string => $lines(array_map($distribute, ...array_map($unlines, $texts)));
$placeholders = self::get_placeholders();

$get = Closure::bind(fn(string $nav): navigation_node => $PAGE->{'_' . $nav} ?? $placeholders[$nav], null, $PAGE);
$trees = [$get('settingsnav'), $get('navigation'), $get('secondarynav')];
$logmsg = "Page info>\n";
$logmsg .= " fullmeurl: " . (self::$fullmeurl ? self::$fullmeurl->out() : "Not set") . "\n";
$logmsg .= " primary active tab: " . ($PAGE->get_primary_activate_tab() ?? "Not set") . "\n";
$logmsg .= " secondary active tab: " . ($PAGE->get_secondary_active_tab() ?? "Not set") . "\n";
$logmsg .= $class . "::" . $function . "> " . $message . "\n\n";
$logmsg .= $align(...array_map(fn(navigation_node $node): string => $node->print_tree(), $trees));
file_put_contents($base . '.trees.txt', $logmsg);

$colour = fn(string $colour, string $str): string => "\033[${colour}m$str\033[0m";
$function = fn(array $frame): string => (isset($frame['class']) ? $colour("1;36", $frame['class']) . "::" : '') . $colour("1;32", $frame['function']);
$file = fn(array $frame): string => str_replace($CFG->dirroot . "/", "", $colour("1;33", $frame['file'] ?? '?')) . ":" . $colour("1;31", "line " . ($frame['line'] ?? '?'));

$backtrace = ['Debug call> ' . $message . "\n", ...array_map(
fn(int $frame): string => ' called from ' .
(isset($rawbacktrace[$frame]) ? $function($rawbacktrace[$frame]) . " in " : "") .
$file($rawbacktrace[$frame - 1]),
range(1, count($rawbacktrace))
)];
file_put_contents($base . '.trace.txt', implode("\n", $backtrace), true);
}

/**
* Sets the title for this node and forces Moodle to utilise it.
*
Expand Down Expand Up @@ -864,13 +963,15 @@ public function get_tabs_array(array $inactive=array(), $return=false) {
* @param navigation_node $parent
*/
public function set_parent(navigation_node $parent) {
$this->debug("Setting $this's parent to $parent");
// Set the parent (thats the easy part)
$this->parent = $parent;
// Check if this node is active (this is checked during construction)
if ($this->isactive) {
// Force all of the parent nodes open so you can see this node
$this->parent->force_open();
// Make all parents inactive so that its clear where we are.
$this->debug("Setting $parent inactive");
$this->parent->make_inactive();
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/pagelib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2435,6 +2435,7 @@ public function has_tablist_secondary_navigation(): bool {
* @param string $navkey the key of the secondary nav node to be activated.
*/
public function set_secondary_active_tab(string $navkey): void {
navigation_node::debug('Setting secondary active tab to ' . $navkey);
$this->_activekeysecondary = $navkey;
}

Expand All @@ -2453,6 +2454,7 @@ public function get_secondary_active_tab(): ?string {
* @param string $navkey
*/
public function set_primary_active_tab(string $navkey): void {
navigation_node::debug('Setting primary active tab to ' . $navkey);
$this->_activenodeprimary = $navkey;
}

Expand Down

0 comments on commit 97bc4ef

Please sign in to comment.