So I wrote it by myself with such assumptions:
- I do really need only flat menus - but still I can use several flat menus to emulate nested menus.
- Menuing should work in every view without putting any arbitrary @decoration or python code to view function code.
- Menu should be able to draw items in the way that template designer wants it to have, no hardcoding of CSS classes for "selected" items.
- No database hits for getting menu - this is obvious, menu does not change at all, why to hit database for it?
- Finally the clever thing - the fact that any item of menu is selected should be evaluated from current URL of view request.
So the first thing to do is to create reusable django app.
~$ ./manage.py createapp menus
Definition of the template tag is the only thing that is important in the menus app:
./menus/ ./menus/templatetags/ ./menus/templatetags/__init__.py ./menus/templatetags/menus.py
In menus.py put following code:
from django.template import Library, Node, TemplateSyntaxError import re register = Library() def match_path(match, path): if re.search(match, path): return 'selected' else: return '' class MenusNode(Node): def __init__(self, model, varname): self.varname = varname self.model = model self.project_menus = __import__('menu') def render(self, context): menu_list = getattr(self.project_menus, self.model)() menu_list = map(lambda x: {'name': x['name'], 'url':x['url'], 'selected': match_path(x['match'], context['request'].path)} , menu_list) context[self.varname] = menu_list return '' def get_menu(parser, token): bits = token.contents.split() if len(bits) != 4 or bits[2] != 'as' : raise TemplateSyntaxError, "bad syntax: get_menu menu_name as menu_variable for url" return MenusNode(bits[1], bits[3]) get_menu = register.tag(get_menu)
Now add new app to your settings.py to INSTALLED_APPS.
Create in your project root dir (the same where you keep settings.py) file called menu.py.
menu.py
from django.core.urlresolvers import reverse def menu_main(): return [ { 'name' : 'Home', 'url' : '/', 'match' : r'^/$', }, { 'name' : 'Pricing', 'url' : '/pricing/', 'match' : r'^/pricing/', }, { 'name' : 'Blog', 'url' : '/blog/', 'match' : r'^/blog/', }, { 'name' : 'Contact', 'url' : '/contact/', 'match' : r'^/contact/', }, { 'name' : 'Sign-in', 'url' : reverse('accounts.views.signin'), 'match' : r'^' + reverse('django.contrib.auth.views.login'), }, { 'name' : 'Your account', 'url' : reverse('accounts.views.accont'), 'match' : r'^/logged/', }, ]
That's all folks. Now you can use your menu in the template. If this is main menu, it is wise to use it in top level template that you will extend from.
{% load menus %} <html> ... <body> ... <div id="menu"> <ul> {% get_menu menu_main as menu_main %} {% for menu_item in menu_main %} <li><a href="{{menu_item.url}}" class="{{menu_item.selected}}">{{menu_item.name}}</a></li> {% endfor %} </ul> </div> ... </body> </html>
As you can see, your menu will be generated from the data structure that is returned after calling appropriate menu name from menu.py. This allows you to even create dynamic menus if needed.
The format of structure is a list of tuples. Each tuple contain three values:
- Display name for menu.
- URL address that the menu item should direct to after clicking.
- Regex to match against current URL in order to determine if particular item should be marked as "selected". This simple approach gives you many possibilites to activate menu items on different set of URLs by making alternations in regex.
Avoiding hardcoding
- If you don't want to hardcode regular expressions into menu.py, just change them to r'^' + reverse(...). This will create a regular expression from the link you are pointing to (what is usually good idea).
- If you don't want to hardcode class="selected" just change it to class="{% if menu_item.selected %}my_fancy_selected_class {% endif %}".
Nested menu
In my case nested menu is when you want to have a main menu at the same time with additional menu (e. g. for logged user). One of items from main menu may be selected as an ancestor item for logged "sub" menu.
You can easily create the second menu, only remember to make URLs in prefix style. For example:
http://mydomain.tld/pricing http://mydomain.tld/blog http://mydomain.tld/contact http://mydomain.tld/logged/view1 http://mydomain.tld/logged/view2 http://mydomain.tld/logged/view2/add/new/item http://mydomain.tld/logged/view3 http://mydomain.tld/logged/view3/some/other/action
When you try to display both menus the item "Your Account" in menu_main will still be active for all "logged" pages because regex will still match for this urls (there is no $ sign at the end of regex). And you can have your nested menu_logged with some different items to display.
def menu_logged(): return [ { 'name' : 'Action 1', 'url' : reverse('myapp.logged.view1'), 'match' : r'^' + reverse('myapp.logged.view1'), }, { 'name' : 'Action 2', 'url' : reverse('myapp.logged.view2'), 'match' : r'^' + reverse('myapp.logged.view2'), }, { 'name' : 'Action 3', 'url' : reverse('myapp.logged.view3'), 'match' : r'^' + reverse('myapp.logged.view3'), }, ]
Notice that every other action made from view2 and view3 will also select Action 2/Action 3 in menu_logged. That's because we use URL prefixing for organizing the URL namespace of django project. Even if that additional actions does not have their menu items - with url prefixing you can assign them to the particular menu item.
No comments:
Post a Comment