Archive pour la catégorie ‘django’

Decorators in Django

Jeudi 22 août 2013

Probably like a lot of you, I’ve googled or checked Django’s code to know how to write my own decorators. And every time I was a bit confused, just copy/pasting the examples and changing the code until it works.

While working on a decorator which was basically an aggregation of other decorators, I’ve finally had to get a deeper understanding, so I’ll try to make it clear for you.

Decorators are functions

First, we’ll forget the annotation notation with the @ to understand them better. So when you do:

@login_required
def my_view(request):
    return HttpResponse()

you could do:

def my_view(request):
    return HttpResponse()

my_view = login_required(my_view)

Another example with parameters (you’ll see it makes a big difference):

@require_http_method(['GET', 'POST'])
def my_view(request):
    return HttpResponse()

is equivalent to:

def my_view(request):
    return HttpResponse()
my_view = require_http_method(['GET', 'POST'])(my_view)

Decorators with no parameters

As we’ve seen, a decorator is just a function taking another function as a parameter and replacing it. So when I’ll call my_view(request), I’ll actually call login_required(my_view)(request).

Remember that a decorator is a design pattern, not a Python or Django specificity. So without googling anything, you could already write your decorator from what you know in Python. We’ll go step by step. First we’ll define an identity decorator which just do nothing more than the view:

def identity(a_view):
    return a_view

in that case, identity(my_view) will return my_view, so my_view(request) will work.

Now, how do we really do something? we can start by just wrapping our view in another one to log something for instance:

def log(a_view):
    def _wrapped_view(request, *args, **kwargs) :
        logger.log('My view is called')
        return a_view(request, *args, **kwargs)
    return _wrapped_view

Notice here, we define a view on the fly which just executes our initial view and logs something. log(my_view) returns _wrapped_view so log(my_view)(request) will execute _wrapped_view(request) which will in fact call my_view(request) in the end.

With that example, we can check if the user is logged in in our wrapped view:

def login_required(a_view):
    def _wrapped_view(request, *args, **kwargs):
        if request.user.is_authenticated():
            return a_view(request, *args, **kwargs)
        return HttpResponseForbiden()
    return _wrapped_view

it should become pretty straightforward: login_required(my_view) return _wrapped_view, so you will really execute _wrapped_view(request)  which will in turn execute your original view if the user is logged in.

Decorators with parameters

Let’s add a parameter to the login_required decorator. We will redirect to login_url, and for now, this parameter will be mandatory, you’ll see why after:

def login_required(login_url)
    def decorator(a_view):
        def _wrapped_view(request, *args, **kwargs):
            if request.user.is_authenticated():
                return a_view(request, *args, **kwargs)
            return HttpResponseRedirect(login_url)
        return _wrapped_view
    return decorator
Oh wait, now we have 3 nested levels? Let's go back to what I've said in the first paragraph:
@login_required('/login/')
def my_view(request):
    ...

is equivalent to

my_view = login_required('/login/')(my_view)

so, login_required(‘/login/’) returning the inner decorator function, my_view becomes decorator(my_view) which is _wrapped_view, what we had before. Makes sense?
We’ve just added a function wrapping everything to return the decorator depending on the parameters.

Optional parameters

Hopefully, you’ve got the main idea. What happens if login_url is optional:

def login_required(login_url=None)
    def decorator(a_view):
        def _wrapped_view(request, *args, **kwargs):
            if request.user.is_authenticated():
                return a_view(request, *args, **kwargs)
            return HttpResponseRedirect(login_url or '/login/')
        return _wrapped_view
    return decorator

I will be able to use:

@login_required
def my_view(request):
    ...

but as we’ve seen, this will result in executing login_required(my_view)(request) which will be decorator(request) aka _wrapped_view. You will get a callable instead of a HttpResponse!

The trick here is to return the decorator or the decorated function depending on the case:

def login_required(func=None, login_url=None)
    def decorator(a_view):
        def _wrapped_view(request, *args, **kwargs):
            if request.user.is_authenticated():
                return a_view(request, *args, **kwargs)
            return HttpResponseRedirect(login_url or '/login/')
        return _wrapped_view

    if func:
        return decorator(func)
    return decorator

In that case, I call it without parameter, it works: login_required(my_view)(request) returns decorator(my_view)(request) aka _wrapped_view(request), which is a HttpResponse.

If I give a parameter: login_required(login_url=’/login/’)(my_view)(request) == decorator(my_view)(request) == _wrapped_view(request) which gives me my HttpResponse.

Just note that you need to name your decorator parameters in that case.

Last details

It’s pretty much it. Last problem you’ll need to fix is that:

