Dynamic Custom Menus by Categories

Dynamic Custom Menus by Categories

by Adam Morris -
Number of replies: 11

In Moodle 2.2, I'm trying to get my custom menus to dynamically organize itself by categories. For example:

Home      Cat1             Cat2
|         |                |
-Bizz     -Course1         -Course3
|         |                |
-Bam      -Course2         -Course4

I have already successfully overriden the core_renderer class, and have all the user's enrolled courses listed as a "flat" tree. I see from…

http://phpdocs.moodle.org/HEAD/moodlecore/navigation/navigation_node.html

…that navigation_node doesn't seem to have a way to access a course's categories.

How do I:
* Loop through categories, finding the courses that way, in the theme's core_renderer context
-or-
* Loop through courses, getting their category name along the way, in the theme's core_renderer context

Since this code will be executed with every page refresh, speed is parmount. Thanks for any help or advice.

Average of ratings: -
In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Mary Evans -
Picture of Core developers Picture of Documentation writers Picture of Peer reviewers Picture of Plugin developers Picture of Testers

If you take a look at Aardvark Automenu in the new Plugins database, you will see it is a Moodle 1.9 theme which pulls all the course categories together to form the menu. I tried converting this into a Moodle 2.0 theme 2 years ago but ran into problems and have not looked at it since.

From memory the php for the menu is in menubar.php. If you manage to work it out let me know, or if you get stuck ask for help.

Cheers

Mary

In reply to Mary Evans

Re: Dynamic Custom Menus by Categories

by Adam Morris -

Thanks for your help Mary. Your code is actually in topmenu.php.

I'm really hoping that I could use a navigation_node, do I really have to hit the database with get_courses($id) to access the categories?

In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Mary Evans -
Picture of Core developers Picture of Documentation writers Picture of Peer reviewers Picture of Plugin developers Picture of Testers

As far a I know yes, but I may be wrong. Have you looked at the structure of the DB tables for categories?  They are in table mdl_course_categories and linked to mdl_courses as a one to many (ONE category has MANY courses).  So the $COURSE->id is important in determining the $COURSE->category.

You need to ask Sam Hemelryk about NAVIGATION as he looks after that side of things in Moodle CORE.

Mary

In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Sam Hemelryk -

Hi Adamn, First up I figure you've found Themes 2.0 adding courses and categories to the custom menu. If you choose to go with the database interaction then that is certainly one way to go. But as you've noted does involve fetching all of that information from the database. Utilising the navigation is the other way to go as you suspect, but perhaps not as easy as you would hope. The navigation generates only what is needed otherwise performance goes out the window on large sites. It was decided early on that categories should not be shown by the navigation by default for the My courses . So the first thing you'll need to do on your site if you want to get this information from the navigation is to ensure that it is going to be there. You can turn on categories in the my courses branch by enabling "Show my course categories" in the settings block (Site administration > Appearance > Navigation). Next we need to get the categories from the structure. Within your theme renderers render_custom_menu you can do the following: $categories = array(); $mycourses = $this->page->navigation->get('mycourses'); if ($mycourses && $mycourses->has_children()) { $categories = $mycourses->children->type(navigation_node::TYPE_CATEGORY); } After that code you can be sure that $categories is an array with 0 or more categories in it depending upon whether the user was logged in and whether they are enrolled in more than one course. Perhaps a couple of things to explain: # If the user is not logged in there will be no my courses branch to work from. You could in that circumstance use the courses branch instead. # If there is only one category to be shown then it will not be shown even if settings say to show categories. From there you should be able to refer to that tutorial again. You need to iterate the categories array and add them to the custom menu. The line for adding a navigation node (using full text rather than short) would be something like this: $menu->add($node->get_content(true), $node->action, $node->get_title()) { For each category you will also need to check for subcategories and of course courses. You can get an array of each in the following way I imagine: $subcategories = $category->children->type(navigation_node::TYPE_CATEGORY); $courses = $category->children->type(navigation_node::TYPE_COURSE); Of course best you do that recursively. One final thing as well. The custom menu in Moodle is only styled to a depth of 3 items I think. You will need to take that into consideration if you are going to have more than 3 levels of categories. Hope that all helps. Cheers Sam

(Edited by Mary Evans - original submission Tuesday, 19 June 2012, 12:04 AM) Just fixed a link to the tutorial at start of this comment.

Average of ratings: Useful (1)
In reply to Sam Hemelryk

Re: Dynamic Custom Menus by Categories

by Adam Morris -

Okay, this is what I have. At the moment, there are no errors, but it's not giving me any menu items at all, because this line:

$categories = $mycourses->children->type(navigation_node::TYPE_CATEGORY);

Is returning nothing. I checked the administration option (it is on), and noticed that "Show course categories" says:

This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.

Is that why?

Entire renderers.php source follows:

 

<?php

class theme_nimble_core_renderer extends core_renderer {

        protected function render_categories(custom_menu $parent, stdClass $category) {
                $parent->add($parent->get_content(true), $category->action, $category->get_title());
                $subcat = $catnode->children->type(navigation_node::TYPE_CATEGORY);
                if ($subcat && $subcat->has_children()) {
                        foreach ($subcat->children as $s) {
                                render_categories($category, $s);
                        }
                }
                $courses = $category->children->type(navigation_node::TYPE_COURSE);
                foreach ($courses->children as $coursenode) {
                        $category->add($coursenode->get_content(true), $coursenode->action, $coursenode->get_title());
                }
        }

