Commits

Matthew Schinckel committed a1f787e

Prepare for release.

  • Participants
  • Parent commits 3c84ef3
  • Tags 1.0

Comments (0)

Files changed (3)

File menus/README.txt

+# django-menus #
+
+django-menus is an app that provides some useful template helpers for rendering and handling menus within django projects.
+
+To use in in your django project, it needs to be installed:
+
+	$ pip install -E /path/to/your/venv django-menus
+
+And `"menus"` needs to be in your `settings.INSTALLED_APPS`.
+
+## menu_item
+
+An inclusion template tag that will create a single instance of a menu item, which will only be rendered if the logged in user can access the referenced view. Secondly, the currently active view will have a CSS class of `active` in it's menu item.
+
+    {% load menu_item %}
+	
+    {% menu_item "/foo/" "Foo" %}
+    {% menu_item "/bar/" "Bar" %}
+
+If we were viewing `/foo/`, this renders to:
+
+    <a class="active" href="/foo/">Foo</a>
+    <a href="/bar/">Bar</a>
+
+Using the standard template. If you want, you can override the `menus/item.html` template to change the display format.
+
+You may also pass in a string like `"url:foo_name"` to the first argument. This will do a `reverse('foo_name')` call (with no args or kwargs) to find a matching url.
+
+If the menu item title is `'home'` (case insensitive), or the url path is `'/'`, then an exact match will be required to mark it as active, otherwise a prefix match is done. This means that if you had a menu item as above, and were viewing the url `/foo/bar/`, then the first menu_item would be marked as active.
+
+
+## tree_menu
+
+An extension to [django-mptt](https://github.com/django-mptt/django-mptt/), this is a template that you can use to have a dynamic tree menu, where selecting items with children expands them, and selecting a leaf node follows the link. To use it, you'll need to have mptt installed into your project as well as this package.
+
+You use it like:
+
+    {% load mptt_tags %}
+    
+    {% block tree_menu %}
+      {% full_tree_for_model app_label.ModelName as menu %}
+      {% include "menu/tree-menu.html" %}
+    {% endblock %}
+	
+If you want it to dynamically hide/show nested data, then you will want to have:
+
+		<script src="{{ STATIC_URL }}menus/js/tree-menu.js"></script>
+		<link rel="stylesheet" href="{{ STATIC_URL }}menus/style/tree-menu.css" 
+			  type="text/css" media="screen" title="no title" charset="utf-8">
+
+Somewhere in your page.
+
+This part is currently in use in one small part of a project, and may change if I start to use it more. This README is a little light on because I haven't touched this code in a long, long time.

File menus/templatetags/menu_item.py

-import types
+"""
+A template tag that creates menu items that introspect the view 
+they point to, only displaying those that the currently logged
+in user can access. It also marks the currently selected item as
+'active' with a css class.
+
+Usage:
+
+    {% menu_item "url:view_name" "Menu Title" %}
+
+If you prefix view_name with url, as shown, then it will use reverse()
+to find the view.  Otherwise, it assumes you have entered the actual
+url.
+
+A third, optional argument is a list of css classes that should be
+applied to this menu item.
+
+Note: If you have urls like /foo/bar/baz/, and your menu is /foo/bar/,
+then this matches the url, and the menu item /foo/bar/ would be selected.
+This would mean you can't use {% menu_item '/' 'Home' %}, so I have a
+couple of special cases:
+    
+* the url '/' is handled specially, only an exact match will cause
+  it to be marked as active.
+* the text 'Menu Title' is compared in a case insensitive fashion
+  to the string 'home': if it matches exactly, then it requires an
+  exact url match (not just a matching prefix) to be marked as
+  active.
+    
+The logic for determining permission to access a view is pretty simple:
+look for any decorators on that view that take a first argument called
+`user` or `u`, and call them with the current request.user object. If
+any fail, then this user may not access that view.
+"""
 
 from django import template
+from django.conf import settings
+from django.core.urlresolvers import Resolver404
+
 register = template.Library()
 from django.core.urlresolvers import reverse, resolve
 
 def get_callable_cells(function):
+    """
+    Iterate through all of the decorators on this function,
+    and put those that might be callable onto our callable stack.
+    
+    Note that we will also include the function itself, as that
+    is callable.
+    
+    This is probably the funkiest introspection code I've ever written in python.
+    """
     callables = []
     if not hasattr(function, 'func_closure'):
         if hasattr(function, 'view_func'):
         return [function]
     for closure in function.func_closure:
         if hasattr(closure.cell_contents, '__call__'):
-            if closure.cell_contents.func_closure:
+            # Class-based view does not have a .func_closure attribute.
+            # Instead, we want to look for decorators on the dispatch method.
+            # We can also look for decorators on a "get" method, if one exists.
+            if hasattr(closure.cell_contents, 'dispatch'):
+                callables.extend(get_callable_cells(closure.cell_contents.dispatch.__func__))
+                if hasattr(closure.cell_contents, 'get'):
+                    callables.extend(get_callable_cells(closure.cell_contents.get.__func__))
+            elif hasattr(closure.cell_contents, 'func_closure') and closure.cell_contents.func_closure:
                 callables.extend(get_callable_cells(closure.cell_contents))
             else:
                 callables.append(closure.cell_contents)
     return callables
 
 def get_tests(function):
+    """
+    Get a list of callable cells attached to this function that have the first
+    parameter named "u" or "user".
+    """
     return [
-        x for x in get_callable_cells(function)[:-1]
-        if isinstance(x, types.LambdaType) or
-        x.func_code.co_varnames[0] in ["user", "u"]
+        x for x in get_callable_cells(function)
+        if x.func_code.co_varnames[0] in ["user", "u"]
     ]
 
 class MenuItem(template.Node):
+    """
+    The template node for generating a menu item.
+    """
     def __init__(self, template_file, url, text, classes=None):
+        """
+        template_file : the name of the template that should be used for each
+                        menu item. defaults to 'menu/item.html', but you can
+                        override this in a new instance of this tag.
+        url:            the url or url:view_name that this menu_item should point to.
+        text:           the text that this menu_item will display
+        classes:        Any CSS classes that should be applied to the item.
+        """
         super(MenuItem, self).__init__()
         self.template_file = template_file or 'menu/item.html'
         url = url.strip('\'"')
             
         self.text = text.strip('\'"')
         self.classes = classes or ""
+        self.nodelist = False
         
     def render(self, context):
+        """
+        At render time, we need to access the context, to get some data that
+        is required.
+        
+        Basically, we need `request` to be in the context, so we can access
+        the logged in user.
+        """
         if self not in context.render_context:
             context.render_context[self] = {
                 'url': self.url,
         local = dict(context.render_context[self])
         
         if 'request' not in context:
+            if settings.DEBUG:
+                raise template.TemplateSyntaxError("menu_item tag requires 'request' in context")
             return ''
-            raise template.TemplateSyntaxError("menu_item tag requires 'request' in context")
         
         request = context['request']
         
+        # To find our current url, look in order at these.
         if 'page_url' in context:
             page_url = context['page_url']
         elif 'flatpage' in context:
         # This is a fairly nasty hack to get around how I have my mod_python (!!!)
         # setup: which sets the SCRIPT_NAME.
         local['url'] = local['url'].replace(request.META.get('SCRIPT_NAME',''), '')
-            
-        view = resolve(local['url']).func
+        
+        # See if that url is for a valid view.
+        try:
+            view = resolve(local['url']).func
+        except Resolver404:
+            if settings.DEBUG:
+                raise
+            return ''
+        
+        # See if the user passes all tests.
+        # Note that any type of Exception will result in a failure.
         try:
             can_view = all([test(user) for test in get_tests(view)])
-        except AttributeError:
-            can_view = False
+        except Exception:
+            if settings.DEBUG:
+                raise
+            return ''
         
+        # If the user can't access the view, this token collapses to an empty string.
         if not can_view:
             return ''
         
+        # Now import and render the template.
         file_name = self.template_file
         
-        if not getattr(self, 'nodelist', False):
+        # Cache the nodelist within this template file.
+        if not self.nodelist:
             from django.template.loader import get_template, select_template
             if isinstance(file_name, template.Template):
                 t = file_name
                 t = get_template(file_name)
             self.nodelist = t.nodelist
         
+        # Special-case: when the menu-item's url is '/' or text is 'home', then we don't mark
+        # it as active unless it's a complete match.
         if page_url.startswith(local['url']):
-            local['classes'] += " active"
+            if (local['url'] != '/' and local['text'].lower() != 'home') or page_url == local['url']:
+                local['classes'] += " active"
         
         new_context = template.context.Context(local)
         return self.nodelist.render(new_context)
         
     
-def tested_menu_item(parser, token):
+def tested_menu_item(_parser, token):
+    """
+    The actual template tag.
+    """
     error_message = "'menu_item' tag requires at least 2, at most 3 arguments"
     
     try:
     if not (3 <= len(parts) <= 4):
         raise template.TemplateSyntaxError(error_message)
     
+    # parts[0] is the name of the tag.
     return MenuItem('menu/item.html', *parts[1:])
 
 register.tag('menu_item', tested_menu_item)
 
 setup(
     name = "django-menus",
-    version = "0.1",
+    version = "1.0",
     description = "Menu helpers for django projects",
     url = "http://bitbucket.org/schinckel/django-menus/",
     author = "Matthew Schinckel",
     ],
     classifiers = [
         'Programming Language :: Python',
-        'License :: Other/Proprietary License',
+        'License :: OSI Approved :: BSD License',
         'Operating System :: OS Independent',
         'Framework :: Django',
     ],