Yawd website

may20

Using signals to generate a decoupled tree-menu application in django

Although there exist several django menu applications -the excellent django-treemenus is one of them-, they all require that the user explicitly sets the menu structure. Consider the following scenario in a django CMS application: The user will probably be able to define a list of article categories -and sub-categories- and will also write a bunch of articles tied to each one of these categories. At the same time, this hypothetical user would also be able to use a blogging engine and publish his thoughts. Let's assume that the user wants to add an article category to the menu and use its sub-categories as sub-menuitems. He also wants to add a link to the blog main page and use the most popular tags as sub-menuitems. To accomplish this he must manually define a menuitem for each menu link and map it to the corresponding url, which is less than ideal. In this article, we will demonstrate a menu application that would enable the user to define a top-level item -e.g. an article category- and optionally have all children items automatically inherited from the corresponding parent element.

First we need to define our menu model structure, which is inspired by the django-treemenus application.

models.py:
from django.db import models
from django.utils.translation import ugettext as _
from myproject.core.models import UnorderedPageBase, CategoryBase

class Menu(models.Model):
    name = models.CharField(max_length=255)
    published = models.BooleanField(default=True)

    class Meta:
        verbose_name_plural = _('Menus')

    def __unicode__(self):
        return u'%s' % self.name

class MenuItem(models.Model):
    menu = models.ForeignKey('Menu', blank=True, null=True, related_name="items")
    parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
    name = models.CharField(max_length=255)
    url = models.CharField(max_length=255, blank=True, null=True)
    auto_inherit_children = models.BooleanField(default=False)
    order = models.IntegerField(default=0)
    published = models.BooleanField(default=True)
    sort = models.IntegerField(default=0)

    class Meta:
        abstract = True
        ordering = ['sort', 'name']
        
    @classmethod
    def re_sort(cls):
        cats = sorted([ (x.get_full_order(), x) for x in cls.objects.all() ])
        for i in range(len(cats)):
            cat = cats[i]
            cat[1].sort = i
            super(MenuItem, cat[1]).save()

    def save(self, *args, **kwargs):
        super(MenuItem, self).save(*args, **kwargs)
        self.re_sort()
        
    def get_display_name(self,get_name=True):
        if self.parent:
            return u'%s|- %s' % (self.get_spaces(), self.name)
        else:
            return u'%s' % self.name
        
    get_display_name.allow_tags = True
    get_display_name.admin_order_field = 'sort'
    get_display_name.short_description = 'Name'
    
    def get_spaces(self):
        if self.parent:
            return u'%s.      ' % self.parent.get_spaces()
        return u''
       
    def get_full_order(self):
        if self.parent:
            return u'%s%d' % (self.parent.get_full_order(), self.order)
        elif self.menu:
            return u'%s%d%s' % (self.menu.name, self.order, self.name)
        return u'%d%s' % (self.order, self.name)
       
    def __unicode__(self):
        if self.parent:
            return u'%s::%s' % (self.parent.__unicode__(), self.name)
        elif self.menu:
            return u'%s::%s' % (self.menu.__unicode__(), self.name)
        return u'%s' % (self.name)

We have defined a Menu model, to enable separate menus in one page -e.g. main-menu, left-menu, registered-user-menu etc. The MenuItems belong to a menu and can be recursive. To achieve this, we defined the 'parent' attribute that points to a MenuItem instance. Obviously, the auto_inherit_children boolean will be used to indicate whether the user wants child elements to derive from the underlying menuitem model or not - we expand on this later on. Other than that, note the field 'sort', used to display recursive elements with the correct, tree-like, order.

MenuItems listAdmin panel screenshot. The 'sort' field is responsible for the item order. The 'order' field is used for fields of the same tree level.

To generate the administration interface shown above, we need to create our admin.py file that simply defines the Menu and MenuItem admin classes.

admin.py
from django.contrib import admin
from myproject.menu.models import MenuItem, Menu

class MenuAdmin(admin.ModelAdmin):
    list_display = ('name', 'published')

class MenuItemAdmin(admin.ModelAdmin):
    list_display = ('get_display_name', 'menu', 'published', 'order', 'id')
    exclude = ('sort',)

admin.site.register(Menu, MenuAdmin)
admin.site.register(MenuItem, MenuItemAdmin)

Now we neeed to develop a template tag that will print our menu. To do so, we create a 'templatetags' package inside our application and define a new module named 'menu_tags.py'.

templatetags/menu_tags.py
from django import template
from django.shortcuts import get_object_or_404
from django.core.urlresolvers import resolve
from django.conf import settings
from models import MenuItem, Menu
from signals import signal_menu

register = template.Library()

@register.inclusion_tag('menu.html', takes_context=True)
def menu_as_ul(context, name):
    menu = get_object_or_404(Menu, name=name, published=True)
    rootitems = menu.items.filter(published=True, parent=None)
    return {
            'menu' : menu,
            'rootitems' : rootitems, 
            'MEDIA_URL' : context['MEDIA_URL'],
            'MENU_JS_URL' : settings.STATIC_URL+ 'js/menu.js',
            'request' : context['request'] 
            }