        protected function render_custom_menu(custom_menu $menu) {

                $categories = array();
                $mycourses = $this->page->navigation->get('mycourses');

                if (isloggedin() && $mycourses && $mycourses->has_children()) {
                        $categories = $mycourses->children->type(navigation_node::TYPE_CATEGORY);

                        if ($categories && $categories->has_children()) {
                                foreach ($categories->children as $catnode) {
                                        render_categories($menu, $catnode);
                                }
                        }
                }

                return parent::render_custom_menu($menu);
        }

}
In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Adam Morris -

Turns out I was looking at the wrong setting. When I got the right setting, I then got the following error.

Call to a member function has_children() on a non-object in /var/www/moodle/theme/nimble/renderers.php on line 27

That is referring to this line:

                        if ($categories && $categories->has_children()) {

I'm not proficient enough with php to track this down exactly, but it seems the navigation lib makes the $categories variable an orderedcollection? So how is that a non-object?

Any help greatly appreciated.

In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Sam Hemelryk -
Hi Adam,

You are headed in the right direction, but the code isn't quite there yet. I've had a quick look at it and you should try the following code.


protected function render_custom_menu(custom_menu $menu) {
// First check if the user is logged in. No point proceeding if they arn't
if (isloggedin()) {
// Get the my courses branch. If it doesn't exist (not enrolled in any courses) then
// this will be false otherwise it will be a navigation_node instance.
$mycourses = $this->page->navigation->get('mycourses');
if ($mycourses && $mycourses->has_children()) {
// Get the category nodes within the my courses branch. This will return an array of navigation_node instances.
// If there arn't any categories this will return an empty array.
$categories = $mycourses->children->type(navigation_node::TYPE_CATEGORY);
foreach ($categories as $catnode) {
// Add each category to the custom menu structure we already have (gets added to the end)
$this->add_category_to_custom_menu($menu, $catnode);
}
}
}
return parent::render_custom_menu($menu);
}

protected function add_category_to_custom_menu(custom_menu $menu, navigation_node $category) {
// We use a sort starting at a high value to ensure the category gets added to the end
static $sort = 1000;

// Add the category to the menu and collect its node. We need to node so that we can add subcategories and courses.
$node = $menu->add($category->get_content(true), $category->action, $category->get_title(), $sort++);

// Add subcategories to the category node by recursivily calling this method.
$subcategories = $category->children->type(navigation_node::TYPE_CATEGORY);
foreach ($subcategories as $subcategory) {
// We need to provide the category node and the subcategory to add
$this->add_category_to_custom_menu($node, $subcategory);
}

// Now we add courses to the category node in the menu
$courses = $category->children->type(navigation_node::TYPE_COURSE);
foreach ($courses as $course) {
$node->add($course->get_content(true), $course->action, $course->get_title());
}
}


Its based upon the code you have there but tidies up a couple of things. You'll notice I renamed a couple of things just to make there names clearer but otherwise same sort of structure.
Have a read, see how you go with understanding it, and ask any questions you have smile

Cheers
Sam
In reply to Sam Hemelryk

Re: Dynamic Custom Menus by Categories

by Adam Morris -

This works as advertised, but needs a bit of modification. First of all get_title() gets you the long names of the courses and the categories, which is probably what your users expect.

Also if you have categories within categories with the above code you end up passing with custom_menu_item to a function that expects custom_menu. Solution is to change the following line in the recursive function:

$this->add_category_to_custom_menu($node$subcategory);

to 

$this->add_category_to_custom_menu_item($node$subcategory);

and define the new function as below:

    protected function add_category_to_custom_menu_item(custom_menu_item $menu_item, navigation_node $category) {
        static $sort = 1000;
        $node = $menu_item->add($category->get_title(), $category->action, $category->get_title(), $sort++);
        $subcategories = $category->children->type(navigation_node::TYPE_CATEGORY);
        foreach ($subcategories as $subcategory) {
           $this->add_category_to_custom_menu_item($node, $subcategory);
        }
        $courses = $category->children->type(navigation_node::TYPE_COURSE);
        foreach ($courses as $course) {
            $node->add($course->get_title(), $course->action, $course->get_title());
        }
    }

In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Mary Evans -
Picture of Core developers Picture of Documentation writers Picture of Peer reviewers Picture of Plugin developers Picture of Testers

Thanks for this Adam. I'll have a play with this code and see how it all fits together.

Cheers

Mary

In reply to Mary Evans

Re: Dynamic Custom Menus by Categories

by Adam Morris -

Another improvement to this code would be detecting if the user is an administrator. My use case is that site admins should be able to navigate their way around the courses (using a single "admin" drop-down) to help support the regular users.

In reply to Adam Morris

Re: Dynamic Custom Menus by Categories

by Mary Evans -
Picture of Core developers Picture of Documentation writers Picture of Peer reviewers Picture of Plugin developers Picture of Testers

Hi Adam or Sam,

Hope you get notification of this comment?

I've been playing with this code and cannot get any of it to work. On the face of it it looks as though it should work, so I am wondering if I am missing something here?

Hope you get to read this and can give me a reply...that would be great!

Thanks

Mary