Écriture de votre première application Django, 3ème partie

Ce tutoriel commence là où le tutoriel 2 s’achève. Nous continuons l’application de sondage Web et allons nous focaliser sur la création de l’interface publique – les « vues ».

Philosophie

Une vue est un « type » de page Web dans votre application Django qui sert généralement à une fonction précise et possède un gabarit spécifique. Par exemple, dans une application de blog, vous pouvez avoir les vues suivantes :

  • La page d’accueil du blog – affiche quelques-uns des derniers billets.

  • La page de « détail » d’un billet – lien permanent vers un seul billet.

  • La page d’archives pour une année – affiche tous les mois contenant des billets pour une année donnée.

  • La page d’archives pour un mois – affiche tous les jours contenant des billets pour un mois donné.

  • La page d’archives pour un jour – affiche tous les billets pour un jour donné.

  • Action de commentaire – gère l’écriture de commentaires sur un billet donné.

Dans notre application de sondage, nous aurons les quatre vues suivantes :

  • La page de sommaire des sondages – affiche quelques-uns des derniers sondages.

  • La page de détail d’un sondage – affiche la question d’un sondage, sans les résultats mais avec un formulaire pour voter.

  • La page des résultats d’un sondage – affiche les résultats d’un sondage particulier.

  • Action de vote – gère le vote pour un choix particulier dans un sondage précis.

Dans Django, les pages Web et les autres contenus sont générés par des vues. Chaque vue est représentée par une simple fonction Python (ou une méthode dans le cas des vues basées sur des classes). Django choisit une vue en examinant l’URL demandée (pour être précis, la partie de l’URL après le nom de domaine).

Dans votre expérience sur le Web, vous avez certainement rencontré des perles comme par exemple « ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B ». Vous serez certainement rassuré en sachant que Django permet des styles d’URL bien plus élégants que cela.

Un modèle d’URL est simplement la forme générale d’une URL ; par exemple : /archive/<année>/<mois>/.

Pour passer de l’URL à la vue, Django utilise ce qu’on appelle des « URLconf ». Un URLconf associe des modèles d’URL (définis par des expressions régulières) à des vues.

Ce tutoriel fournit des instructions de base sur l’utilisation des URLconf, et vous pouvez consultez django.core.urlresolvers pour plus de détails.

Écriture d’une première vue

Écrivons la première vue. Ouvrez le fichier polls/views.py et placez-y le code Python suivant :

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the poll index.")

C’est la vue Django la plus simple possible. Pour appeler cette vue, il s’agit de l’associer à une URL, et pour cela nous avons besoin d’un URLconf.

Pour créer un URLconf dans le répertoire polls, créez un fichier nommé urls.py. Votre répertoire d’application devrait maintenant ressembler à ceci :

polls/
    __init__.py
    admin.py
    models.py
    tests.py
    urls.py
    views.py

Dans le fichier polls/urls.py, insérez le code suivant :

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index')
)

L’étape suivante est de faire pointer l’URLconf racine vers le module polls.urls. Dans mysite/urls.py, insérez un appel include(), ce qui donnera :

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^polls/', include('polls.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

Vous avez maintenant relié une vue index dans l’URLconf. Ouvrez http://localhost:8000/polls/ dans votre navigateur et vous devriez voir le texte “Hello, world. You’re at the poll index.” qui a été défini dans la vue index.

La fonction url() reçoit quatre paramètres, dont deux sont obligatoires : regex et view, et deux facultatifs : kwargs et name. À ce stade, il est interessant d’examiner le rôle de chacun de ces paramètres.

Paramètre d’url() : regex

Le terme « regex » est communément utilisé comme raccourci pour « expression régulière », qui consiste en une syntaxe pour retrouver des motifs dans des chaînes de caractères, ou dans ce cas, dans des modèles d’URL. Django commence par la première expression régulière puis continue de parcourir la liste en comparant l’URL reçue avec chaque expression jusqu’à ce qu’il en trouve une qui correspond.

