Another menu & breadcrumb application for Django, with support for syncing all links from Python, and allowing website admins to customise the trees.
Maybe. You should try it.
I want to be able to declare menus in Python, but have them be flexible enough to allow for changes to come via client-input data (eg: users)
The idea in brief:
from menuhin.models import MenuItemGroup, URI, ModelURI class MyMenu(MenuItemGroup): def get_urls(self): for i in xrange(1, 10): yield URI(title=i, url='/example/%d/' % i) objs = MyModel.objects.all() for obj in objs: yield ModelURI(title='test', url=obj.get_absolute_url(), model_instance=obj)
That's it.
Discovery of menus is done by configuring a MENUHIN_MENU_HANDLERS
setting,
emulating the form of Django's MIDDLEWARE_CLASSES
:
MENUHIN_MENU_HANDLERS = ( 'myapp.mymenus.MyMenu', )
These Python classes may then be used by the Django admin, or the bundled management command, to import the URL + Title into a tree hierarchy provided by django-treebeard.
To keep the python-written URIs up to date, the following are available:
- a management command,
python manage.py update_menus
- It accepts
--site=N
to target only a specific DjangoSITE_ID
- It accepts
--dry-run
where no inserts will be done. Most useful with--verbosity=2
- It accepts
- The Django admin
Menus
tree view exposes a new Import page, where one of theMENUHIN_MENU_HANDLERS
may be selected, along with aSite
to apply it to. - a Post Save signal handler (
menuhin.listeners.create_menu_url
) to create a newMenuItem
when the given instance is first created, as long as the model has aget_absolute_url
, and optionally, aget_menu_title
orget_title
method - a Pre Save signal handler (
menuhin.listeners.update_old_url
) to updateMenuItem
instances should the original model'sget_absolute_url
change, to keep the URL correct. - a Pre Delete signal handler (
menuhin.listeners.unpublish_on_delete
) for quietly removing menu items which represent URLs that can no longer exist because they've been deleted. - a celery task (
menuhin.tasks.update_urls_for_all_sites
) which may be set up to run periodically to fill in anything missing.
There is a middleware, menuhin.middleware.RequestTreeMiddleware
which
puts the following lazy attributes onto request
:
request.menuitem
- theMenuItem
for the current request, orNone
if no suitable match was found.request.ancestors
- anyMenuItem
instances further up the tree, fromrequest.menuitem
based on the arrangement (in the admin, usually)request.descendants
- allMenuItem
instances below this one.request.siblings
- allMenuItem
instances adjacent to this one in the tree. Includes itself, so there will always be one sibling, I think.request.children
- onlyMenuItem
instances one level directly below this one.
If you don't want the middleware, there are context processors too:
menuhin.context_processors.request_ancestors
exposes the context variableMENUHIN_ANCESTORS
, which should contain the same as the middleware'srequest.ancestors
menuhin.context_processors.request_descendants
exposes the context variableMENUHIN_DESCENDANTS
, which should contain the same as the middleware'srequest.descendants
If a stored title has {{ xyz }}
in it when rendered by the template tags,
the title will be parsed as if it were a Django template, using the
MenuItem
field attributes as kwargs, plus request
if it was in the
parent context.
If the stored title has {x}
in it, and didn't have {{ abc }}
in it,
the title is parsed using the Python string formatting DSL, such that
every field attribute of the MenuItem
is given as a kwarg, as is
request
if it was in the parent context.
Thus, both of the following are valid titles:
hello, {{ request.user|default:'anonymous' }}
hello, {request.user}
A brief overview of the template tags available:
Requires a single argument, which is used to look up the MenuItem
in
question:
{% load menus %} {% show_breadcrumbs request.path %} {% show_breadcrumbs "my-slug" %} {% show_breadcrumbs 4 %}
- If the argument is all digits, it is presumed to be the primary key,
and is used as-is to fetch the
MenuItem
in question, along with it's ancestors. - If the argument is a valid slug (that is, contains no characters invalid
for a
SlugField
) it is treated as such, and is used in combination with the currentSite
(based on theSITE_ID
) to fetch theMenuItem
in question, along with it's ancestors. - If the argument is neither of the above, it is presumed to be a URL,
and so is looked up by
MenuItem
path and the currentSite
(based on theSITE_ID
) to fetch theMenuItem
in question, along with it's ancestors.
The default template for showing breadcrumbs (
menuhin/show_breadcrumbs.html
) puts a whole bunch of CSS classes
and data-* attributes on the HTML elements, so you can customise heavily.
You can change the template used by providing a second argument pointing
at your chosen file:
{% load menus %} {% show_breadcrumbs request.path "a/b/c.html" %}
The tag may also be used to promote a new context variable, which sidesteps the rendering process and ignores the template:
{% load menus %} {% show_breadcrumbs request.path as breadcrumb_data %} {% for node in breadcrumb_data.ancestor_nodes %} {{ node }} {% endfor %}
Takes a string representing a MenuItem
slug and optionally a depth to
descend to from the discovered MenuItem
to display a tree:
{% load menus %} {% show_menu "default" 10 %}
Finds the MenuItem
for the current Site
which matches that slug,
and outputs up to ten levels below it.
The default template (menuhin/show_menu.html
) for showing the menu puts
a whole bunch of CSS classes and data-* attributes on the HTML elements, so
you can customise heavily without needing to override it, though that is
possible too:
{% load menus %} {% show_menu "xyz" 100 "x/y/z.html" %}
Like the show_breadcrumbs
tag, show_menu
may be used to create a new
context variable containing the data otherwise provided to the included
template:
{% load menus %} {% show_menu ... as outvar %} {{ outvar.menu_root }} {% for x in outvar.menu_nodes %} {{ x }} {% endfor %}
There's a menuhin.sitemaps.MenuItemSitemap
which will output all
published menu items for the current site (as set by the SITE_ID
)
Assuming your menus cover most/all of your pages, it's an efficient way to provide the sitemap, though it can be improved by using django-static-sitemaps.
Published MenuItem
instances in the sitemap get a lower priority the
deeper into the tree they are, and the change frequency is dynamically set
depending on how recently the MenuItem
was last changed.
- Test coverage is not 100%.
- Doesn't take querystrings into account yet.
django-menuhin
is available under the terms of the
Simplified BSD License (alternatively known as the FreeBSD License, or
the 2-clause License). See the LICENSE
file in the source
distribution for a complete copy.