Django function based views : 1 contrôleur = 1 fonction
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.pyfrom 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
.
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.pydef 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 :
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 :
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 :
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
:
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
.
@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.pyurl(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
:
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
@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
- 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.
- Une vue
- Un modèle
- Un contrôleur
- Un contrôleur frontal
- Un méta-modèle
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.
Pour plus de détails, voyez la documentation officielle, section render_to_response.