Django function based views : 1 contrôleur = 1 fonction

Nous allons voir sur cette page comment écrire des contrôleurs (views) puissants très simplement en Django, sous la forme de fonctions : un contrôleur = une fonction. Tout simplement !

Tout le monde dans un fichier !

Encore un paragraphe où les javaïstes (je n'ai absolument rien contre les javaïstes, au passage… personne n'est parfait ;) vont faire les gros yeux : par défaut, en Django, on écrit tous les contrôleurs (views) d'une application dans un seul fichier. Ce fichier s'appelle views.py.

Pourquoi ? Souvenez-vous : un contrôleur (views), ce n'est qu'un chef d'orchestre ou un agent de faction à une intersection, ce n'est pas lui qui joue et ce n'est pas lui qui écrit le code de la route. Du coup, les contrôleurs (views) en Django sont simples, et courts. Ils peuvent donc être placés dans un fichier tout en gardant une excellente lisibilité (et en plus, on est en Python : retirez 10 kilos d'accolades, de getters et de setters, et vous ne conservez que la substantifique moelle…).

Dans 90% des applications que vous écrirez, cette façon de faire conviendra parfaitement. Pour les 10% restants, les applications où il y a énormément de contrôleurs (views) compliqués, il reste bien entendu possible de splitter le fichier views.py en plusieurs fichiers.

Écriture de nos contrôleurs (views)

Lors de la création de notre application chistera avec la commande djangoadmin.py, Django a placé un fichier views.py vide dans le répertoire de l'application. Nous allons maintenant le compléter.

Pour rappel, notre fichier urls.py définit les routes vers deux contrôleurs :

chistera/urls.py
from django.conf.urls import patterns, url

urlpatterns = patterns('',
    url(r'^dashboard/$', 'chistera.views.dashboard', name='dashboard'),
    url(r'^backlog/(?P<backlog_id>[0-9]+)/$', 'chistera.views.backlog', name='backlog'),
)

Nous devons donc définir les deux destinations de ces routes :

  • un contrôleur (view) dashboard ;
  • un contrôleur (view) backlog.

Import des modèles de l'application

Bien entendu, nos contrôleurs (views) vont avoir besoin de nos modèles : nous allons commencer par les importer dans le fichier views.py.

chistera/views.py
from chistera.models import *

Écriture du contrôleur (vue) dashboard

OK, nous sommes maintenant prêts pour écrire un premier contrôleur (view). Notre contrôleur (view) dashboard doit récupérer les équipes et les backlogs pour pouvoir invoquer ensuite une vue (template) qui les affichera à l'écran. Voici sa déclaration :

chistera/views.py
def dashboard(request):
    backlogs = ProductBacklog.objects.all()
    teams = Team.objects.all()
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})

Nous avons déclaré une simple fonction dashboard(), qui reçoit automatiquement un paramètre request. Ce paramètre est fourni par Django au moment de l'appel de la fonction en provenance du contrôleur frontal, et comprend tout ce dont on peut avoir besoin concernant la requête de l'utilisateur (les paramètres get, post, etc.). Pour le coup, nous n'en avons pas besoin.

Les deux premières lignes sont tellement simples que les expliquer serait presque une insulte à votre intelligence… L'ORM de Django a une syntaxe limpide : comme nous l'avons déjà vu par ailleurs, chacun de nos modèles dispose automatiquement d'un manager nommé objects. Ce manager nous permet de réaliser toutes les opérations courantes sur les modèles, et notamment de récupérer l'ensemble des enregistrement via la méthode all(). Le retour de cette méthode est un objet de type QuerySet, qui est en fait une liste doté de méthodes supplémentaires et fournies par l'ORM de Django.

Nos variables backlogs et teams reçoivent donc des listes d'objets, que l'on pourra utiliser ensuite.

En Django, tous les contrôleurs (views) doivent retourner un objet de type HttpResponse. Ce sont ces HttpResponse qui sont transmises aux templates.

La dernière ligne de la fonction permet de créer l'objet HttpResponse à partir de différents éléments :

  • le chemin du template à utiliser (la vue en langage MVC courant) ;
  • un dictionnaire contenant les variables que nous voulons ajouter au contexte, c'est à dire à l'ensemble des choses qui seront fournies au template.

Et c'est tout ! Notre premier contrôleur est en place. Si vous tentez d'afficher la page à l'URL http://127.0.0.1:8000/dashboard/, vous risquez néanmoins d'avoir des petits soucis dus au fait que nous utilisons dans notre contrôleur des fonctions définies dans des modules que nous n'avons pas importés (render_to_response). Fixons ce problème, et voici notre fichier views.py fonctionnel :

chistera/views.py
from django.shortcuts import render_to_response

from chistera.models import *