Notez que ces expressions régulières ne cherchent pas dans les paramètres GET et POST, ni dans le nom de domaine. Par exemple, dans une requête vers http://www.example.com/myapp/, l’URLconf va chercher myapp/. Dans une requête vers http://www.example.com/myapp/?page=3, l’URLconf va aussi chercher myapp/.

Si vous avez besoin d’aide avec les expressions régulières, jetez un oeil à l’article Wikipedia et à la documentation du module Python re. À noter aussi que le livre « Mastering Regular Expressions », écrit par Jeffrey Friedl, est remarquable. En pratique, il n’y a cependant pas besoin d’être un expert des expressions régulières, car il suffit de savoir capturer des motifs simples. En fait, les expressions complexes risquent de pénaliser les performances, et il faut plutôt éviter de faire appel à tout le potentiel des expressions régulières.

Enfin, une note sur la performance : ces expressions régulières sont compilées la première fois que le module contenant l’URLconf est chargé. Elles sont extrêmement rapides (tant qu’elles ne sont pas trop complexes, comme indiqué ci-dessus).

Paramètre d’url() : view

Lorsque Django trouve une expression régulière qui correspond, il appelle la fonction de vue spécifiée, avec un objet HttpRequest comme premier paramètre et toutes valeurs « capturées » par l’expression régulière comme autres paramètres. Si l’expression régulière utilise des captures simples, les valeurs sont passées comme paramètres positionnels ; si ce sont des captures nommées, les valeurs sont passées comme paramètres nommés. Nous montrerons cela par un exemple un peu plus loin.

Paramètre d’url() : kwargs

Des paramètres nommés arbitraires peuvent être transmis dans un dictionnaire vers la vue cible. Nous n’allons pas exploiter cette fonctionnalité dans ce tutoriel.

Paramètre d’url() : name

Le nommage des URL permet de les référencer de manière non ambiguë depuis d’autres portions de code Django, en particulier dans les gabarits. Cette fonctionnalité puissante permet d’effectuer des changements globaux dans les modèles d’URL de votre projet en ne modifiant qu’un seul fichier.

Écriture de vues supplémentaires

Ajoutons maintenant quelques vues supplémentaires dans polls/views.py. Ces vues sont légèrement différentes, car elles acceptent un paramètre :

def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

def results(request, poll_id):
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

def vote(request, poll_id):
    return HttpResponse("You're voting on poll %s." % poll_id)

Liez ces nouvelles vues avec leurs URL dans le module polls.urls en ajoutant les appels url() suivants :

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<poll_id>\d+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<poll_id>\d+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<poll_id>\d+)/vote/$', views.vote, name='vote'),
)

Ouvrez votre navigateur à l’adresse « /polls/34/ ». La méthode detail() sera exécutée et affichera l’ID fourni dans l’URL. Essayez aussi « /polls/34/results/ » et « /polls/34/vote/ », elles afficheront les pages modèles de résultats et de votes.

Lorsque quelqu’un demande une page de votre site Web, par exemple « /polls/34/ », Django charge le module Python mysite.urls parce qu’il est mentionné dans le réglage ROOT_URLCONF. Il trouve la variable nommée urlpatterns et parcourt dans l’ordre les expressions régulières. Les fonctions include() utilisées ne font que référencer d’autres URLconf. Remarquez que les expressions dans les fonctions include() ne sont pas terminées par $ (marque de fin de chaîne), mais plutôt par une barre oblique. À chaque occurrence de include(), Django retire de l’URL la partie correspondant à l’expression régulière et passe le reste à l’URLconf indiqué pour la suite du traitement.

L’idée derrière include() est de faciliter la connexion d’URL. Comme l’application de sondages possède son propre URLconf (polls/urls.py), ses URL peuvent être injectés sous « /polls/ », sous « /fun_polls/ » ou sous « /content/polls/ » ou tout autre chemin racine sans que cela change quoi que ce soit au fonctionnement de l’application.

