kleindan.dev

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: Wyrenderowany formularz

Najważniejszymi atrybutami dla elementów <input> są:

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.

Wyrenderowane dane

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:

Poll voting

A wyniki będą prezentowane w ten sposób: Poll results

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.