První aplikace v Djangu, část 4

Tento návod začíná tam, kde skončila třetí část ukázkové aplikace. Budeme pokračovat v naší webové anketní aplikaci a zaměříme se na jednoduché zpracování formulářů a na metody, které povedou k zeštíhlení již napsaného kódu.

Jednoduchý formulář

Otevřete si šablonu s detailem ankety (“polls/detail.html”), kterou jsme vytvořili v předchozí části tohoto návodu a vložme do ní HTML formulář:

<h1>{{ poll.question }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="/polls/{{ poll.id }}/vote/" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

Stručně k funkci:

  • Díky provedené úpravě se na stránce zobrazí sada radio buttonu. Atribut value každého buttonu bude odpovídat konkrétnímu ID anketní volby (ID z databázové tabulky). Atribut name bude nastaven na hodnotu “choice”. Pokud někdo odešle formulář, dorazí do view POST data, např. choice=3.
  • Akce formuláře (atribut action) je nastavena na hodnotu /polls/{{ poll.id }}/vote/, metoda (atribut method) na “post”. Použití method=”post” (namísto method=”get”) je velmi důležité, protože data odeslaná z formuláře způsobí změnu dat na straně serveru. Kdykoliv budete řešit podobnou situaci, využívejte výhradně method=”post”. Nejde o specifikum Djanga, tato rada platí obecně pro všechny webové aplikace.
  • Výraz forloop.counter vrací číselnou hodnotu odpovídající počtu provedený cyklů smyčky for.
  • Protože vytváříme POST formulář (který může na straně serveru vyvolat modifikaci dat), neměli bychom zapomenout na CSRF (Cross Site Request Forgeries). Django naštěstí obsahuje systém, který nás před tímto rizikem dokáže ochránit — stačí všechny POST formuláře vedoucí na interní URL vybavit tagem {% csrf_token %}.

Tak. Formulář jsme v šabloně definovali, teď je na čase napsat view, které se postará o zpracování dat. Vzpomeňte si na třetí část tohoto návodu, ve kterém jsme definovali URL pravidlo:

(r'^(?P<poll_id>\d+)/vote/$', 'vote'),

Tehdá jsme vytvořili “falešnou” implementaci hlasování — funkci vote(). Pojďme teď napsat kód, který bude opravdu něco dělat. Přidejte do souboru mysite/polls/views.py následující řádky:

from django.shortcuts import get_object_or_404, render_to_response
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from django.template import RequestContext
from mysite.polls.models import Choice, Poll
# ...
def vote(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # zobrazime formular znovu, tentokrat s chybovou hlaskou
        return render_to_response('polls/detail.html', {
            'poll': p,
            'error_message': "Nevybral(-a) jste zadnou anketni volbu.",
        }, context_instance=RequestContext(request))
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Po kazdem uspesnem zpracovani POST dat je treba vyvolat
        # HttpResponseRedirect. Tato praktika zabrani dvojimu
        # odeslani (a uzlozeni) dat z formulare v situacich, kdy
        # uzivatel klikne na tlacitko "Zpet" ve svem webovem
        # prohlizeci.
        return HttpResponseRedirect(reverse('mysite.polls.views.results', args=(p.id,)))

V kódu se nám objevilo několik novinek:

  • request.POST je objekt podobný slovníku, ze kterého můžeme vytahovat odeslaná data s pomocí jejich jména. Např. request.POST[‘choice’] vrátí ID vybrané volby ve formě řetězce. Hodnoty z request.POST se vrací vždy v podobě řetězce.

    Pamatujte, že Django prostřednictvím request.GET zpřístupňuje rovněž GET data — v našem ukázkovém kódu ale explicitně používáme request.POST, abychom měli jistotu, že data přichází skutečně z POSTu.

  • request.POST[‘choice’] vyvolá vyjímku KeyError v případě, že choice nebude v POST datech přítomno. Náš kód tuto situaci ošetřuje, a pokud by nastala, zobrazí formulář znovu spolu s chybovou hláškou.

  • Po započítání vybrané volby vrátí náš kód HttpResponseRedirect (namísto obvyklého HttpResponse). HttpResponseRedirect přijímá jediný argument: URL na které bude uživatel přesměrován (nepřehlédněte následující bod, ve kterém se vysvětluje způsob konstrukce URL adresy).

    Jak již zmiňuje komentář v kódu — po každém úspěšném zpracování POST dat by mělo následovat přesměrování s pomocí HttpResponseRedirect; jde o obecně platnou radu, kterou uplatníte nejen v Djangu, ale při jakémkoliv vývoji webových aplikací.

  • Pro vytvoření URL adresy (viz konstruktor HttpResponseRedirect) jsme použili funkci reverse(). Díky této funkci nemusíme do kódu “zadrátovávat” pevná URL, které bychom v případě jejich změny museli pracně hledat a opravovat. Funkci reverse() předáme jméno view, na které chceme přesměrovat a data, která odpovídají proměnným částem v URL pravidlu. V našem konkrétním případě může funkce reverse() vrátit např.:

    '/polls/3/results/'
    

    … kde 3 je hodnota p.id. Po přesměrování bude zavoláno view ‘results’, které zobrazí výsledky ankety. Všimněte si, že jméno view musíme definovat v plné formě (včetně prefixů).

Jak už bylo v předchozí třetí části tohoto návodu zmiňováno, request je objekt HttpRequest. Více informací o jeho vlastnostech naleznete v dokumentaci request a response.

Po provedení hlasování v anketě, bude uživatel přesměrován na finální stránku s výsledky. Napišme pro ni view:

def results(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': p})

Při pohledu na tento kód si možná vzpomenete na view detail() z třetí části návodu. Oba kódy jsou si velmi podobné, rozdíl je pouze v názvu šablony. Za chvíli se této nadbytečnosti zbavíme.

Vytvořme šablonu results.html:

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="/polls/{{ poll.id }}/">Chcete znovu hlasovat?</a>

Přepněte se do svého prohlížeče, navštivte stránku /polls/1/ a hlasujte pro některou z možností. Měla by se vám zobrazit stránka s výsledky, která bude po každé provedené volbě aktualizovaná. Pokud se pokusíte odeslat formulář bez zvolení některé z možností, měla by se vám zobrazit chybová hláška.

Generická view: Krátký kód, dobrý kód

View detail() (z třetí části návodu) a results() jsou velice jednoduché — a redundantní. View index() (rovněž z třetí části), které se stará o zobrazení seznamu anket, je na tom podobně.

Tato view reprezentují obvyklé procesy ve vývoji webových aplikací: podle parametrů z URL získej data z databáze, nahrej šablonu a vrať uživateli její vykreslenou (rendered) podobu. Protože se s podobnou situací budete setkávat stále, Django pro vás připravilo sadu pomocných “generic views”.

Generické view abstrahují obvyklé postupy do té míry, že pro svou aplikaci nebudete muset napsat ani řádek Python kódu.

Pojďme nyní přepsat naši anketní aplikaci tak, aby využívala generická view. Sami uvidíte kolik kódu nám úprava ušetří. Bude to jednoduché:

  1. Upravíme URLconf.
  2. Přejmenujeme pár šablon.
  3. Smažeme některá nepotřebná view.
  4. Opravíme URL pravidla pro nová view.

Čtěte dál.

Proč zasahovat do hotového, funkčního kódu?

Obvykle se pro generická view rozhodnete hned na začátku vytváření aplikace. Zvážíte jejich výhody pro váš konkrétní záměr a buď je použijete, nebo si view napíšete vlastní. V tomto návodu postupujeme naopak — nejdříve jsme volili pracnou cestu, abychom získali základní představu o fungování Djanga, a když už v tom máme jasno, poukazujeme na snadnější postupy.

Jinými slovy: než použijete kalkulačku, musíte znát základy matematiky.

Nejdříve otevřete soubor polls/urls.py. Bude vypadat nějak takto:

from django.conf.urls.defaults import *

urlpatterns = patterns('mysite.polls.views',
    (r'^$', 'index'),
    (r'^(?P<poll_id>\d+)/$', 'detail'),
    (r'^(?P<poll_id>\d+)/results/$', 'results'),
    (r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)

Změníme jej do následující podoby:

from django.conf.urls.defaults import *
from mysite.polls.models import Poll

info_dict = {
    'queryset': Poll.objects.all(),
}

urlpatterns = patterns('',
    (r'^$', 'django.views.generic.list_detail.object_list', info_dict),
    (r'^(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict),
    url(r'^(?P<object_id>\d+)/results/$', 'django.views.generic.list_detail.object_detail', dict(info_dict, template_name='polls/results.html'), 'poll_results'),
    (r'^(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)

Použijeme dvě generická view: object_list() a object_detail(). Tato view abstrahují obvyklé postupy “zobraz seznam objektů” a “zobraz detailní stránku pro konkretní typ objektu”.

  • Každé generické view potřebuje vědět, s jakými daty má pracovat. Tato data přijímá ve formě slovníku, přičemž pod klíčem queryset očekává seznam objektů.
  • Generické view object_detail() očekává přítomnost klíče “object_id” v URL pravidlu. Podle něj následně vytáhne objekt z databáze (podle ID). Z tohoto důvodu přejmenujeme poll_id na object_id.
  • URL pravidlo vedoucí na stránku s výsledky jsme pojmenovali jako poll_results (další podrobnosti o pojmenovávání pravidel). Definici pravidla jsme přepsali za pomoci funkce url()

Výchozí nastavení generického view object_detail() použije šablonu pojmenovanou podle vzoru <app name>/<model name>_detail.html. V našem případě to bude šablona “polls/poll_detail.html”. Přejmenujme tedy šablonu polls/detail.html na polls/poll_detail.html, a změňme její jméno i ve view vote() (viz volání render_to_response()).

Podobně přejmenujeme šablony pro generické view object_list() (podle vzoru <app name>/<model name>_list.html). Konkrétně, ze šablony polls/index.html uděláme polls/poll_list.html.

Protože máme v definici URL pravidel více záznamů, které shodně používají object_detail(), zadefinujeme manuálně jméno šablony pro stránku s výsledky: template_name=’polls/results.html’. Kdybychom to neudělali, obě detailní view by použila stejnou šablonu. Odbočka: všimněte si použití dict() — vrátí pozměněný slovník “in-place”.

Poznámka

django.db.models.QuerySet.all() je lenoch

Cože? Pro zobrazení detailu stránky se volá Poll.objects.all() (tj. z databáze se tahají všechny ankety)? Není třeba podléhat předčasným mrzutostem přátelé. Poll.objects.all() je ve skutečnosti speciální objekt QuerySet, který je “líný” — databázový dotaz není vykonán, dokud to není nezbytně nutné. Než si generické view vyžádá z databáze konkrétní objekt, je výraz Poll.objects.all() dostatečně upřesněn a SQL bude v ideálním případě vytahovat jediný řádek z tabulky.

Pokud se chcete dozvědět další podrobnosti, nahlédněte do dokumentace o databázovém API, které obsahuje samostatnou kapitolu o lenivé povaze objektů QuerySet.

V předchozích částech návodu jsme uvnitř šablon pracovali s kontextovými proměnnými poll and latest_poll_list. Generická view ale mají proměnné pojmenované obecněji, konkrétně object a object_list. Projděte si všechny své šablony a změňte proměnné latest_poll_list na object_list, a poll na object.

V této chvíli již můžete ze souboru polls/views.py smazat view index(), detail() a results(). Už je na nic nepotřebujeme — byly nahrazeny generickými view.

View vote() zachováme, stará se o zpracování odeslaných dat z formuláře. Musíme jej ale lehce upravit — ve volání render_to_response() přejmenujeme kontextovou proměnnou poll na object.

Poslední věcí kterou musíme upravit je způsob, jakým pracujeme s URL adresami. Ve view vote() používáme funkci reverse(). Protože generická view mohou být používána na více místech aplikace, nemůžeme se na ně odkazovat jménem funkce. Výraz upravíme tak, že bude obsahovat jméno URL pravidla:

return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))

Hotovo. Naše aplikace nyní využívá možností generických view. Spusťe si vývojový server a ověřte si její chování v praxi.

Další podrobnosti o generických view najdete v samostatné dokumentaci.

Již brzy

V tomto místě náš návod končí. V budoucnu bude doplněn o popisy dalších zajímavých částí Djanga:

  • Pokročilé metody zpracování formulářů
  • Využití RSS frameworku
  • Využití cache frameworku
  • Využití komentářového frameworku
  • Pokročilé vlastnosti administračního rozhraní: Práva
  • Pokročilé vlastnosti administračního rozhraní: Uživatelský JavaScript

Než k tomu dojde, sami se porozhlédněte na stránce Co si přečíst dál.