Écriture de votre première application Django, 4ème partie¶
Ce tutoriel commence là où le tutoriel 3 s’achève. Nous continuons l’application de sondage Web et allons nous focaliser sur la gestion de formulaire simple et sur la réduction du code.
Écriture d’un formulaire simple¶
Nous allons mettre à jour le gabarit de la page de détail (« polls/details.html ») du tutoriel précédent, de manière à ce que le gabarit contienne une balise HTML <form>
:
<h1>{{ question.question_text }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>
Un résumé rapide :
- Ce gabarit affiche un bouton radio pour chaque choix de question. L’attribut
value
de chaque bouton radio correspond à l’ID du vote choisi. Le nom (name
) de chaque bouton radio est"choice"
. Cela signifie que lorsque quelqu’un sélectionne l’un des boutons radio et valide le formulaire, les données POSTchoice=#
(où # est l’identifiant du choix sélectionné) seront envoyées. Ce sont les concepts de base des formulaires HTML. - Nous avons défini
{% url 'polls:vote' question.id %}
comme attributaction
du formulaire, et nous avons précisémethod="post"
. L’utilisation demethod="post"
(par opposition àmethod="get"
) est très importante, puisque le fait de valider ce formulaire va entraîner des modifications de données sur le serveur. À chaque fois qu’un formulaire modifie des données sur le serveur, vous devez utilisermethod="post"
. Cela ne concerne pas uniquement Django ; c’est une bonne pratique à adopter en tant que développeur Web. forloop.counter
indique combien de fois la balisefor
a exécuté sa boucle.- Comme nous créons un formulaire POST (qui modifie potentiellement des données), il faut se préoccuper des attaques inter-sites. Heureusement, vous ne devez pas réfléchir trop longtemps car Django offre un moyen très simple à utiliser pour s’en protéger. En bref, tous les formulaires POST destinés à des URL internes doivent utiliser la balise de gabarit
{% csrf_token %}
.
Maintenant, nous allons créer une vue Django qui récupère les données envoyées pour nous permettre de les exploiter. Souvenez-vous, dans le tutoriel 3, nous avons créé un URLconf pour l’application de sondage contenant cette ligne :
path('<int:question_id>/vote/', views.vote, name='vote'),
Nous avions également créé une implémentation rudimentaire de la fonction vote()
. Créons maintenant une version fonctionnelle. Ajoutez ce qui suit dans le fichier polls/views.py
:
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from .models import Choice, Question
# ...
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
Ce code contient quelques points encore non abordés dans ce tutoriel :
request.POST
est un objet similaire à un dictionnaire qui vous permet d’accéder aux données envoyées par leurs clés. Dans ce cas,request.POST['choice']
renvoie l’ID du choix sélectionné, sous forme d’une chaîne de caractères. Les valeurs dansrequest.POST
sont toujours des chaînes de caractères.Notez que Django dispose aussi de
request.GET
pour accéder aux données GET de la même manière – mais nous utilisons explicitementrequest.POST
dans notre code, pour s’assurer que les données ne sont modifiées que par des requêtes POST.request.POST['choice']
lèvera une exceptionKeyError
sichoice
n’est pas spécifié dans les données POST. Le code ci-dessus vérifie qu’une exceptionKeyError
n’est pas levée et réaffiche le formulaire de question avec un message d’erreur sichoice
n’est pas rempli.Après l’incrémentation du nombre de votes du choix, le code renvoie une
HttpResponseRedirect
plutôt qu’uneHttpResponse
normale.HttpResponseRedirect
prend un seul paramètre : l’URL vers laquelle l’utilisateur va être redirigé (voir le point suivant pour la manière de construire cette URL dans ce cas).Comme le commentaire Python l’indique, vous devez systématiquement renvoyer une
HttpResponseRedirect
après avoir correctement traité les données POST. Ceci n’est pas valable uniquement avec Django, c’est une bonne pratique du développement Web.Dans cet exemple, nous utilisons la fonction
reverse()
dans le constructeur deHttpResponseRedirect
. Cette fonction nous évite de coder en dur une URL dans une vue. On lui donne en paramètre la vue vers laquelle nous voulons rediriger ainsi que la partie variable de l’URL qui pointe vers cette vue. Dans ce cas, en utilisant l’URLconf défini dans la partie 3 de ce tutoriel, l’appel de la fonctionreverse()
va renvoyer la chaîne de caractères :'/polls/3/results/'
où
3
est la valeur dequestion.id
. Cette URL de redirection va ensuite appeler la vue'results'
pour afficher la page finale.
Comme expliqué dans la partie 3 de ce tutoriel, request
est un objet HttpRequest
. Pour plus d’informations sur les objets HttpRequest
, voir la documentation des requêtes et réponses.
Après le vote d’une personne dans une question, la vue vote()
redirige vers la page de résultats de la question. Écrivons cette vue :
from django.shortcuts import get_object_or_404, render
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
C’est presque exactement la même que la vue detail()
du tutoriel 3. La seule différence est le nom du gabarit. Nous éliminerons cette redondance plus tard.
Écrivons maintenant le gabarit polls/results.html
:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
Maintenant, rendez-vous à la page /polls/1/
avec votre navigateur et votez pour la question proposée. Vous devriez voir une page de résultats qui sera mise à jour à chaque fois que vous voterez. Si vous validez le formulaire sans avoir coché votre choix, vous devriez voir le message d’erreur.
Note
Le code de notre vue vote()
présente un petit problème. Il obtient d’abord l’objet selected_choice
depuis la base de données, puis calcule la nouvelle valeur de votes
, et enregistre ensuite le résultat dans la base de données. Si deux utilisateurs du site Web essaient de voter exactement au même moment, cela peut mal se passer : la même valeur, disons 42, sera obtenue pour votes
. Puis, la nouvelle valeur calculée sera de 43 pour les deux utilisateurs qui enregistreront cette valeur, alors qu’elle devrait être de 44 au final.
On appelle cela une situation de compétition. Si cela vous intéresse, vous pouvez lire Prévention des conflits de concurrence avec F() pour savoir comment il est possible d’éviter ce genre de situations.
Utilisation des vues génériques : moins de code, c’est mieux¶
Les vues detail()
(cf. tutoriel 3) et results()
sont triviales – et comme mentionné précédemment, redondantes. La vue index()
qui affiche une liste de sondages est similaire.
Ces vues représentent un cas classique du développement Web : récupérer les données depuis la base de données suivant un paramètre contenu dans l’URL, charger un gabarit et renvoyer le gabarit interprété. Ce cas est tellement classique que Django propose un raccourci, appelé le système de « vues génériques ».
Les vues génériques permettent l’abstraction de pratiques communes, à un tel point que vous n’avez pas à écrire de code Python pour écrire une application.
Nous allons convertir notre application de sondage pour qu’elle utilise le système de vues génériques. Nous pourrons ainsi supprimer une partie de notre code. Nous avons juste quelques pas à faire pour faire cette conversion. Nous allons :
- Convertir l’URLconf.
- Supprimer quelques anciennes vues désormais inutiles.
- Introduire de nouvelles vues basées sur les vues génériques de Django.
Lisez la suite pour plus de détails.
Pourquoi ces changements de code ?
En général, lorsque vous écrivez une application Django, vous devez estimer si les vues génériques correspondent bien à vos besoins et, le cas échéant, vous les utiliserez dès le début, plutôt que de réarranger votre code à mi-chemin. Mais ce tutoriel s’est concentré intentionnellement sur l’écriture des vues « à la dure » jusqu’à ce point, pour mettre l’accent sur les concepts de base.
Tout comme vous devez posséder des bases de maths avant de commencer à utiliser une calculatrice.
Correction de l’URLconf¶
Tout d’abord, ouvrez la configuration d’URL polls/urls.py
et modifiez-la ainsi :
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
path('<int:question_id>/vote/', views.vote, name='vote'),
]
Notez que le nom du motif correspondant dans les chaînes de chemin des deuxième et troisième motifs a été modifié de <question_id>
en <pk>
.
Correction des vues¶
Ensuite, nous allons enlever les anciennes vues index
, detail
et results
et utiliser à la place des vues génériques de Django. Pour cela, ouvrez le fichier polls/views.py
et modifiez-le de cette façon :
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
... # same as above, no changes needed.
Nous utilisons ici deux vues génériques : ListView
et DetailView
. Respectivement, ces deux vues permettent l’abstraction des concepts « afficher une liste d’objets » et « afficher une page détaillée pour un type particulier d’objet ».
- Chaque vue générique a besoin de connaître le modèle sur lequel elle va agir. Cette information est fournie par l’attribut
model
. - La vue générique
DetailView
s’attend à ce que la clé primaire capturée dans l’URL s’appelle"pk"
, nous avons donc changéquestion_id
enpk
pour les vues génériques.
Par défaut, la vue générique DetailView
utilise un gabarit appelé <nom app>/<nom modèle>_detail.html
. Dans notre cas, elle utiliserait le gabarit "polls/question_detail.html"
. L’attribut template_name
est utilisé pour signifier à Django d’utiliser un nom de gabarit spécifique plutôt que le nom de gabarit par défaut. Nous avons aussi indiqué le paramètre template_name
pour la vue de liste results
, ce qui permet de différencier l’apparence du rendu des vues « results » et « detail », même s’il s’agit dans les deux cas de vues DetailView
à la base.
De la même façon, la vue générique ListView
utilise par défaut un gabarit appelé <nom app>/<nom modèle>_list.html
; nous utilisons template_name
pour indiquer à ListView
d’utiliser notre gabarit existant "polls/index.html"
.
Dans les parties précédentes de ce tutoriel, les templates ont été renseignés avec un contexte qui contenait les variables de contexte question
et latest_question_list
. Pour DetailView
, la variable question
est fournie automatiquement ; comme nous utilisons un modèle nommé Question
, Django sait donner un nom approprié à la variable de contexte. Cependant, pour ListView
, la variable de contexte générée automatiquement s’appelle question_list
. Pour changer cela, nous fournissons l’attribut context_object_name
pour indiquer que nous souhaitons plutôt la nommer latest_question_list
. Il serait aussi possible de modifier les templates en utilisant les nouveaux nom de variables par défaut, mais il est beaucoup plus simple d’indiquer à Django les noms de variables que nous souhaitons.
Lancez le serveur et utilisez votre nouvelle application de sondage basée sur les vues génériques.
Pour plus de détails sur les vues génériques, voir la documentation des vues génériques.
Lorsque vous êtes à l’aise avec les formulaires et les vues génériques, lisez la 5ème partie de ce tutoriel pour apprendre comment tester notre application de sondage.