@register.inclusion_tag('menuitem.html', takes_context=True)
def menuitem_children_as_ul(context, menuitem):
    children = []
    if (hasattr(menuitem, 'auto_generated') and menuitem.auto_inherit_children) 
                    or menuitem.auto_inherit_children:
        resolved = resolve(menuitem.url)
        signal_menu.send(sender=get_class( '%s.%s' % ( resolved.func.__module__, 
            resolved.func.__name__) ), item=menuitem,
            children=children, args=resolved.args, kwargs=resolved.kwargs)
    elif not hasattr(menuitem, 'auto_generated'):
        children = menuitem.children.filter(published=True)

    return {
            'parent' : menuitem,
            'items' : children,
            'MEDIA_URL' : context['MEDIA_URL'],
            }

def get_class( kls ):
    parts = kls.split('.')
    module = ".".join(parts[:-1])
    m = __import__( module )
    for comp in parts[1:]:
        m = getattr(m, comp)            
    return m     

The tag itself is fairly simple. It looks for the requested Menu object, finds its children elements and feeds them to a template that produces the html output. To generate the list of sub-menuitems, the script checks whether the 'auto_inherit_children' boolean field is True. If not, it just recursively adds MenuItems. If 'auto_inherit_children' is True, the tag will send a django signal using the URL's corresponding view class as sender. This view class' signal listener is now responsible for updating the sub-menuitems list for this element. This implementation assumes class-based view implementations. Class-based views were introduced in Django 1.3.

The django signal is simply defined as follows. It sends the menuitem list, so that listeners can update it.

signals.py
from django.dispatch import Signal

signal_menu = Signal(providing_args=['item','children','args','kwargs'])

Before proceeding, we also need to create the templates that generate our html output. Create a new 'templates' directory inside the menu application's root and add the following files.

templates/menu.html
{% load menu_tags %}
<ul class="menu_top" id="{{menu.name}}">
{% for item in rootitems %}
  <li class="menuitem_top">
    <a class="menuitemlink_top menuitemlink_megamenu outer" href="{{item.url}}">
    	{{item.name}}
    </a>
    {% menuitem_children_as_ul item %}
  </li>
{% endfor %}
</ul>
<script type="text/javascript" src="{{MENU_JS_URL}}"></script>


templates/menuitem.html
{% load menu_tags %}
{% if items %}
<div class="{% if parent.parent or parent.auto_generated %}side{% else 
%}drop{% endif %}menu">
	<ul>
	{% for item in items %}
        <li class="menu_item_top">
          <div class="menu_item_link">
            <a href="{{item.url}}"><span>{{ item.name }}</span></a>
          </div>
          {% menuitem_children_as_ul item %}
        </li>
    	{% endfor %}
	</ul>				
</div>
{% endif %}

Of course, you also need to specify the javascript that will handle the drop-down menu along with some css work. These are not covered by this article.

Adapter classes

Any third-party application defines its own models and functions. For our menu application to render a menuitem, each listener must wrap its own models to a form that the menu application understands. To achieve this, we define an adapter class that listeners should use. We create a new module in our menu application, named adapters.py

adapters.py
class MenuItemAdapter(object):
    
    class Children(object):
        def __init__(self):
            self.all = []

    def __init__(self, name=None, url=None, parent=None,
                    auto_inherit_children=True, published=True):
        
        self.name, self.url, self.parent, = name, url, parent
        self.auto_inherit_children, self.published = auto_inherit_children, published
        self.auto_generated = True
        self.children = MenuItemAdapter.Children()

Listener example

Now that the menu application is ready, let's assume that an article application defines articles and article categories. In its model file, we need to declare our receiver as follows:

some_application/models.py
from django.db import models
from menu.signals import signal_menu
from receivers import content_category_menu 
from views import ContentCategoryArticlesListView, ArticleDetailView

class ContentCategory(models.Model):
    ...name=
parent=
....
class Article(ArticleBase): ...model definition here... #signal receiver to generate breadcrumb & menu signal_menu.connect(content_category_menu, sender=ContentCategoryArticlesListView, dispatch_uid="menu_content_category")

Finally, we must implement the receiver funtion:

some_application/receivers.py
from django.shortcuts import get_object_or_404
from django.core.urlresolvers import reverse
from menu.adapters import MenuItemAdapter

def content_category_menu(sender, **kwargs):
    from models import ContentCategory
    category = get_object_or_404(ContentCategory,published=True, 
            pk=kwargs['kwargs']['cat_id'])
    
    for cat in category.children.filter(published=True):
        kwargs['children'].append(menuitem_category_adapter(cat, kwargs['item']))

    for article in category.articles.filter(published=True):
        kwargs['children'].append(menuitem_article_adapter(article, kwargs['item']))

def menuitem_category_adapter(cat, parent=None):
    obj = MenuItemAdapter(name=cat.name, url=cat.get_absolute_url(), parent=parent)
    for child in cat.children.filter(published=True):
        obj.children.all.append(menuitem_category_adapter(child))
    for child in cat.articles.filter(published=True):
        obj.children.all.append(menuitem_article_adapter(child))
    return obj    

def menuitem_article_adapter(article, parent=None):
    obj = MenuItemAdapter(name=article.name, url=article.get_absolute_url(), 
            auto_inherit_children=False, parent=parent)
    return obj

That's it! Our menu application is decoupled from any third-party applications our django-powered project may use! We need to define a receiver function, register a listener and it works.

Meta

Published: May 20, 2011
Comments:  
Word Count: 2,048

Follow-Up Articles

Comments powered by Disqus