Skip to content

Latest commit

 

History

History
338 lines (227 loc) · 16 KB

File metadata and controls

338 lines (227 loc) · 16 KB

Using hooks to modify menus

On loading, Wagtail will search for any app with the file wagtail_hooks.py and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a Page object is saved or when the main menu is constructed.

Registering functions with a Wagtail hook is done through the @hooks.register decorator:

from wagtail.wagtailcore import hooks

@hooks.register('name_of_hook')
def my_hook_function(arg1, arg2...)
    # your code here

Alternatively, hooks.register can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:

hooks.register('name_of_hook', my_hook_function)

Wagtailmenus utilises this same 'hooks' mechanism to allow you make modifications to menus at certain points during the rendering process.

Hooks for modifying QuerySets

When a menu instance is gathering the data it needs to render itself, it typically uses one or more QuerySets to fetch Page and MenuItem data from the database. These hooks allow you to modify those QuerySets before they are evaluated, allowing you to efficiently control menu contents.

If you need to override a lot of menu class behaviour, and you're comfortable with the idea of subclassing the existing classes and models to override the necessary methods, you might want to look at custom_menu_classes. But, if all you want to do is change the result of a menu's get_base_page_queryset() or get_base_menuitem_queryset() (say, to limit the links that appear based on the permissions of the currently logged-in user), you may find it quicker & easier to use the following hooks instead.

menus_modify_base_page_queryset

Whenever a menu needs Page data, the menu's get_base_page_queryset() method is called to get a 'base' queryset, which then has additional filter() and exclude() statements added to it as required.

By default, get_base_page_queryset() applies a few simple filters to prevent certain pages appearing in your menus:

Page.objects.filter(live=True, expired=False, show_in_menus=True)

However, if you'd like to filter this result down further, you can do so using something like the following:

Note

The below example shows only a subset of the arguments that are passed to methods using this hook. For a full list of the arguments supplied, see the hooks_argument_reference below.

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_base_page_queryset')
def make_some_changes(
    queryset, request, menu_instance, original_menu_tag, current_site,

**kwargs

):

""" Ensure only pages 'owned' by the currently logged in user are included. NOTE: MUST ALWAYS RETURN A QUERYSET """ if not request.user.is_authenticated(): return queryset.none() return queryset.filter(owner=self.request.user)

This would ensure that only pages 'owned' by currently logged-in user will appear in menus. And the changes will be applied to ALL types of menu, regardless of what template tag is being called to do the rendering.

Or, if you only wanted to change the queryset for a menu of a specific type, you could modify the code slightly like so:

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_base_page_queryset')
def make_some_changes(
    queryset, request, menu_instance, original_menu_tag, current_site,
    **kwargs
):
    """
    Ensure only pages 'owned' by the currently logged in user are included,
    but only for 'main' or 'flat' menus.
    NOTE: MUST ALWAYS RETURN A QUERYSET
    """
    if menu_type in ('main_menu', 'flat_menu'):
        if not request.user.is_authenticated():
            return queryset.none()
        queryset = queryset.filter(owner=self.request.user)

    return queryset  # always return a queryset

menus_modify_base_menuitem_queryset

When rendering a main or flat menu, top-level items are defined in the CMS, so the menu must fetch that data first, before it can work out whatever additional data is required for rendering.

By default, get_base_menuitem_queryset() simply returns all of the menu items that were defined in the CMS. Any page data is then fetched separately (using get_base_page_queryset()), and the two results are combined to ensure that only links to appropriate pages are included in the menu being rendered.

However, if you'd only like to include a subset of the CMS-defined menu item, or make any further modifications, you can do so using something like the following:

Note

