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.