Skip to content

v2.5.0

Compare
Choose a tag to compare
@ababic ababic released this 14 Oct 19:59
· 452 commits to master since this release

What's new?

Class-based rendering behaviour for menus

This version of wagtailmenus sees quite a large refactor in an attempt to address a large amount of repetition and inconsistency in tempate tag code, and to also break the process of rendering menus down into more clearly defined steps that can be overridden individually where needed.

While the existing template tags remain, and are still the intended method for initiating rendering of menus from templates, their responsibilities have diminished somewhat. They are now only really responsible for performing basic validation on the option values passed to them - everything else is handled by the relevant Menu class.

The base Menu class defines the default 'rendering' logic and establishes a pattern of behaviour for the other classes to follow. Then, the more specific classes simply override the methods they need to produce the same results as before.

Below is an outline of the new process, once the menu tag has prepared it's option values and is ready to hand things over to the menu class:

  1. The menu class's render_from_tag() method is called. It takes the current context, as well as any 'option values' passed to / prepared by the template tag.

  2. render_from_tag() calls the class's get_contextual_vals_from_context() method, which analyses the current context and returns a ContextualVals instance, which will serve as a convenient (read-only) reference for 'contextual' data throughout the rest of the process.

  3. render_from_tag() calls the class's get_option_vals_from_options() method, which analyses the provided option values and returns an OptionVals instance, which will serve as a convenient (read-only) reference for 'option' data throughout the rest of the process. The most common attributes are accessible directly (e.g. opt_vals.max_levels and opt_vals.template_name), but some menu-specific options, or any additional values passed to the tag, will be stored as a dictionary, available as opt_vals.extra.

  4. render_from_tag() calls the class's get_instance_for_rendering() method, which takes the prepared ContextualVals and OptionVals instances, and uses them to get or create and return a relevant instance to use for rendering.

  5. In order for the menu instance to handle the rest of the rendering process, it needs to be able to access the ContextualVals and OptionVals instances that have already been prepared, so those values are passed to the instance's prepare_to_render() method, where references to them are saved on the instance as private attributes; self._contextual_vals and self._option_vals.

  6. With access to everything it needs, the instance's render_to_template() method is called. This in turn calls two more instance methods.

  7. The get_context_data() method creates and returns a dictionary of values that need to be available in the template. This includes obvious things such as a list of menu_items for the current level, and other not-so-obvious things, which are intended to be picked up by the sub_menu tag (if it's being used in the template to render additional levels). The menu items are provided by the get_menu_items_for_rendering() method, which in turn splits responsibility for sourcing, priming, and modifying menu items between three other methods: get_raw_menu_items(), prime_menu_items() and modify_menu_items(), respectively.

  8. The get_template() method indentifies and returns an appropriate Template instance that can be used for rendering.

  9. With the data and template gathered, render_to_template() then converts the data into a Context object and sends it to the template's render() method, creating a string representation of the menu, which is sent back for inclusion in the original template.

Hooks added to give developers more options for manipulating menus

While wagtailmenus has long supported the use of custom classes for most things (allowing developers to override methods as they see fit), for a long time, I've felt that it should be easier to override some core/shared behaviour without the technical overhead of having to create and maintain multiple custom models and classes. So, wagtailmenus now supports several 'hooks', which allow you to do just that.

They use the hooks mechanism from Wagtail, so you may already be familiar with the concept. For more information and examples, see the new section of the documentation: :ref:hooks.

New 'autopopulate_main_menus' command added

The 'autopopulate_main_menus' command has been introduced to help developers integrate wagtailmenus into an existing project, by removing some of the effort that is often needed to populating main menu for each project from scratch. It's been introduced as an extra (optional) step to the instruction in: Installing wagtailmenus

Utilises the new add_menu_items_for_pages() method, mentioned below.

New 'add_menu_items_for_pages()' method added for main & flat menus

For each page in the provided PageQuerySet a menu item will be added to the menu, linking to that page. The method has was added to the MenuWithMenuItems model class, which is subclassed by AbstractMainMenu and AbstractFlatMenu, so you should be able to use it on custom menu model objects, as well as objects using the default models.

Overriding 'get_base_page_queryset()' now effects top-level menu items too

Previously, if you overrode get_base_page_queryset() on a custom main menu or flat menu model, the page-tree driven part of the menu (anything below the top-level) would respect that, but top-level menu items linking to pages excluded by get_base_page_queryset() would still be rendered.

Now, 'top_level_items' has been refactored to call get_base_page_queryset() to filter down and return page data for items at the top level too, so developers can always expect changes to get_base_page_queryset() to be reflected throughout entire menus.

'MenuItemManager.for_display()' now returns all items, regardless of the status of linked pages

When sourcing data for a main or flat menu, it doesn't make sense to apply two sets of filters relating to pages status/visibility, so 'for_display' now simply returns ALL menu items defined for a menu, and any unsuitable page links are filtered out in a menu instances 'top_level_items' by calling upon 'get_base_page_queryset'.

Minor changes & bug fixes

  • Fixed an issue with runtests.py that was causing tox builds in Travis CI
    to report as successful, even when tests were failing. Contributed by
    Oliver Bestwalter (obestwalter).
  • The stop_at_this_level argument for the sub_menu tag has been
    officially deprecated and the feature removed from documentation. It hasn't
    worked for a few versions and nobody has mentioned it, so this is the first
    step to removing it completely.
  • Made the logic in 'pages_for_display' easier to override on custom menu
    classes by breaking it out into a separate 'get_pages_for_display()'
    method (that isn't decorated with cached_property).
  • Added support for Wagtail 1.12

Upgrade considerations

The ChildrenMenu's 'root_page' attribute is deprectated in favour of 'parent_page'

In previous versions, the ChildrenMenu and SectionMenu classes both extended the same MenuFromRootPage class, which takes root_page as an init argument, then stores a reference to that page using an attribute of the same name.

The ChildrenMenu class has now been updated to use parent_page as an init argument and attribute name instead, which feels like a much better fit. This same terminology has also been adopted for the SubMenu class too.

If you're subclassing the ChildrenMenu class in your project, please update any code referencing root_page to use parent_page instead. Support for the old name will be removed in version 2.7.

'MenuWithMenuItems.get_base_menuitem_queryset()' no longer filters the queryset

By default, the queryset returned by 'get_base_menuitem_queryset' on menu instances will now return ALL menu items defined for that menu, regardless of the status / visibility of any linked pages.

Previously, the result was filtered to only include pages with 'live' status, and with a True 'show_in_menus' value.

If you're calling 'get_base_menuitem_queryset' anywhere in your project, and are relying on the original method to return the same value as it did before, you will need to apply the additional filters to the queryset, like so:

from django.db.models import Q

...

menu_item_qs = menu.get_base_menuitem_queryset()
menu_item_qs = menu_item_qs.filter(
    Q(link_page__isnull=True) |
    Q(link_page__live=True)
    Q(link_page__expired=False) &
    Q(link_page__show_in_menus=True)
)

'MenuItemManager.for_display()' no longer filters the queryset

If you are subclasssing MenuItemManger to create managers for your custom menu item models, and are relying on the original 'for_display' method to filter out links based on their linked page's status/visibility, you may wish to revise your code to filter out the pages as before, like so:

from django.db.models import Q
from wagtailmenus.managers import MenuItemManager

...

class CustomMenuItemManager(MenuItemManager):

    def for_display(self):
        qs = super(CustomMenuItemManager, self).for_display()
        qs = qs.filter(
            Q(link_page__isnull=True) |
            Q(link_page__live=True) &
            Q(link_page__expired=False) &
            Q(link_page__show_in_menus=True)
        )
        # Now apply any custom filters
        ...
        # Return queryset
        return qs

The sub_menu tag will raise an error if used in a non-menu template

Despite the docs always having stated that the 'sub_menu' tag is only intended for use in menu templates for other types of menu; Up until now, it has functioned similarly to the 'children_menu' tag if used in a regular Django template. But, if you try to call 'sub_menu' from anything other than a menu template now, a SubMenuUsageError error will now be raised.

I highly doubt this will trip anybody up, but sorry if it does. Recent versions of Django seem to swallow deprecation warnings when they occur in the course of rendering a template tag, so even if there were a deprecation period for this, the warnings probably wouldn't have been seen by anyone.

wagtailmenus.models.menus.MenuFromRootPage is deprecated

With ChildrenMenu being refactored to use 'parent_page' as an attribute instead of 'root_page', and the new SubMenu menu class taking a similar approach, the MenuFromRootPage name only seems relevent to SectionMenu, so it has been deprecated in favour of using a more generically-named MenuFromPage class, which is subclassed by all three.

wagtailmenus.menu_tags.prime_menu_items() is deprecated

The method has been superseded by new logic added to the Menu class.

wagtailmenus.menu_tags.get_sub_menu_items_for_page() is deprecated

The method has been superseded by new logic added to the Menu class.

wagtailmenus.utils.misc.get_attrs_from_context() is deprecated

The method has been superseded by new logic added to the Menu class.

wagtailmenus.utils.template.get_template_names() is deprecated

The method has been superseded by new logic added to the Menu class.

wagtailmenus.utils.template.get_sub_menu_template_names() is deprecated

The method has been superseded by new logic added to the Menu class.