The below example shows only a subset of the arguments that are passed to methods using this hook. For a full list of the arguments supplied, see the hooks_argument_reference below.

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_base_menuitem_queryset')
def make_some_changes(
    queryset, request, menu_instance, original_menu_tag, current_site,
    **kwargs
):
    """
    If the request is from a specific site, and the current user is
    authenticated, don't show links to some custom URLs.
    NOTE: MUST ALWAYS RETURN A QUERYSET
    """
    if(
        current_site.hostname.startswith('intranet.') and 
        request.user.is_authenticated()
    ):
        queryset = queryset.exclude(handle__contains="visiting-only")
    return queryset  # always return a queryset

These changes would be applied to all menu types that use menu items to define the top-level (main and flat menus). If you only wanted to change the queryset for a flat menus, or even a specific flat menu, you could modify the code slightly like so:

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_base_menuitem_queryset')
def make_some_changes(
    queryset, request, menu_instance, original_menu_tag, current_site,
    **kwargs
):
    """
    When generating a flat menu with the 'action-links' handle, and the
    request is for a specific site, and the current user is authenticated,
    don't show links to some custom URLs.
    NOTE: MUST ALWAYS RETURN A QUERYSET
    """
    if(
        original_menu_tag == 'flat_menu' and 
        menu_instance.handle == 'action-links' and
        current_site.hostname.startswith('intranet.') and 
        request.user.is_authenticated()
    ):
        queryset = queryset.exclude(handle__contains="visiting-only")
    return queryset  # always return a queryset

Hooks for modifying menu items

While the above tags are concerned with modifying the data used in a menu, the following hooks are called later on in the rendering process, and allow you to modify the list of MenuItem or Page objects before they are sent to a template to be rendered.

There are two hooks you can use to modify menu items, which are called at different stages of preparation.

menus_modify_raw_menu_items

This hook allows you to modify the list before it is 'primed' (a process that sets href, text, active_class and has_children_in_menu attributes on each item), and before being sent to a parent page's modify_submenu_items() method for further modification (see manipulating_submenu_items).

Note

The below example shows only a subset of the arguments that are passed to methods using this hook. For a full list of the arguments supplied, see the hooks_argument_reference below.

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_raw_menu_items')
def make_some_changes(
    menu_items, request, parent_page, original_menu_tag, menu_instance,
    current_level, **kwargs
):
    """
    When rendering the first level of a 'section menu', add a copy of the
    first page to the end of the list.

    NOTE: prime_menu_items() will attempt to add 'href', 'text' and other
    attributes to these items before rendering, so ideally, menu items
    should all be `MenuItem` or `Page` instances.
    """
    if original_menu_tag == 'section_menu' and current_level == 1:
        # Try/except in case menu_items is an empty list
        try:
            menu_items.append(menu_items[0])
        except KeyError:
            pass
    return menu_items  # always return a list

The modified list of menu items will then continue to be processed as normal, being passed to the menu's 'prime_menu_items()' method for priming, and then on to the parent page's modify_submenu_items() for further modification.

menus_modify_primed_menu_items

This hook allows you to modify the list of items after they have been 'primed' and the modified by a parent page's modify_submenu_items() methods (see manipulating_submenu_items).

Note

The below example shows only a subset of the arguments that are passed to methods using this hook. For a full list of the arguments supplied, see the hooks_argument_reference below.

from wagtail.wagtailcore import hooks

@hooks.register('menus_modify_primed_menu_items')
def make_some_changes(
    menu_items, request, parent_page, original_menu_tag, menu_instance,
    current_level, **kwargs
):
    """
    When rendering the first level of a 'main menu', add an additional
    link to the RKH website

    NOTE: This result won't undergo any more processing before sending to
    a template for rendering, so you may need to set 'href' and 
    'text' attributes / keys so that those values are picked up by menu
    templates.
    """
    if original_menu_tag == 'main_menu' and current_level == 1:
        # Just adding a simple dict here, as these values are all the
        # template needs to render the link
        menu_items.append({
            'href': 'https://rkh.co.uk',
            'text': 'VISIT RKH.CO.UK',
            'active_class': 'external',
        })
    return menu_items  # always return a list

