Wednesday, March 30, 2011

Django Pattern For Model Permissions

I think I've found an interesting way to set up a permissions system in Django.  This system allows you to specify granular permissions on specific object instances and is template friendly.

Setup The Permissions Infrastructure

First, you create an "abstract" Permissions class that models will use as a permissions base class.  This base permissions class sets up simple caching for the permissions and creates some common methods to be used by all permissions objects.  The current_user represents the logged in that you want to check permissions for a particular object.

Notice the use of threadlocals here.  If current_user is not supplied in the constructor, the class will try to lookup the current_user from a variable by the threadlocals middleware (see below).  Be warned, however, some people don't seem to like the threadlocals approach.  To be honest, I'm still trying to evaluate the pluses and minuses, but it sure makes certain things in the templates easier.  Comments are welcome.

core/permissions.py

from core.middleware import threadlocals                        
                                                                       
class Permissions(object):

    def __init__(self, obj, current_user=False):
        if not current_user:
            current_user=threadlocals.get_current_user()
        if not current_user:
            current_user = None
        self.current_user = current_user
        self.obj = obj

        self.cache = {}
            
    def clear_cache(self, key=None):
        if key:
            try:
               del self.cache[key]
            except KeyError:
               pass
        else:
            self.cache.clear()

    def get_current_user(self):
        return self.current_user

    def __unicode__(self):
        return "Permissions for: %s" % (self.obj)

The thredlocals middleware stores the current user in the local thread.   You need to include this middleware in my settings.py middleware settings.

core/middleware/threadlocals.py - Reference

import threading

_thread_locals = threading.local()

def get_current_user():
    return getattr(_thread_locals, 'user', None)

class ThreadLocals(object):
    """Middleware that gets various objects from the
    request object and saves them in thread local storage."""
    def process_request(self, request):
        _thread_locals.user = getattr(request, 'user', None)

Now you can create a Permission subclass within my model class.  This Permissions model inherits the abstract Permissions class that we defined earlier.   Now, you can create simple permissions methods within my Permissions child class.  For example, I've created is_creator() and can_edit() methods in this class.

Finally, inside of MyModel class, you create a permissions() method that instantiates a permissions instance and returns that instance.

myapp/models.py

from core import permissions 

class MyModel(models.Model):

    creator = models.ForeignKey('auth.User')
    field1 = models...
    field2 = models...  # define Django Model fields here 

    class Permissions(permissions.Permissions):

        def can_edit(self):
            KEY='cache_can_edit'
            if self.cache.has_key(KEY):
                return self.cache[KEY]

            if self.is_creator():
                return_value = True
            else:
                return_value = False

            self.cache.update({KEY:return_value})
            return return_value


        def is_creator(self):
            KEY="cache_is_creator"
            if self.cache.has_key(KEY):
                return self.cache[KEY]
            if self.obj.creator == self.current_user:
                return_value = True
            else:
                return_value = False
            self.cache.update({KEY:return_value})
            return return_value


    def permissions(self, current_user=None):
         return self.Permissions(self, current_user)


Using The Permissions

You can use the permissions system in a view as exemplified by the following code.  Because the abstract Permissions class contains some basic caching, multiple calls to the same permissions function will not hit the database multiple times.

myapp/views.py

from myapp import models.py

@login_required
def index(request, .... ):
    current_user = request.user 
    
    mymodel = MyModel.objects.get_mymodel_instance(.....)  

    mymodel_permissions = mymodel.permissions(current_user=current_user)

    if mymodel_permissions.can_edit():
        do_something_special()

    ....


Now what's really cool is how you can make use of this in templates.  Because of the threadlocals, you simply can make calls the my Permission instance methods directly in the template without using a templatetag to pass in the current user -- because of the framework that we setup above, the current user will be assumed automatically.   Using the "with" statement, you can instantiate the permissions object only once so you can take advantage of the caching in the template.

templates/myapp/index.html
{% with mymodel.permissions as permissions %}
   <div>{% if permissions.can_edit %}<a href="">edit</a>{% endif %}</div>
{% endwith %}

You can still unittest this pattern easily by creating your tests similar to the way you create your views; you can simply pass in the current_user to the mymodel.permissions() function.

I hope this pattern is useful to someone.  I'm sure there are better approaches.  I'm glad to hear your comments.
Thanks!
Joe

Tuesday, March 22, 2011

Django Environment Quick Setup

aptitude install python-virtualenv  # for debian-based systems
virtualenv --no-site-packages myproject
cd myproject/
source ./bin/activate
pip install django  south  django-extensions
pip install flup  psycopg2
mkdir -p ./etc/ ./var/log/ ./app/django/
cd ./app/django/
django-admin.py startproject mymain

Django Project Setup Conveniences

Whenever I start a new Django project, there are several common steps that I to help standardize my setups.

In settings.py...

I define a PROJECT_ROOT variable which contains the path to the project directory. 

This is useful for various settings in the setup.py:
import sys, os
PROJECT_ROOT = os.path.dirname(__file__)

I set my MEDIA_ROOT to be a path relative to the PROJECT_ROOT.  
Of course, I will need to create this directory on the filesystem. 
MEDIA_ROOT = os.path.join(PROJECT_ROOT, '..','htdocs','media').replace('\\','/') + '/'

And I set a relative TEMPLATE_DIRS path.  I will need to make this directory as well.
TEMPLATE_DIRS = (
    os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'),
)

Sometimes I add extra directories to the path so I can store specific packages within the project directory. (I might use this if I want to keep all related modules together).
sys.path.insert(0, os.path.join(PROJECT_DIR, "contrib"))
sys.path.insert(0, os.path.join(PROJECT_DIR, "src"))

I generally like to put admin media in the following location:
ADMIN_MEDIA_PREFIX = '/media/admin/'

Almost all the time, I install the following modules in INSTALLED_APPS:
'south', # django-south
'django_extensions', # django-command-extensions

In urls.py...


If I want to use the Django debug server, I'll set this near the top:
from django.conf import settings
urlpatterns = patterns('',)
if settings.DEBUG:
    urlpatterns = patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve',
                   {'document_root': settings.MEDIA_ROOT}),
    )


Introduction

This blog will document my adventures in Django, the Python web framework.  It is a direct fork of my Linux Info blog, but will be specifically utilized for documenting my Django activity.  Though my aim is to post Django code and examples that are helpful, there will most certainly be better/different ways to approach various challenges.  Any comments are much appreciated.
Joe