Archive pour le mot-clef ‘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!

Les coulisses de Ma Petite Auto Entreprise

Samedi 19 février 2011

Comme je l’avais annoncé, cet article, qui fait référence au lancement de la bêta, va plus traiter de la genèse de Ma Petite Auto Entreprise et de l’aspect technique.

Naissance de l’application

Dès que j’ai commencé mon activité d’auto-entrepreneur, en avril 2010, j’ai voulu géré rigoureusement mon activité. J’ai commencé avec une feuille de calcul sous Open Office et des modèles de document éditables pour les contrats. Les informations qui m’importaient étaient mon chiffre d’affaire, les charges à provisionner, les factures à éditer ou en attente de paiement, le reste à faire avant d’atteindre le plafond, etc … cela a bien marché quelques temps, mais j’ai vite été limité par la simplicité du système. En tant qu’informaticien, c’était aussi simple pour moi de réaliser rapidement un prototype d’application web en Django pour faire la même chose et surtout faire bien plus.

La première raison est donc le besoin personnel, mais j’ai essayé de garder en tête que l’application pourrait servir à d’autres. Au fil des semaines, en parallèle de mes missions d’expertise technique, j’ai continué à faire évoluer le logiciel avec comme objectif de le publier. La deuxième raison qui m’a poussé à continuer est que cette application est, je l’espère, une belle démonstration de mes compétences (et une promotion pour python et Django, malheureusement bien trop rare en entreprise). En effet, on me demande souvent de montrer des références, et ayant principalement travaillé sur des intranets, c’est un peu compliqué … Finalement, la dernière raison est évidemment l’espoir d’en tirer un bénéfice financier et ainsi pouvoir concentrer une majeure partie de mon temps à l’édition de ce logiciel.

Aspect technique

Ma Petite Auto Entreprise est en fait le nom du site qui héberge l’application. L’application en elle-même se nomme « aemanager ». Elle est libre et placée sous licence GPLv3. Les sources sont disponibles sur Github.

Aemanager est développé à l’aide de Django, framework web écrit en python vraiment très pratique et jQuery, framework javascript. Les données sont stockées dans une base PostgreSQL. Les sources sont hébergées à l’aide de Git, gestionnaire de version décentralisé (DVCS) initié par Linus Torvald. A l’heure où j’écris cet article il y a plus de 150 tests unitaires pour s’assurer du bon fonctionnement de l’application et elle a été testée sous Chrome, Firefox et IE. Les évolutions effectuées tout au long du développement nécessitent parfois de casser le modèle de données, c’est pourquoi des migrations sont réalisées à l’aide de South, une extension Django dédiée à cela. Voila pour les grandes lignes.

Je peux également ajouter que pour la génération PDF j’utilise Reportlab, pour les graphiques un plugin jQuery : Flot, Elrte pour l’éditeur wysiwyg des devis et contrats et django-registration pour la création de compte utilisateur.

Même si l’application est a priori spécifique à la France, elle est internationalisée car j’ai pris l’habitude de toujours coder en anglais. Concernant les paiements, aemanager est interfacé avec Paypal. Et pour conclure, le tout est hébergé sur un serveur virtualisé en SSL (avec un certificat valide !).

Le tout a déjà représenté quelques semaines de boulot à plein temps et probablement beaucoup d’autres à venir ;-)