Voici ce qui se produit lorsqu’un utilisateur se rend à l’adresse « /polls/34/ » :

  • Django trouve une correspondance à '^polls/'

  • Puis, Django supprime la portion qui correspondait ("polls/") et envoie le texte restant (`` “34/”) à l'URLconf « polls.urls » pour traitement final, avec une correspondance pour``r'^(?P<poll_id>\d+)/$', ce qui produira un appel à la vue detail() comme ceci :

    detail(request=<HttpRequest object>, poll_id='34')
    

La partie poll_id='34' vient de (?P<poll_id>\d+). En utilisant des parenthèses autour d’un motif, cela « capture » le texte correspondant à ce motif et l’envoie en tant que paramètre à la fonction de la vue ; le terme ?P<poll_id> définit le nom qui va être utilisé pour identifier le motif trouvé, et \d+ est une expression régulière pour chercher une suite de chiffres (c’est-à-dire un nombre).

Comme les motifs d’URL sont des expressions régulières, il n’y a vraiment aucune limite à ce qu’ils vous permettent de faire. Et il n’y a pas besoin d’ajouter de fioritures aux URL tel que .html – sauf si vous le voulez vraiment, auquel cas vous pouvez faire quelque chose comme ça :

(r'^polls/latest\.html$', 'polls.views.index'),

Mais ne le faites pas. C’est stupide.

Écriture de vues qui font réellement des choses

Chaque vue est responsable de faire une des deux choses suivantes : retourner un objet HttpResponse contenant le contenu de la page demandée, ou lever une exception, comme par exemple Http404. Le reste, c’est votre travail.

Votre vue peut lire des entrées depuis une base de données, ou pas. Elle peut utiliser un système de gabarits comme celui de Django – ou un système de gabarits tiers – ou pas. Elle peut générer un fichier PDF, produire de l’XML, créer un fichier ZIP à la volée, tout ce que vous voulez, en utilisant les bibliothèques Python que vous voulez.

Voilà tout ce que veut Django : HttpResponse ou une exception.

Because it’s convenient, let’s use Django’s own database API, which we covered in Tutorial 1. Here’s one stab at a new index() view, which displays the latest 5 poll questions in the system, separated by commas, according to publication date:

from django.http import HttpResponse

from polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.order_by('-pub_date')[:5]
    output = ', '.join([p.question for p in latest_poll_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Cependant, il y a un problème : l’allure de la page est codée en dur dans la vue. Si vous voulez changer le style de la page, vous devrez modifier votre code python. Nous allons donc utiliser le système de gabarits de Django pour séparer le style du code Python en créant un gabarit que la vue pourra utiliser.

Tout d’abord, créez un répertoire nommé templates dans votre répertoire polls. C’est là que Django recherche les gabarits.

Le réglage TEMPLATE_LOADERS de Django contient une liste de classes qui savent comment importer des gabarits à partir de différentes sources. L’une de ces classes par défaut est django.template.loaders.app_directories.Loader qui recherche dans un sous-répertoire « templates » de chaque application incluses dans INSTALLED_APPS. C’est ainsi que Django sait où trouver les gabarits de l’application polls même si nous n’avons pas touché à TEMPLATE_DIRS, comme nous l’avions fait dans le tutoriel 2.

Organisation des gabarits

Nous pourrions placer tous nos gabarits à seul endroit et tout fonctionnerait très bien. Cependant, ce gabarit appartient à l’application polls. Contrairement au gabarit admin que nous avons créé lors du tutoriel précédent, nous placerons celui-ci dans le répertoire des gabarits de l’application (polls/templates) plutôt que dans celui du projet (templates). Nous aborderons plus en détails les raisons de ces choix dans le tutoriel sur les applications réutilisables.

Dans le répertoire templates que vous venez de créer, créez un autre répertoire nommé polls` dans lequel vous placez un nouveau fichier index.html. Autrement dit, le chemin de votre gabarit doit être polls/templates/polls/index.html. Conformément au fonctionnement du chargeur de gabarit app_directories (cf. explication ci-dessus), vous pouvez désigner ce gabarit dans Django tout simplement par polls/index.html.

Espace de noms des gabarits

Il serait aussi possible de placer directement nos gabarits dans polls/templates (plutôt que dans un sous-répertoire polls), mais ce serait une mauvaise idée. Django choisit le premier gabarit qu’il trouve pour un nom donné et dans le cas où vous avez un gabarit de même nom dans une autre application, Django ne fera pas la différence. Il faut pouvoir indiquer à Django le bon gabarit, et la manière la plus simple de faire cela est d’utiliser des espaces de noms. C’est-à-dire que nous plaçons ces gabarits dans un autre répertoire portant le nom de l’application.

Insérez le code suivant dans ce gabarit :

{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Mettons maintenant à jour notre vue index dans polls/views.py pour qu’elle utilise le gabarit :

from django.http import HttpResponse
from django.template import RequestContext, loader

from polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = RequestContext(request, {
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(template.render(context))

Ce code charge le gabarit appelé polls/index.html et lui fournit un contexte. Ce contexte est un dictionnaire qui fait correspondre des objets Python à des noms de variables de gabarit.

Chargez la page en appelant l’URL « /polls/ » dans votre navigateur et vous devriez voir une liste à puces contenant le sondage « What’s up » du tutoriel 1. Le lien pointe vers la page de détail du sondage.

Un raccourci : render()

Il est très courant de charger un gabarit, remplir un contexte et renvoyer un objet HttpResponse avec le résultat du gabarit interprété. Django fournit un raccourci. Voici la vue index() complète, réécrite :

from django.shortcuts import render

from polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    context = {'latest_poll_list': latest_poll_list}
    return render(request, 'polls/index.html', context)

Notez qu’à partir du moment où nous avons fait ceci dans toutes nos vues, nous n’avons plus à importer loader, RequestContext et HttpResponse (il faut conserver HttpResponse tant que les méthodes initiales pour detail, results et vote sont présentes).

La fonction render() prend comme premier paramètre l’objet requête, un nom de gabarit comme deuxième paramètre et un dictionnaire comme troisième paramètre facultatif. Elle retourne un objet HttpResponse composé par le gabarit interprété avec le contexte donné.

Les erreurs 404

Attaquons-nous maintenant à la vue du détail d’un sondage – la page qui affiche la question pour un sondage donné. Voici la vue :

from django.http import Http404
from django.shortcuts import render

from polls.models import Poll
# ...
def detail(request, poll_id):
    try:
        poll = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404
    return render(request, 'polls/detail.html', {'poll': poll})

Le nouveau concept ici : la vue lève une exception de type Http404 si un sondage avec l’ID demandé n’existe pas.

Nous parlerons un peu plus tard de ce que pourriez mettre dans le gabarit polls/detail.html, mais si vous voulez avoir rapidement un exemple qui fonctionne, écrivez simplement ceci :

{{ poll }}

et vous obtiendrez un résultat élémentaire.

Un raccourci : get_object_or_404()

Il est très courant d’utiliser get() et de lever une exception Http404 si l’objet n’existe pas. Django fournit un raccourci. Voici la vue detail() réécrite :

from django.shortcuts import render, get_object_or_404

from polls.models import Poll
# ...
def detail(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    return render(request, 'polls/detail.html', {'poll': poll})

La fonction get_object_or_404() prend un modèle Django comme premier paramètre et un nombre arbitraire de paramètres mots-clés, qu’il transmet à la méthode get() du gestionnaire du modèle. Elle lève une exception Http404 si l’objet n’existe pas.

Philosophie

Pourquoi utiliser une fonction auxiliaire get_object_or_404() plutôt que d’intercepter automatiquement une exception ObjectDoesNotExist à un plus haut niveau, ou laisser l’API modèle lever une exception Http404 à la place de ObjectDoesNotExist ?

Parce que cela couplerait la couche de gestion des modèles à la couche de vue. Un des buts principaux de la conception de Django et de garder un couplage le plus faible possible. Un peu de couplage contrôlé est introduit dans le module django.shortcuts.

Il y a aussi une fonction get_list_or_404(), qui fonctionne comme get_object_or_404(), sauf qu’elle utilise filter() au lieu de la méthode get(). Elle lève une exception Http404 si la liste est vide.

Utilisation du système de gabarits

Revenons à la vue detail() de notre application de sondage. Étant donné la variable de contexte poll, voici à quoi le gabarit polls/detail.html pourrait ressembler :

<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

Le système de gabarits utilise une syntaxe d’accès aux attributs de variables à l’aide de points. Dans cet exemple avec {{ poll.question }}, Django commence par rechercher un dictionnaire dans l’objet poll. En cas d’échec, il cherche un attribut – qui fonctionne dans ce cas. Si la recherche d’attribut n’avait pas fonctionné, Django aurait essayé une recherche d’index sur une liste.

L’appel de méthode a lieu dans la boucle {% for %} : poll.choice_set.all est interprété comme le code Python poll.choice_set.all(), qui renvoie un itérable d’objets Choice et qui convient pour l’utilisation de la balise {% for %}.

Voir le guide des gabarits pour plus d’informations sur les gabarits.

Suppression des URL codés en dur dans les gabarits

Rappelez-vous, lorsque nous avons ajouté le lien vers le sondage dans le gabarit polls/index.html, le lien a été partiellement codé en dur comme ceci :

<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>

Le problème de cette approche codée en dur et fortement couplée est qu’il devient fastidieux de modifier les URL dans des projets qui ont beaucoup de gabarits. Cependant, comme vous avez défini le paramètre « name » dans les fonctions url() du module polls.urls, vous pouvez supprimer la dépendance en chemins d’URL spécifiques définis dans les configurations d’URL en utilisant la balise de gabarit {% url %} :

<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>

Note

Si {% url 'detail' poll.id %} (avec guillemets) ne fonctionne pas, mais que {% url detail poll.id %} (sans guillemet) fonctionne, cela signifie que vous utilisez une version de Django < 1.5, auquel cas vous devez ajouter la déclaration suivante au sommet de votre gabarit :

{% load url from future %}

Le principe de ce fonctionnement est que l’URL est recherchée dans les définitions du module polls.urls. Ci-dessous, vous pouvez voir exactement où le nom d’URL de « detail » est défini :

...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<poll_id>\d+)/$', views.detail, name='detail'),
...

Si vous souhaitez modifier l’URL de détail des sondages, par exemple sur le modèle polls/specifics/12/, il suffit de faire la modification dans polls/urls.py au lieu de devoir toucher au contenu du ou des gabarits :

...
# added the word 'specifics'
url(r'^specifics/(?P<poll_id>\d+)/$', views.detail, name='detail'),
...

Espaces de noms et noms d’URL

Le projet du tutoriel ne contient qu’une seule application, polls. Dans des projets Django réels, il peut y avoir cinq, dix, vingt applications ou plus. Comment Django arrive-t-il à différencier les noms d’URL entre elles ? Par exemple, l’application polls possède une vue detail et il se peut tout à fait qu’une autre application du même projet en possède aussi une. Comment peut-on indiquer à Django quelle vue d’application il doit appeler pour une URL lors de l’utilisation de la balise de gabarit {% url %} ?

La réponse est donnée par l’ajout d’espaces de noms à votre URLconf racine. Dans le fichier mysite/urls.py (celui du projet, pas de l’application), modifiez le code pour y inclure un espace de noms :

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^polls/', include('polls.urls', namespace="polls")),
    url(r'^admin/', include(admin.site.urls)),
)

Modifiez maintenant la partie suivante du gabarit polls/index.html` :

<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>

pour qu’elle pointe vers la vue « detail » à l’espace de nom correspondant :

<li><a href="{% url 'polls:detail' poll.id %}">{{ poll.question }}</a></li>

Lorsque vous êtes à l’aise avec l’écriture des vues, lisez la partie 4 de ce tutoriel pour apprendre la gestion de formulaires simples et les vues génériques.