Formularze HTML i obsługa żądań HTTP POST w Django
Formularze HTTP
Dotychczas korzystając z naszej aplikacji wpisywaliśmy w pasek adresu przeglądarki URI, który chcieliśmy pobrać, a przeglądarka wysyłała do naszego serwera żądanie HTTP GET
.
Aplikacje internetowe zazwyczaj nie opierają się wyłącznie na pobieraniu informacji, musimy też mieć możliwość wysyłania danych do serwera.
Taką funkcję spełniają w przypadku protokołu HTTP
żądania POST
, które przeglądarka wysyła w przypadku przesyłania formularzy HTML.
Eksperymentalny formularz HTML
Najlepiej będzie przygotować sobie przykładowy formularz HTML i obsłużyć go w naszej aplikacji, żeby zobaczyć, jakie mamy opcje przy tworzeniu formularza i w jaki sposób dane wpisane w przeglądarce trafią do naszej aplikacji.
Zacznijmy od stworzenia najgorszego formularza HTML w historii. Do tego posłuży nam następujący kod:
{% extends "polls/base.html" %}
{% block content %}
<form action="{% url 'polls:forms' %}" method="POST">
{% csrf_token %}
<label for="idButton">Button</label>
<input id="idButton" type="button" value="click me"><br>
<label for="idCheckbox">Checkbox</label>
<input name="checkbox" id="idCheckbox" type="checkbox"><br>
<label for="idColor">Color</label>
<input name="color" id="idColor" type="color"><br>
<label for="idDate">Date</label>
<input name="date" id="idDate" type="date"><br>
<label for="idDatetimeLocal">DatetimeLocal</label>
<input name="datetime-local" id="idDatetimeLocal" type="datetime-local"><br>
<label for="idEmail">Email</label>
<input name="email" id="idEmail" type="email"><br>
<label for="idFile">File</label>
<input name="file" id="idFile" type="file"><br>
<label for="idHidden">Hidden</label>
<input name="hidden" id="idHidden" type="hidden"><br>
<label for="idImage">Image</label>
<input name="image" id="idImage" type="image"><br>
<label for="idMonth">Month</label>
<input name="month" id="idMonth" type="month"><br>
<label for="idNumber">Number</label>
<input name="number" id="idNumber" type="number"><br>
<label for="idPassword">Password</label>
<input name="password" id="idPassword" type="password"><br>
<label for="idRadio1">Radio 1</label>
<input id="idRadio1" name="radio" type="radio" value="1"><br>
<label for="idRadio2">Radio 2</label>
<input id="idRadio2" name="radio" type="radio" value="2"><br>
<label for="idRadio3">Radio 3</label>
<input id="idRadio3" name="radio" type="radio" value="foo"><br>
<label for="idRange">Range</label>
<input name="range" id="idRange" type="range"><br>
<label for="idSearch">Search</label>
<input name="search" id="idSearch" type="search"><br>
<label for="idTel">Tel</label>
<input name="tel" id="idTel" type="tel"><br>
<label for="idText">Text</label>
<input name="text" id="idText" type="text"><br>
<label for="idTime">Time</label>
<input name="time" id="idTime" type="time"><br>
<label for="idUrl">Url</label>
<input name="url" id="idUrl" type="url"><br>
<label for="idWeek">Week</label>
<input name="week" id="idWeek" type="week"><br>
<label for="idSubmit">Submit</label>
<input id="idSubmit" type="submit">
<label for="idReset">Reset</label>
<input id="idReset" type="reset"><br>
</form>
{% endblock content %}
Aby móc ten szablon wyświetlić, będziemy musieli zmodyfikować pliki urls.py
oraz views.py
dodając nową ścieżkę do oraz kod widoku:
# ...
app_name = "polls"
urlpatterns = [
path('', views.index, name="index"),
path('temp/forms/', views.forms, name="forms"),
path('<str:poll_name>/', views.by_name, name="poll"),
]
# ...
def forms(request):
if request.method == 'GET':
return render(request, "polls/forms.html", {})
else:
return HttpResponseBadRequest("Unsupported method")
Ten niepiękny kawałek kodu HTML będzie przez przeglądarkę wyświetlony jako następujący formularz:
Najważniejszymi atrybutami dla elementów <input>
są:
name
, który przekłada się na to, pod jaką nazwą dane zostaną przesłane do aplikacji internetowejid
, który pozwala nam stworzyć etykietę<label>
dla tego polatype
, który określa, jakiego rodzaju danych spodziewamy się od użytkownika.value
, który ma różne zastosowania w zależności od typu taguinput
Poświęć chwilę na przejrzenie kodu HTML i przeanalizowaniu, jak poszczególne elementy są renderowane.
Więcej informacji na temat elementów <input>
można znaleźć na tej stronie.
Zadanie praktyczne
Używając debuggera zatrzymaj renderowanie widoku po przesłaniu formularza i spróbuj zlokalizować dane przesłane w formularzu w obiekcie “request”.
Obsługa metody HTTP POST
Kiedy już zakończymy eksperymenty, przejdźmy do tego, jak wygląda obsługa danych przesłanych w formularzu do naszej aplikacji. W tym celu zmodyfikujemy plik views.py
w następujący sposób:
def forms(request):
if request.method == 'GET':
return render(request, "polls/forms.html", {})
if request.method == 'POST':
return render(request, "polls/post.html", {'params': request.POST})
else:
return HttpResponseBadRequest("Unsupported method")
Następnie dodając poniższy szablon HTML w pliku templates/polls/post.html
:
{% extends "polls/base.html" %}
{% block content %}
<ul>
{% for key, value in params.items %}
<li>{{key}} = {{value}}</li>
{% endfor %}
</ul>
{% endblock content %}
Po tych zmianach, po przesłaniu naszego formularza możemy zobaczyć nazwy i wartości wszystkich przesłanych parametrów.
Pierwszą rzeczą wymagającą odrobiny wyjaśnienia jest lekko tajemniczy csrfmiddlewaretoken
, którego nie dodawaliśmy w naszym szablonie ani nie nadawaliśmy mu żadnej wartości, a jednak jest on obecny w danych przesłanego formularza. W dużym skrócie jest to element wymagany przez Django dla wszystkich formularzy przesyłanych metodą POST ze względów bezpieczeństwa. Dlaczego dokładnie jest wymagany i w jaki sposób zwiększa bezpieczeństwo opowiemy sobie w jednej z przyszłych lekcji.
Zadanie praktyczne
Usuń z szablonu templates/polls/forms.html
linijkę z kodem {% csrf_token %}
i sprawdź, czy formularz nadal działa, a jeżeli nie, to jakie są efekty.
Głosowanie w aplikacji ankiet przy pomocy formularzy HTML i żądań HTTP POST
Kończąc póki co eksperymenty, wróćmy do naszej oryginalnej aplikacji i dodajmy do niej możliwość głosowania w naszych ankietach.
Zacznijmy struktury naszych danych, ponieważ skoro będziemy głosować na nasze odpowiedzi, to gdzieś musimy tę informację przechowywać. Dlatego musimy dodać informację o ilości głosów do każdej z naszych opcji.
open_polls = {
"pets": {
"question": "Which are better, cats or dogs?",
"options": [
{
'text': "cats",
'votes': 0,
},
{
'text': "dogs",
'votes': 0,
},
],
},
"flavours": {
"question": "Which is better? Chocolate or vanilla?",
"options": [
{
'text': "chocolate",
'votes': 0,
},
{
'text': "vanilla",
'votes': 0,
},
],
},
"baby-boy-names":
{
"question": "What is the best name for a baby boy?",
"options": [
{
'text': "bryan",
'votes': 0,
},
{
'text': "donald",
'votes': 0,
},
{
'text': "justin",
'votes': 0,
},
]
}
}
Następnie przejdźmy do zmiany szablonu używanego do wyświetlania ankiety tak, aby pod pytaniem znalazły się opcje odpowiedzi oraz guzik pozwalający nam zagłosować w ankiecie:
{% extends "polls/base.html" %}
{% block content %}
{% if poll %}
<form action="{% url 'polls:vote' poll_name%}" method="post">
{% csrf_token %}
<fieldset>
<legend><h3>{{ poll.question }}</h3></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% for choice in poll.options %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ forloop.counter0 }}">
<label for="choice{{ forloop.counter }}">{{ choice.text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
{% else %}
<p>No such poll '{{ poll_name }}'!</p>
{% endif %}
{% endblock %}
A na koniec dodajmy obsługę odbioru danych z naszego formularza dodając następujący kod do pliku polls/views.py
:
def vote(request, poll_name):
if poll_name in poll_names:
selected_choice = open_polls[poll_name]['options'][int(request.POST["choice"])]
selected_choice['votes'] += 1
return HttpResponseRedirect(reverse("polls:results", args=[poll_name]))
else:
return render( request, "polls/detail.html", { "poll_name": poll_name, },)
Pojawił się tu nowy element, HttpResponseRedirect
. Zwrócenie obiektu tej klasy z widoku Django spowoduje, że w odpowiedzi HTTP znajdzie się informacja dla przeglądarki o adresie, pod jaki ma się następnie udać.
Aby móc wyświetlić wyniki ankiety, będziemy musieli dodatkowo stworzyć widok wyników. Kod takiego widoku może wyglądać następująco:
def results(request, poll_name):
context = { "poll_name": poll_name, }
retcode = 200
if poll_name in poll_names:
context['poll'] = open_polls[poll_name]
context['total_votes'] = max(1, sum(option['votes'] for option in open_polls[poll_name]['options']))
else:
retcode = 404
return render(request, "polls/results.html", context, status=retcode)
Ostatnim krokiem będzie dodanie szablonu renderującego wyniki naszej ankiety na ekran:
{% extends "polls/base.html" %}
{% block content %}
{% if poll %}
<p>{{ poll.question }}</p>
<ul>
{% for option in poll.options %}
<li>
<span class="poll-option">{{ option.text }}: {{option.votes}} vote(s)</span><br>
<span class="poll-votes" style="--votes: {{option.votes}}; --total-votes: {{total_votes}};"></span>
</li>
{% endfor %}
</ul>
{% else %}
<p>No such poll!</p>
{% endif %}
{% endblock %}
Nie zapomnijmy też podpiąć naszych widoków do listy obsługiwanych ścieżek:
from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
path('', views.index, name="index"),
path('<str:poll_name>/', views.by_name, name="poll"),
path('vote/<str:poll_name>/', views.vote, name="vote"),
path('results/<str:poll_name>/', views.results, name="results"),
]
Teraz używając odrobiny magii arkuszy stylów CSS jesteśmy w stanie sprawić, że nasze ankiety będą pozwalały na głosowanie w następujący sposób:
A wyniki będą prezentowane w ten sposób:
Proponuję po prostu skopiować poniższy arkusz stylów, a o tym, jak samemu uzyskiwać podobne i ładniejsze wyniki pomówimy na jednej z przyszłych lekcji.
.container {
margin: 0 auto;
width: 80%;
color: #222;
background-color: #d4d1ff;
padding: 2rem;
}
.container ul {
list-style-type: none;
padding: .1rem;
margin: .1rem 0;
}
.container ul li {
margin: .9rem 0;
}
.container a {
background-color: #b1adfe;
border-radius: .2rem;
padding: .2rem;
}
.container .poll-option {
font-weight: bold;
font-family: sans-serif;
}
.container .poll-votes {
background-color: #96acf6;
display: inline-block;
width: calc(5px + 300px*(var(--votes)/var(--total-votes)));
height: 1.2rem;
border-radius: .2rem;
}
.container form input[type=submit] {
background-color: #88E;
border: none;
border-radius: 1rem;
padding: 1rem 2rem;
text-decoration: none;
margin: 4px 2px;
cursor: pointer;
font-size: 1.4rem;
font-weight: 600;
}
.container form fieldset {
font-family: sans-serif;
border-radius: 1rem;
border-style: solid;
border-width: .2rem;
background: #88E;
border-color: #DDF;
font-weight: 500;
font-size: 1.1rem;
}
.container form fieldset input {
margin: 0.4rem 0.1rem;
}
.container form fieldset legend {
background: #88E;
border-radius: inherit;
padding: 1rem 2rem;
border-width: .2rem;
border-style: solid;
border-color: #DDF;
}
.container form fieldset legend h3 {
margin: 0;
}
Zadanie praktyczne
Zastanów się, jak na podstawie tego co już wiesz mógłbyś zaimplementować dodawanie kolejnych ankiet?
Na następnej lekcji pomówimy o tym, jak możemy usprawnić i ułatwić tworzenie szablonów dzięki Django, a także zaczniemy przenosić nasze dane tam, gdzie ich miejsce, czyli do bazy danych.