>>> def decorator(func):
...   def _wrapped(request):
...     return func(request)
...   return _wrapped
...
>>> @decorator
... def my_view(request):
...   pass
...
>>> my_view.__name__
'_wrapped'
your decorated view has the wrong name (and wrong doc among other things). I won’t detail but to fix that, use Python/Django wraps decorator and functions:
>>> from functools import wraps
>>> from django.utils.decorators import available_attrs
>>> def decorator(func):
...   @wraps(func, assigned=available_attrs(func))
...   def _wrapped(request):
...     return func(request)
...   return _wrapped
...
>>> @decorator
... def my_view(request):
...   pass
...
>>> my_view.__name__
'my_view'
To simplify, it’s doing _wrapped.__name__ = func.__name__, _wrapped.__doc__ = func.__doc__, etc.
That’s it, it’s time to decorate!

Debugging an exception raised by Django

Dimanche 3 avril 2011

I know for a while how to trace code with pdb, but I used to use the famous

import pdb;pdb.set_trace()

which enable debugging when this line is reached.

However, I’ve had an exception raised by django when saving an object :

File "/opt/virtualenvs/django1.2/lib/python2.6/site-packages/Django-1.2.5...
    return int(value)
ValueError: invalid literal for int() with base 10: ''
and it’s totally anoying to trace the save() function step by step. After looking at pdb doc, I’ve found what I was looking for : the post_mortem() method :
try:
    object.save()
except:
    import pdb;pdb.post_mortem()
this runs pdb where the exception is raised, and then allows me to see which is that field on my model which is not properly set.

Serving static files with Apache while controlling access with Django

Dimanche 13 mars 2011

Theses days, I’ve added the ability to upload files in Aemanager for Ma Petite Auto Entreprise. Since a user may only access files he owns, a solution could have been putting them into an obfuscated directory (with a uid) in the media directory but I didn’t want to rely on such a solution which I consider not enough secure. I could have then let Apache serve theses files.

An other way to achieve this goal could be putting files in a directory not accessible by http, then in a view, read the file and stream the content with Django. It matches my goal but it isn’t efficient nor recommended to serve static files through Django.

The last solution, the better, is to use mod_xsendfile, a module for Apache. The user ask for a file represented by an arbitrary url and Django just sends a response without content, setting X-Sendfile header to tell Apache where it should read the content.

First, you need to install mod_xsendfile following these instructions :

$ sudo apxs2 -cia mod_xsendfile.c

Restart Apache when finished.

Here is a view of Aemanager which provide protected static files:

@settings_required
@subscription_required
def contract_uploaded_contract_download(request, id):
    contract = get_object_or_404(Contract, pk=id, owner=request.user)
    response = HttpResponse(mimetype='application/force-download')
    response['Content-Disposition'] = 'attachment;filename="%s"'\
                                    % smart_str(contract.contract_file.name)
    response["X-Sendfile"] = "%s%s" % (settings.FILE_UPLOAD_DIR, 
                                       contract.contract_file.name)
    response['Content-length'] = contract.contract_file.size

    return response

Here the important thing is the X-Sendfile header.

However, there’s also a problem with files which have non-ascii characters in their name. The best solution I’ve found is to use:

unicodedata.normalize('NFKD', filename).encode('ascii', 'ignore')

This replaces characters such as « é » by their ascii equivalent, « e » in this case.

Here is my model to let you see how it’s done :

store = FileSystemStorage(location=settings.FILE_UPLOAD_DIR)

def contract_upload_to_handler(instance, filename):
    return "%s/contract/%s" % (instance.owner.username,
                               unicodedata.normalize('NFKD',
                                                     filename).encode('ascii',
                                                                      'ignore'))

class Contract(OwnedObject):
    customer = models.ForeignKey(Contact,
                                 verbose_name=_('Customer'),
                                 related_name="contracts")
    title = models.CharField(max_length=255,
                             verbose_name=_('Title'))
    contract_file = models.FileField(upload_to=contract_upload_to_handler,
                                     null=True,
                                     blank=True,
                                     storage=store,
                                     verbose_name=_('Uploaded contract'))
    content = models.TextField(verbose_name=_('Content'),
                               null=True,
                               blank=True)
    update_date = models.DateField(verbose_name=_('Update date'),
                                   help_text=_('format: mm/dd/yyyy'))

EDIT : The Xsendfile module also exists for nginx and charset issue in filename can be solved with header X-Accel-Charset: utf-8

Django-registration : Why my activation e-mail is not translated ?

Mercredi 16 février 2011

That was the question a few minutes ago. Just don’t forget that makemessages only looks for .html templates so tell it to look for .txt files too :

./manage.py makemessages -a -e html,txt

Run Django tests automatically before committing on Git

Mercredi 16 février 2011

Since I often forget to run unit tests before committing, I spend a lot of time looking for the bad commit when I find regressions 3 commits later.

However it’s really easy to automatically run tests before each commit. In .git/hooks/pre-commit, put :

python manage.py test
exit $?

then chmod 755 this file and it’s done. I really love git :-)

Do not forget to source your virtualenv before committing.

Note that tests are run on your working tree and not the commit itself, so if you commit only a part of your working tree, it may fail while your commit passes tests.