def dashboard(request):
    backlogs = ProductBacklog.objects.all()
    teams = Team.objects.all()
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})

Relançons à présent nos tests unitaires…

$ python manage.py test chistera
Creating test database for alias 'default'...

...

======================================================================
ERROR: test_dashboard_authenticated_user (chistera.tests.ChisteraTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/pascal/django/miage_scrum/chistera/tests.py", line 40, in test_dashboard_authenticated_user
    response = self.client.get(reverse('chistera:dashboard'))
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 453, in get
    response = super(Client, self).get(path, data=data, **extra)
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 279, in get
    return self.request(**r)
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 424, in request
    six.reraise(*exc_info)
  File "/Library/Python/2.7/site-packages/django/core/handlers/base.py", line 115, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
  File "/Users/pascal/django/miage_scrum/chistera/views.py", line 10, in dashboard
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})
  File "/Library/Python/2.7/site-packages/django/shortcuts/__init__.py", line 29, in render_to_response
    return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 170, in render_to_string
    t = get_template(template_name)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 146, in get_template
    template, origin = find_template(template_name)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 139, in find_template
    raise TemplateDoesNotExist(name)
TemplateDoesNotExist: chistera/dashboard.html

======================================================================
ERROR: test_dashboard_not_authenticated_user (chistera.tests.ChisteraTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/pascal/django/miage_scrum/chistera/tests.py", line 34, in test_dashboard_not_authenticated_user
    response = self.client.get(url)
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 453, in get
    response = super(Client, self).get(path, data=data, **extra)
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 279, in get
    return self.request(**r)
  File "/Library/Python/2.7/site-packages/django/test/client.py", line 424, in request
    six.reraise(*exc_info)
  File "/Library/Python/2.7/site-packages/django/core/handlers/base.py", line 115, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
  File "/Users/pascal/django/miage_scrum/chistera/views.py", line 10, in dashboard
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})
  File "/Library/Python/2.7/site-packages/django/shortcuts/__init__.py", line 29, in render_to_response
    return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 170, in render_to_string
    t = get_template(template_name)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 146, in get_template
    template, origin = find_template(template_name)
  File "/Library/Python/2.7/site-packages/django/template/loader.py", line 139, in find_template
    raise TemplateDoesNotExist(name)
TemplateDoesNotExist: chistera/dashboard.html

----------------------------------------------------------------------
Ran 4 tests in 1.585s

FAILED (errors=4)
Destroying test database for alias 'default'...

Il y a du progrès : Django ne se plaint plus du fait que nos contrôleurs (views) n'existent pas. Normal, nous les avons créés. Il y a encore un soucis : les templates mentionnés dans nos tests n'existent toujours pas.

Ceci se confirme quand on essaie de charger la page dans un navigateur :

Django template does not exist

Pour éviter cette erreur, créons simplement un répertoire templates dans le dossier de notre projet, dans lequel nous créons un sous-répertoire chistera destiné à accueillir tous les templates de l'application. Dans ce répertoire, créons deux fichiers vides (pour le moment) : dashboard.html et backlog.html.

miage_scrum
|--- manage.py
|--- miage_scrum
|    |--- __init__.py
|    |--- settings.py
|    |--- urls.py
|    |--- wsgi.py
|--- templates
     |--- chistera
          |--- dashboard.html
          |--- backlog.html

En relançant à nouveau les tests, nous obtenons à présent des résultats intéressants :

$ python manage.py test chistera
Creating test database for alias 'default'...

...

======================================================================
FAIL: test_dashboard_not_authenticated_user (chistera.tests.ChisteraTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/pascallando/Sites/django/miage_scrum/chistera/tests.py", line 35, in test_dashboard_not_authenticated_user
    self.assertTemplateNotUsed(response, 'chistera/dashboard.html')
  File "/Library/Python/2.7/site-packages/django/test/testcases.py", line 802, in assertTemplateNotUsed
    " the response" % template_name)
AssertionError: Template 'chistera/dashboard.html' was used unexpectedly in rendering the response

----------------------------------------------------------------------
Ran 4 tests in 0.801s

FAILED (failures=3)
Destroying test database for alias 'default'...

OK, plus de soucis de template inexistant à présent, c'est assez épuré, mais ça fonctionne :

Django template vide

Notre test test_dashboard_not_authenticated_user échoue tout de même : un utilisateur non authentifié essayant d'accéder au dashboard… y parvient ! C'est un problème.

Pour régler ce problème, nous allons simplement mettre en œuvre le décorateur login_required fourni par Django, sur notre contrôleur (view) dashboard :

chistera/views.py
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required

from chistera.models import *

@login_required
def dashboard(request):
    backlogs = ProductBacklog.objects.all()
    teams = Team.objects.all()
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})