Argument reference

In the above examples, **kwargs is used in hook method signatures to make them accepting of other keyword arguments, without having to declare every single argument that should be passed in. Using this approach helps create leaner, tidier code, and also makes it more 'future-proof', since the methods will automatically accept any new arguments that may be added by wagtailmenus in future releases.

Below is a full list of the additional arguments that are passed to methods using the above hooks:

request

The HttpRequest instance that the menu is currently being rendered for.

parent_context

The Context instance that the menu is being rendered from.

parent_page

If the menu being rendered is showing 'children' of a specific page, this will be the Page instance who's children pages are being displayed. The value might also be None if no parent page is involved. For example, if rendering the top level items of a main or flat menu.

menu_tag

The name of the tag that was called to render the current part of the menu. If rendering the first level of a menu, this will have the same value as original_menu_tag. If not, it will have the value 'sub_menu' (unless you're using custom tags that pass a different 'tag_name' value to the menu class's 'render_from_tag' method)

original_menu_tag

The name of the tag that was called to initiate rendering of the menu that is currently being rendered. For example, if you're using the main_menu tag to render a multi-level main menu, even though sub_menu may be called to render subsequent additional levels, 'original_menu_tag' should retain the value 'main_menu'. Should be one of: 'main_menu', 'flat_menu', 'section_menu' or 'children_menu'. Comparable to the menu_type values supplied to other hooks.

menu_instance

The menu instance that is supplying the data required to generate the current menu. This could be an instance of a model class, like MainMenu or FlatMenu, or a standard python class like ChildrenMenu or SectionMenu.

original_menu_instance

The menu instance that is supplying the data required to generate the current menu. This could be an instance of a model class, like MainMenu or FlatMenu, or a standard python class like ChildrenMenu or SectionMenu.

current_level

An integer value indicating the 'level' or 'depth' that is currently being rendered in the process of rendering a multi-level menu. This will start at 1 for the first/top-level items of a menu, and increment by 1 for each additional level.

max_levels

An integer value indicating the maximum number of levels that should be rendered for the current menu. This will either have been specified by the developer using the max_levels argument of a menu tag, or might have been set in the CMS for a specific MainMenu or FlatMenu instance.

current_site

A Wagtail Site instance, indicating the site that the current request is for (usually also available as request.site)

current_page

A Wagtail Page instance, indicating what wagtailmenus believes to be the page that is currently being viewed / requested by a user. This might be None if you're using additional views in your project to provide functionality at URLs that don't map to a Page in Wagtail.

current_page_ancestor_ids

A list of ids of Page instances that are an 'ancestor' of current_page.

current_section_root_page

If current_page has a value, this will be the top-most ancestor of that page, from just below the site's root page. For example, if your page tree looked like the following:

Home (Set as 'root page' for the site)
├── About us
├── What we do
├── Careers
|   ├── Vacancy one
|   └── Vacancy two
├── News & events
|   ├── News
|   |   ├── Article one
|   |   └── Article two
|   └── Events
└── Contact us

If the current page was 'Vacancy one', the section root page would be 'Careers'. Or, if the current page was 'Article one', the section root page would be 'News & events'.

use_specific

An integer value indicating the preferred policy for using PageQuerySet.specific() and Page.specific in rendering the current menu. For more information see: specific_pages.

allow_repeating_parents

A boolean value indicating the preferred policy for having pages that subclass MenuPageMixin add a repeated versions of themselves to it's children pages (when rendering a sub_menu for that page). For more information see: menupage_and_menupagemixin.

apply_active_classes

A boolean value indicating the preferred policy for setting active_class attributes on menu items for the current menu.

use_absolute_page_urls

A boolean value indicating the preferred policy for using full/absolute page URLs for menu items representing pages (observed by prime_menu_items() when setting the href attribute on each menu item). In most cases this will be False, as the default behaviour is to use 'relative' URLs for pages.