views:

445

answers:

2

Hi,

I want to create dynamic menus according to user permissions. As was already discussed here and by the the documentation itself, I know that I can achieve this in the templates using the following snippet:

{% if perms.polls.can_vote %}
    <li>
        <a href="/polls/vote">Vote</a>
    </li>
{% endif %}

But the problem is that for security reasons I want to limit the access to the views too. The snippet that I found in the documentation is the following:

from django.contrib.auth.decorators import permission_required

def my_view(request):
    # ...
my_view = permission_required('polls.can_vote', login_url='/loginpage/')(my_view)

Isn't this against DRY principle? Isn't there a way to define only in one place what is the permission needed for each url? Perhaps in urls.py?

+2  A: 

EDIT: (See end of post for the original text of the answer with the initial, simple idea.)

After being kindly stricken with a cluebat (see the OP's comment below), I find I can see more to the problem than before. Sorry it took so long. Anyway:

Would this kind of template be alright for you?

{% for mi in dyn_menu_items %}
  {% if mi.authorised %}
     <a href={{ mi.url }}>{{ mi.title }}</a>
  {% endif %}
{% endfor %}

To make this work on the Python side, you could use RequestContext in your views with a custom context processor setting the dyn_menu_items variable appropriately. In case some background information is required, the Advanced Templates chapter of the Django Book introduces RequestContext, shows how to use it with render_to_response (kinda important :-)) etc.

Also, I guess at this point it could be useful to put the view functions responsible for the locked-up sections of your site in a list somewhere:

_dyn_menu_items = [(url1, view1, title1, perm1), ...]

Then you could map a couple of functions, say prepare_pattern and prepare_menu_item across that list, having it work roughly like so:

def prepare_pattern(menu_item):
    url1, view, title, perm = menu_item
    pattern = PREPARE_URLCONF_ENTRY_SOMEHOW(...) # fill in as appropriate
    return pattern

def prepare_menu_item(menu_item):
    url, view, title, perm = menu_item
    mi = PREPARE_THE_BIT_FOR_REQUESTCONTEXT(...) # as above
    return mi

These could be combined into a single function, of course, but not everybody would find the result more readable... Anyway, the output of map(prepare_menu_item, _dyn_menu_items) would need to be a dictionary to be passed to your views by a helpful context processor (the figuring out of which, it being the slightly tedious bit here, I'll leave to you ;-)), whereas the output of map(prepare_pattern, _dyn_menu_items), let's call it dyn_menu_patterns, would be used in patterns('', *dyn_menu_patterns), to be used in your URLconf.

I hope this makes sense and is of some help...

THE PRE-EDIT ANSWER:

Based on your short description, I'm not sure what solution would be best for you... But if the permission_required snippet does what you want, just not DRY-ly enough, how about rolling your own wrapper:

def ask_to_login(perm, view):
    return permission_required(perm, login_url='/loginpage/', view)

You could put this anywhere, including in URLconf. Then you could replace all mentions of '/loginpage/' with reference to a variable defined towards the top of your URLs file and you'd have yourself a solution with a single mention of the actual login URL, for one-place-only update of said URL should you have to move it around. :-)

Of course the views would still need to be wrapped explicitly; if that bothers you, you could try to make ask_to_login into a decorator for easy wrapping at the definition site. (But perhaps it's really best not to do it, lest you force yourself to dig your views from under the decorator in case you need them undecorated at some point in the future.)

Michał Marczyk
Thanks, but I was looking for something "DRYer" like http://code.google.com/p/greatlemers-django-tools/, but I do not know if the project is still active.
jbochi
Yeah, I can see what you mean now... I edited my answer to include a sketch of a possible solution, I wonder if that's of any use to you?
Michał Marczyk
+1  A: 

I'm aware this question was asked a couple of weeks ago now, but you mentioned http://code.google.com/p/greatlemers-django-tools/ in one of your comments so I thought I'd chip in.

The project is still active (although it's slightly on the backburner at the moment) but I'm not sure if it is as DRY as you're after. You would still have to specify permissions twice, once in the model object for the menu item and once on the view. This isn't necessarily a bad thing however as the permissions that you define on the menu item may be slightly different from those on the view.

If you wanted to do everything in one place I'd probably suggest a combination of a utility function for use in urls.py that can add restrictions to a view whilst also storing said restriction somewhere for use with a special template tag. I'd imagine it may look something like this.

# Stored in a file named access_check_utils.py say.
from django.conf.urls.defaults import url
from django.core.urlresolvers import get_callable
from django.contrib.auth.decorators import permission_required

access_checked_urls = {}

def access_checked_url(regex, view, kwargs=None, name=None, prefix='', perms=None, login_url=None):
    if perms is None:
        perms = []
    callback = None
    if callable(view):
        callback = view
    elif isinstance(view, basestring):
        if prefix:
            view_path = "%s.%s" % (prefix, view)
        else:
            view_path = view
        try:
            callback = get_callable(view_path)
        except:
            callback = None
    if callback is not None:
        # Add all the permissions
        for perm in perms:
            callback = permission_required(perm, login_url=login_url)(callback)
        if name is not None:
            access_checked_urls[name] = perms
    else:
        callback = view
    return url(regex, callback, kwargs=kwargs, name=name, prefix=prefix)

That should work for the pit needed in urls.py called the same way as you would with a normal url but with the added perms and login_url parameters (perms should be a list of all the relevant ones).

# In a templatetag folder somewhere
from django import template
from django.core.urlresolvers import

# This needs to point to the right place.
from access_check_utils import access_checked_urls

register = template.Library()

@register.inclusion_tag("access_checked_link.html", takes_context=True)
def access_checked_link(context, title, url, *args, **kwargs):
    perms = access_checked_urls.get(url, [])
    if not perms:
       allowed = True
    else:
       allowed = context.request.user.has_perms(perms)
    return { 'allowed': allowed,
             'url': reverse(url, *args, **kwargs),
             'title': title }

This would have an associated template file like:

{% if allowed %}<a href="{{ url }}">{{ title }}</a>{% endif %}

I've not tested this fully, but it should work (or at least be a good basis for something that should work). I'll probably even look to adding something like this into gdt_nav allowing it to check for these base permissions if they exist, and then checking for any extras added.

Hope this is of some help.

--

G

Greatlemer
Thank you so much for your very detailed answer. It's also good to know that the project is still active! I will give it a try :-)
jbochi