Notre contrôleur est à présent « protégé » par le décorateur @login_required : seules les requêtes provenant d'utilisateurs authentifiées pourront aboutir. Très simple et très efficace.

Écriture du contrôleur (vue) backlog

De manière analogue à ce que nous avons fait pour le contrôleur (view) dashboard, nous allons écrire, dans le fichier views.py, la fonction contrôleur (view) backlog.

chistera/views.py
@login_required
def backlog(request, backlog_id):
    backlog = get_object_or_404(ProductBacklog, pk=backlog_id)
    stories = UserStory.objects.filter(product_backlog=backlog)
    return render_to_response('chistera/backlog.html', {'backlog': backlog, 'stories': stories})

Rien de bien nouveau ici, si ce n'est que nous souhaitons récupérer un backlog donné, repéré par un identifiant. Souvenez-vous, dans le contrôleur frontal, nous avions défini la route comme :

chistera/urls.py
url(r'^backlog/(?P<backlog_id>[0-9]+)/$', 'chistera.views.backlog', name='backlog')

Nous avons donc une variable nommée backlog_id, qui est automatiquement passée au contrôleur (view) backlog par Django : ce qui explique que notre fonction backlog() admet un argument supplémentaire backlog_id par rapport à la fonction dashboard().

Autre remarque : nous utilisons ici une fonction pratique (« shortcut », une sorte de raccourcis…) nommée get_object_or_404 : cette fonction tente de récupérer un enregistrement sur la base d'une contrainte (ici sur la clé primaire : pk=backlog_id) et retourne un code 404 si l'objet est introuvable. Pratique !

Voici le code complet de notre fichier views.py :

chistera/views.py
from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.auth.decorators import login_required

from chistera.models import *

@login_required
def dashboard(request):
    backlogs = ProductBacklog.objects.all()
    teams = Team.objects.all()
    return render_to_response('chistera/dashboard.html', {'backlogs': backlogs, 'teams': teams})

@login_required
def backlog(request, backlog_id):
    backlog = get_object_or_404(ProductBacklog, pk=backlog_id)
    stories = UserStory.objects.filter(product_backlog=backlog)
    return render_to_response('chistera/backlog.html', {'backlog': backlog, 'stories': stories})

Conclusion sur les contrôleurs

Nous avons vu dans ce tutoriel assez dense comment créer des contrôleurs (views) avec Django, en utilisant les function-based views. Nos deux contrôleurs sont regroupés dans un seul fichier views.py, et sont très courts (seulement 3 lignes par contrôleur !), faisant appel à des fonctions « shortcut », des helpers de Django ainsi qu'à des décorateurs fournis par le framework.

Nous allons voir dans le prochain tutoriel comment créer les templates (autrement dit, les « vues » au sens MVC) pour afficher des choses !

Testez vos connaissances

Qu'est-ce que @login_required ?
  • Un décorateur
  • Une manière de spécifier à Django que le contrôleur qui suit n'est pas protégé par mot de passe.
  • Une fonction définie par Django.
  • Un serveur de messagerie SMTP.
get_object_or_404(Sprint, pk=sprint_id) est équivalent à…
  • Ce code :
    try:
        sprint = Sprint.objects.get(pk=sprint_id)
    except Sprint.DoesNotExist:
        raise Http404
  • Ce code :
    sprint = Sprint.objects.get(pk=sprint_id)
        print '404 error'
  • Ce code :
    get_object_or_404(Sprint, id=sprint_id)
  • Ce code :
    sprint = Sprint.objects.filter(pk=sprint_id)
  • Ce code :
    sprint = Sprint.objects.filter(pk=sprint_id)
    if not sprint:
        raise Http404
                
  • Rien
Qu'est-ce qu'un contexte ?
  • C'est un ensemble de contrôleurs et de vues.
  • C'est un ensemble de variables/valeurs alimenté par un contrôleur (view) et disponibles dans une vue (template).
  • C'est la manière par laquelle les templates peuvent afficher des valeurs assignées à des variables dans les contrôleurs.
  • C'est un objet qui permet de générer une vue.
En Django, une view est l'équivalent MVC de…
  • Une vue
  • Un modèle
  • Un contrôleur
  • Un contrôleur frontal
  • Un méta-modèle
Que permet de réaliser render_to_response() ?
  • Génération d'un modèle automatiquement dans le cadre du module de scaffolding
  • Rendu d'une vue (template) en utilisant un contexte spécifié à l'aide d'un dictionnaire, et retour d'une chaîne de caractère.
  • Rendu d'une vue (template) en utilisant un contexte spécifié à l'aide d'un dictionnaire, et retour d'un objet de type HttpResponse.
  • Rendu d'une vue (template) en utilisant un contexte spécifié à l'aide d'un dictionnaire, et ne retourne rien.
  • C'est un no-op : une fonction qui ne fait rien.