This tutorial begins where Tutorial 3 left off. We’re continuing the web-poll application and will focus on form processing and cutting down our code.
Gdzie szukać pomocy:
Jeśli masz trudności w przejściu tego tutorialu, przejdź do sekcji Uzyskiwanie pomocy często zadawanych pytań.
Zaktualizujmy nasz szablon szczegółów ankiety („polls/detail.html”) z ostatniego tutoriala, aby zawierał element HTML <form>
:
polls/templates/polls/detail.html
¶<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% 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 %}
</fieldset>
<input type="submit" value="Vote">
</form>
Krótkie sprawozdanie:
value
każdego przycisku opcji to ID związanej z pytaniem odpowiedzi. Atrybut name
każdego przycisku opcji to "choice"
. To znaczy, że kiedy ktoś wybierze jeden z przycisków opcji i wyśle formularz, formularz wyśle dane POST choice=#
, gdzie # jest ID wybranej odpowiedzi. To jest podstawowa idea formularzy HTML.action
to {% url 'polls:vote' question.id %}
, and we
set method="post"
. Using method="post"
(as opposed to
method="get"
) is very important, because the act of submitting this
form will alter data server-side. Whenever you create a form that alters
data server-side, use method="post"
. This tip isn’t specific to
Django; it’s good web development practice in general.forloop.counter
mówi ile razy tag for
przeszedł przez swoją pętlę{% csrf_token %}
.Teraz stwórzmy widok Django, który obsługuje wysłane dane i coś z nimi robi. Pamiętaj, w Tutorialu 3, stworzyliśmy URLconfa dla aplikacji ankietowej, który zawiera tę linię:
polls/urls.py
¶path('<int:question_id>/vote/', views.vote, name='vote'),
Stworzyliśmy także zaślepkową implementację funkcji vote()
. Stwórzmy prawdziwą wersję. Dodaj następujący kod do polls/views.py
:
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,)))
Ten kod zawiera kilka rzeczy, których jeszcze nie omówiliśmy w tym tutorialu:
request.POST
to słownikowy obiekt, który daje dostęp do wysłanych danych według nazwy klucza. W tym przypadku request.POST['choice']
zwraca ID wybranej odpowiedzi jako stringa. Wartości request.POST
są zawsze stringami.
Zwróć uwagę, że Django ma także request.GET
dające dostęp do danych GET w ten sam sposób – ale my specjalnie używamy request.POST
w naszym kodzie, aby upewnić się, że dane są zmieniane tylko przez wywołanie POST.
request.POST['choice']
zgłosi KeyError
jeśli atrybutu choice
nie było wśród danych POST. Powyższy kod sprawdza KeyError
i ponownie wyświetla formularz pytania z komunikatem błędu, jeśli choice
nie został wskazany.
Po zwiększeniu licznika odpowiedzi, kod zwraca HttpResponseRedirect
zamiast normalnej HttpResponse
. HttpResponseRedirect
bierze jeden argument: URL, do którego użytkownik będzie przekierowany (zobacz następny punkt, aby dowiedzieć się, jak w tym przypadku kontruujemy URL-e).
As the Python comment above points out, you should always return an
HttpResponseRedirect
after successfully dealing with
POST data. This tip isn’t specific to Django; it’s good web development
practice in general.
Używamy funkcji reverse()
w konstruktorze HttpResponseRedirect
w tym przykładzie. Ta funkcja pomaga uniknąć konieczności hardkodowania URL-i w funkcji widoku. Daje jej się nazwę widoku, któremu chcemy przekazać kontrolę i zestaw zmiennych z wzorca URL-a, który prowadzi do tego widoku. W tym przypadku, używając URLconfa, którego napisaliśmy w Tutorialu 3, to wywołanie reverse()
zwróci taki ciąg znaków:
'/polls/3/results/'
gdzie 3
jest wartością question.id
. Ten przekierowany URL później wywoła widok 'results'
, aby wyświetlić ostateczną stronę.
Jak wspomniano w Tutorialu 3, request
jest obiektem HttpRequest
. Po więcej informacji na temat obiektów HttpRequest
, zobacz dokumentację żądań i odpowiedzi.
Po tym, jak ktoś zagłosuje w danym pytaniu, widok vote()
przekieruje go na stronę wyników dla pytania. Napiszmy ten widok:
polls/views.py
¶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})
Jest prawie identyczny z widokiem detail()
z Tutoriala 3. Jedyna różnica to nazwa szablonu. Naprawimy tę redundancję później.
Teraz stwórzmy szablon polls/results.html
:
polls/templates/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>
Teraz wejdź na /polls/1/
w swojej przeglądarce i zagłosuj w ankiecie. Powinieneś zobaczyć stronę wyników, która aktualizuje się za każdym razem, kiedy oddasz głosz. Jeśli wyślesz formularz bez wybranej odpowiedzi, powinieneś zobaczyć komunikat błędu.
Informacja
Jest mały problem w kodzie naszego widoku vote()
. Najpierw dostaje obiekt selected_choice
z bazy danych, następnie oblicza nową wartość `` votes i dalej zapisuje ją z powrotem do bazy danych. Jeśli dwóch użytkowników twojej witryny spróbuje zagłosować dokładnie w tym samym czasie, może to pójść źle: Ta sama wartość, powiedzmy 42, będzie pobrana dla votes
. Następnie dla obu użytkowników nowa wartość 43 jest obliczona i zapisana, ale oczekiwaną wartością jest 44.
Nazywa się to hazardem (race condition). Jeśli jesteś zainteresowany, możesz przeczytać Avoiding race conditions using F(), aby dowiedzieć się, jak możesz rozwiązać ten problem.
Widoki detail()
(z Tutoriala 3) i results()
są bardzo krótkie – i, jak wspomnieliśmy wcześniej, redundantne. Widok index()
, który wyświetal listę ankiet, jest podobny.
These views represent a common case of basic web development: getting data from the database according to a parameter passed in the URL, loading a template and returning the rendered template. Because this is so common, Django provides a shortcut, called the „generic views” system.
Widoki generyczne wyodrębniają powszechne wzorce do punktu, gdzie nie potrzebujesz nawet pisać pythonowego kodu, aby napisać aplikację.
Przekonwertujmy naszą aplikację ankietową, aby używała systemu widoków generycznych, abyśmy mogli usunąć sporo naszego własnego kodu. Musimy podjąć kilka kroków, aby dokonać konwersji. Będziemy:
Kontynuuj czytanie, aby dowiedzieć się szczegółów.
Dlaczego podmiana kodu?
W ogólności, pisząc aplikację Django, ocenisz, czy widoki generyczne są dobre do twojego problemu i będziesz używał ich od początku zamiast robić refactoring kodu w połowie pracy. Ten tutorial specjalnie był skupiony na pisaniu widoków „w trudny sposób” aż do teraz, aby skupić się podstawowych konceptach.
Powinieneś znać podstawową matematykę zanim zaczniesz używać kalkulatora.
Najpierw otwórz URLconfa w polls/urls.py
i zmień go tak:
polls/urls.py
¶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'),
]
Zwróć uwagę, że nazwa dopasowanego wzorca w ciągach znaków ścieżki drugiego i trzeciego wzorca zmieniła się z <question_id>
na <pk>
.
Następnie usuniemy nasze stare widoki index
, detail
i results
i użyjemy zamiast nich generycznych widoków Django. Aby to zrobić, otwórz plik polls/views.py
i zmień go w ten sposób:
polls/views.py
¶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.
Używamy tutaj dwóch widoków generycznych: ListView
i DetailView
. Odpowiednio, te dwa widoki abstrahują koncepty „wyświetlania listy obiektów” i „wyświetlania strony szczegółów dla wybranego typu obiektu”.
model
.DetailView
spodziewa się wartości klucza głównego zebranego z URL-a i nazwanego "pk"
, więc zmieniliśmy question_id
na pk
dla widoków generycznych.Domyślnie widok generyczny DetailView
używa szablonu o nazwie <app name>/<model name>_detail.html
. W naszym przypadku użyłby szablonu "polls/question_detail.html"
. Atrybut template_name
jest używany, by kazać Django użyć wskazanego szablonu zamiast samo-wygenerowanej domyślnej nazwy szablonu. Wskazujemy również template_name
dla widoku listy results
– to powoduje, że widok wyników i widok szczegółów mają inny wygląd po wyrenderowaniu, mimo że oba są DetailView
pod maską.
Podobnie widok generyczny ListView
używa domyślnego szablonu o nazwie <app name>/<model name>_list.html
; używamy template_name
, aby powiedzieć ListView
, by używała naszego istniejącego szablonu "polls/index.html"
.
W poprzednich częściach tutoriala szablonom dawaliśmy kontekst, który zawierał zmienne kontekstu question
i latest_question_list
. Dla DetailView
zmienna question
jest dostarczona automatycznie – ponieważ używamy modelu Django (Question
), Django może wyznaczyć odpowiednią nazwę dla zmiennej kontekstowej. Jednak dla ListView automatycznie generowaną zmienną kontekstową jest question_list
. Aby to nadpisać nadajemy wartość atrybutowi context_object_name
, wskazując, że chcemy użyć zamiast niej latest_question_list
. Alternatywnym podejściem byłoby zmienienie szablonów, aby zgadzały się z nowymi domyślnymi zmiennymi kontekstowymi – ale dużo prościej jest powiedzieć Django, aby używało zmiennej, jakiej chcesz.
Uruchom serwer i użyj nowej aplikacji ankietowej opartej na widokach generycznych.
Po wszystkie szczegóły widoków generycznych, sprawdź dokumentację widoków generycznych.
Kiedy już czujesz się dobrze z formularzami i widokami generycznymi, przeczytaj część 5 tego tutoriala, aby nauczyć się testowania naszej ankietowej aplikacji.
mar 08, 2023