První aplikace v Djangu, část 3

Tento návod začíná tam, kde kde skončila druhá část ukázkové aplikace. Budeme pokračovat v naší webové anketní aplikaci a zaměříme se na její veřejnou část – “views” (pohledy).

Filozofie

View je “druh” webové stránky, která má ve vaší aplikaci určitý úkol a výsledek své činnosti prezentuje prostřednictvím šablony. Například, v blogovací aplikaci by jste mohli najít tato views:

  • Úvodní stránka blogu – zobrazuje několik posledních záznamů.
  • Stránka s detailem záznamu – stránka s pevnou adresou (permalinkem) pro konkrétní záznam.
  • Archiv pro rok – zobrazí všechny měsíce roku s počtem záznamů pro každý z nich.
  • Archiv pro měsíc – zobrazí všechny dny měsíce s počty záznamů pro každý z nich.
  • Archiv pro den – zobrazí všechny záznamy pro zadaný den.
  • Komentář – zpracuje odeslání komentáře ke konkrétnímu záznamu.

V naší anketní aplikaci budeme mít tyto čtyři views:

  • Archiv anket – zobrazí několik posledních anket.
  • Detail ankety – zobrazí anketní otázku bez výsledků, ale s formulářem pro hlasování.
  • Stránka s výsledky – zobrazí výsledky pro určitou anketu.
  • Hlasování – zpracuje hlasování pro konkrétní volbu v anketě.

V Djangu je každé view zrealizováno jednoduchou funkcí v Pythonu.

Vytvořte si svoje URL

První krokem při psaní views je definování struktury URL adres. Toho docílíme vytvořením jednoduchého Python modulu nazývaného URLconf. Soubory URLconf se starají o přiřazení URL adresy k určitému kódu.

Když si uživatel vyžádá konkrétní stránku, Django se podívá do nastavení ROOT_URLCONF, které obsahuje řetězec zapsaný v tečkované syntaxi Pythonu. Django tento modul načte a hledá v něm proměnnou urlpatterns, což je sekvence n-tic ve formátu:

(regulární výraz, Python callback funkce [, nepovinný slovník])

Django začne s prvním regulárním výrazem a postupně porovnává vyžádanou URL adresu se všemi dalšími výrazy v seznamu, dokud nenajde shodu.

Když je shoda nalezena, Django zavolá Python callback funkci, a jako první argument předá objekt HttpRequest. Veškeré “zachycené” hodnoty z regulárního výrazu předá jako jako keyword argumenty (společně s proměnnými z nepovinného slovníku — pokud byly nějaké definovány; viz třetí položka v urlpatterns).

Podrobnější informace o objektech HttpRequest naleznete v Request and response objects, detaily o souborech URLconf v URL dispatcher.

Když jste na začátku prvního návodu spouštěli django-admin.py startproject mysite, vytvořil se výchozí URLconf v mysite/urls.py. Volba ROOT_URLCONF (v settings.py) byla automaticky nastavena tak, aby ukazovala právě na tento soubor:

ROOT_URLCONF = 'mysite.urls'

Upravte nyní mysite/urls.py:

from django.conf.urls.defaults import *

from django.contrib import admin
    admin.autodiscover()

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

Rozeberme si úpravu podrobněji. Když si někdo vyžádá vaši stránku — řekněme “/polls/23/”, Django načte tento modul, protože na něj ukazuje volba ROOT_URLCONF. Hledá proměnnou s názvem urlpatterns a prochází po pořádku regulární výrazy. Když najde shodující se regulární výraz — r’^polls/(?P<poll_id>\d+)/$’ — tak načte přiřazený balíček/modul: mysite.polls.views.detail. To odpovídá funkci detail() v mysite/polls/views.py. Nakonec funkci detail() zavolá tímto způsobem:

detail(request=<HttpRequest object>, poll_id='23')

Část poll_id=‘23’ přichází z pravidla (?P<poll_id>\d+). Kulaté závorky “zachytí” text ze shodujícího se výrazu a pošlou jej jako argument do view funkce; ?P<poll_id> definuje název, kterým se zachycená část pojmenuje; a \d+ je regulární výraz pro sekvenci číslic (číslo).

Protože URL pravidla jsou definována za pomocí regulárních výrazů, můžete s jejich pomocí dělat téměř cokoliv. S klidem zapomeňte na ty hromady adres končících výrazem .php — pokud ovšem nemáte špatný smysl pro humor; v tom případě by jste mohli napsat něco jako:

(r'^polls/latest\.php$', 'mysite.polls.views.index'),

Ale nedělejte to. Je to hloupé.

Pamatujte, že tyto regulární výrazy neprohledávají parametry GET, POST, a ani názvy domén. Například, při požadavku na http://www.example.com/myapp/, bude URLconf hledat pravidlo pro myapp/. Při požadavku na http://www.example.com/myapp/?page=3, bude opět hledat myapp/.

Pokud potřebujete pomoc s regulárními výrazy, podívejte se do Wikipedie a dokumentace Pythonu. Také kniha O’Reilly “Mastering Regular Expressions” od Jeffreyho Friedla je fantastická.

Nakonec poznámka k výkonu: regulární výrazy jsou kompilovány pouze při prvním spuštění URLconf. Jsou báječně rychlé.

Napište své první view

Zatím jsme žádné view nevytvořili — definovali jsme pouze seznam pravidel v souboru URLconf. Pojďme se přesvědčit, že Django na obsah souboru URLconf správně reaguje.

Spusťte vývojový server Djanga:

python manage.py runserver

Ve vašem webovém prohlížeči navštivte adresu “http://localhost:8000/polls/“. Měla by se Vám zobrazit přítulně namodralá chybová stránka s následující zprávou:

ViewDoesNotExist at /polls/

Tried index in module mysite.polls.views. Error was: 'module'
object has no attribute 'index'

Uvedená chyba nastala kvůli tomu, že jsme ještě nevytvořili funkci index() v modulu mysite/polls/views.py.

Zkuste navšívit i adresy “/polls/23/”, “/polls/23/results/” a “/polls/23/vote/”. Chybové zprávy vám řeknou, které views se Django poukoušelo najít (a nenašlo, protože ještě nejsou napsány).

Je na čase napsat první view. Otevřete soubor mysite/polls/views.py a vložte do něj následující Python kód:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the poll index.")

Toto je nejjednodušší možné view. Navštivte v prohlížeči adresu “/polls/” a měli by jste uvidět výše uvedený text.

Pojďme napsat další views. Jsou trošku jiná, protože přijímají argument (který je přijat z adresy pomocí regulárního výrazu definovaného v souboru URLconf):

def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

def results(request, poll_id):
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

def vote(request, poll_id):
    return HttpResponse("You're voting on poll %s." % poll_id)

Podívejte se na “/polls/34/”. Na stránce se vám zobrazí takové ID, jaké

Podívejte se na “/polls/34/”. Navštívením této adresy dojde ke spuštění funkce detail() a zobrazení ID, které zadáte v URL. Zkuste navštívit i další adresy “/polls/34/results/” a “/polls/34/vote/” — ukážou se vám stránky s výsledky a hlasováním.

Napište views, které opravdu něco dělají

Každé view je zodpovědné za jednu ze dvou věcí: vrácení objektu HttpResponse obsahujícího vyžádanou stránku, nebo vyvolání vyjímky, např. Http404. Zbytek je na vás.

Vaše view může číst záznamy z databáze, nebo také ne. Může používat šablonový systém Djanga — nebo šablonový systém třetích stran napsaný v Pythonu — nebo také nemusí používat žádný. Může vytvářet PDF soubor, vypisovat XML, vytvářet za chodu soubory ZIP, cokoli budete chtít, za použití jakýchkoliv Python knihoven.

Jediné, co Django chce, je HttpResponse. Nebo vyjímku.

Protože jde o běžnou praxi, využijeme nyní databázové API Djanga, které jsme probrali v první části. Následující view index() zobrazí posledních pět otázek z anket, oddělených čárkami a seřazených podle data publikování:

from mysite.polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    output = ', '.join([p.question for p in latest_poll_list])
    return HttpResponse(output)

Toto řešení má ale jeden problém: Vzhled stránky je “zadrátovaný” přímo do kódu view. Když budete chtít změnit podobu stránky, budete muset upravovat Python kód. Lepším řešením je oddělit vzhled od kódu s pomocí šablonového systému Djanga:

from django.template import Context, loader
from mysite.polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    t = loader.get_template('polls/index.html')
    c = Context({
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(t.render(c))

Tento kód načte šablonu nazvanou “polls/index.html” a předá jí kontext, což je slovník, který mapuje šablonové proměnné na objekty Pythonu.

Znovu ve svém prohlížeči načtěte stránku. Chybová hláška se změní:

TemplateDoesNotExist at /polls/
polls/index.html

Aha. Ještě jsme nenapsali šablonu! Pojďme tedy na to. Nejprve vytvořte adresář, kam bude mít Django přístup. (Django běží pod uživatelským jménem vašeho serveru.) Nedávejte jej ale přímo pod kořenový adresář webového serveru. Není dobré zpřístupňovat šablony veřejnosti. Když už pro nic jiného, tak pro pocit větší bezpečnosti vaší aplikace. Upravte TEMPLATE_DIRS v souboru settings.py tak, aby Django vědělo, kde má hledat šablony — stejně jako jste to udělali v druhé části návodu (viz kapitola “Upravte si kompletní vzhled prostředí”).

Dále vytvořte podadresář polls a umístěte do něj soubor index.html. Poznámka: výše uvedený kód loader.get_template(‘polls/index.html’) je nasměrován na “[template_directory]/polls/index.html”.

Do šablony vložte následující:

{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Načtěte znovu stránku ve svém prohlížeči a uvidíte seznam s odrážkami obsahující anketu “What’s up” z první části návodu. Jednotlivé odkazy povedou na stránku s detailem ankety.

Zjednodušení: render_to_response()

Běžný postup je načíst šablonu, naplnit kontext a vrátit objekt HttpResponse s vykresleným obsahem šablony. Django pro tyto případy nabízí praktickou zkratku:

from django.shortcuts import render_to_response
from mysite.polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

Poznámka: Pokud takto upravíme všechna view, už nemusíme importovat loader, Context a HttpResponse.

Funkce render_to_response() přijímá jako první argument jméno šablony a jako druhý (nepovinný) argument slovník. Výstupem z funkce je objekt HttpResponse s šablonou vykreslenou podle dodaných kontextových proměnných.

Vyvolání 404

Teď se podíváme na view pro detail ankety — stránku, která zobrazuje otázku dané ankety:

from django.http import Http404
# ...
def detail(request, poll_id):
    try:
        p = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404
    return render_to_response('polls/detail.html', {'poll': p})

Tady je kód zrealizován následovně: view vyvolá výjímku Http404, pokud anketa s požadovaným ID neexistuje.

Za chvíli se budeme podrobněji věnovat tomu, čím naplnit soubor polls/detail.html, aby naše view pěkně fungovalo. Pokud jej ale chcete rychle zprovoznit, vložte do něj:

{{ poll }}

(pro tentokrát to stačí).

Zkratka: get_object_or_404()

Běžný postupem je vytáhnout objekt z databáze pomocí volání get(), a v případě že neexistuje vyvolat vyjímku Http404. Django nabízí příjemnou zkratku:

from django.shortcuts import render_to_response, get_object_or_404
# ...
def detail(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p})

Funkce get_object_or_404() přijímá jako první argument model Djanga, a na dalších pozicích libovolný počet argumentů , které se předají funkci get(). Pokud se podle dodaných parametrů objekt nenalezne, vyvolá se vyjímka Http404.

Zamyšlení

Proč používáme pomocnou funkci get_object_or_404() namísto automatického zachytávání vyjímek ObjectDoesNotExist na vyšší úrovni, anebo proč model rovnou nevyvolá vyjímku Http404 namísto ObjectDoesNotExist?

Protože by se vrstva modelu a pohledu (views) propojila vazbou. Jedním z hlavních cílů Djanga je udržovat podobné vazby co nejslabší (protože pak je mnohem snadnější nahradit určitou funkční část jinou).

Existuje také funkce get_list_or_404(), která funguje stejně jako get_object_or_404() — s tím rozdílem, že používá metodu filter() namísto get(). Funkce get_list_or_404() vyvolá vyjímku Http404 pokud je seznam prázdný.

Napište view pro 404 (stránka nenalezena)

Když je vyvoláno Http404, Django načte speciální view určené pro zpracovávání vyjímek 404. Toto view se hledá v proměnné handler404, což je řetězec v tečkované syntaxi Pythonu — ve stejném formátu jaký používají URLconf callback funkce. Na view 404 není nic zvláštního: Je to prachobyčejné view.

Psaním view pro 404 se běžně nemusíte zabývat. Soubory URLconf standardně začínají řádkem:

from django.conf.urls.defaults import *

Tímto způsobem dojde automaticky k nastavení handler404. Jak se můžete sami přesvědčit, v souboru django/conf/urls/defaults.py je handler404 standardně nastaven na django.views.defaults.page_not_found().

Poslední čtyři poznámky k view 404:

  • Pokud je DEBUG nastaveno na True (v souboru settings.py), pak vaše view 404 nebude nikdy použito (a šablona 404.html nebude nikdy vykreslena). Místo něj se vám ukáže “traceback”.
  • Pokud Django nenajde shodu s regulárními výrazy v souboru URLconf, je vyvoláno view 404.
  • Pokud nedefinujete vlastní view pro 404 — a použijete výchozí (což se doporučuje) — stejně musíte udělat jednu věc: Vytvořit šablonu 404.html v kořenovém adresáři šablon. Výchozí view 404 použije tuto šablonu pro všechny chyby 404.
  • Pokud je volba DEBUG (v souboru settings.py) nastavena na False a nemáte vytvořenu šablonu 404.html, vyvolá se vyjímka Http500. Moudré vědmy proto radí: nezapoměňte si vytvořit šablonu 404.html!

Napište view 500 (chyba serveru)

Podobně může být v souborech URLconf definován handler500, ukazující na view, které se volá v případě chyby serveru. Chyby serveru mohou nastat, pokud se ve vašem kódu objeví runtime chyby.

Používejte šablonový systém

Vraťme se k view detail() z naší anketní aplikace. S kontextovou proměnnou poll (obsahující konkrétní objekt modelu Poll) by šablona “polls/detail.html” mohla naložit například takto:

<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }}</li>
{% endfor %}
</ul>

Šablonový systém používá “tečkovací” syntaxi pro přístup k proměnným objektu. K výrazu {{ poll.question }} Django nejprve přistoupí tak, jako by se jednalo o slovník, ve kterém hledá patřičný klíč. Pokud se mu to nepovede, pokusí se v objektu nalézt atribut — což v tomto případě funguje. Naposledy se pokusí zavolat metodu question() na objektu poll.

Volání metody se uskutečňuje ve smyčce {% for %}: výraz poll.choice_set.all je brán jako Python kód poll.choice_set.all(). Ten vrátí iterátor nad objekty voleb (Choice), se kterým následně může tag {% for %} bez problémů pracovat. Více informací o šablonách naleznete v jejich průvodci.

Zjednodušení souborů URLconf

Zkuste si teď sami pohrát s vytváření views a šablon, ať s jejich používání dostanete cvik. Během toho si možná všimnete, že v souboru URLconf se zbytečně opakují některé shodné části:

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

Abychom byli přesní, v každém callbacku se objevuje mysite.polls.views.

Protože jsou podobné situace docela běžné, framework pro URLconf nabízí pro neustále se opakující předpony zjednodušení. Shodné předpony můžete vyjmout a dosadit je jako první argument ve funkci patterns(). Třeba takto:

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

Z funkčního hlediska jde o identický kód. Druhý způsob je ale přehlednější.

Oddělování souborů URLconf

Když už jsme u toho, měli bychom také oddělit URL adresy naší aplikace od konfigurace Django projektu. Django aplikace mají být zásuvné — což znamená, že jakákoliv aplikace by se měla dát přenést do jiné instalace Djanga s minimálními komplikacemi.

Naše aplikace je na dobré cestě, díky striktní adresářové struktuře vytvořené pomocí python manage.py startapp, ale jedna její část je napevno spojena s nastavením Djanga: URLconf.

URL adresy jsme upravovali v souboru mysite/urls.py, ale design URL adres by měl být specifický pro aplikaci a ne pro celou Django instalaci — takže přesuneme URL adresy do adresáře s aplikací.

Zkopírujte soubor mysite/urls.py do mysite/polls/urls.py. Ze souboru mysite/urls.py odstraňte URL adresy specifické pro anketní aplikaci a vložte místo nich include():

# ...
    urlpatterns = patterns('',
        (r'^polls/', include('mysite.polls.urls')),
        # ...

include() odkazuje na jiný URLconf. Všimněte si, že regulární výraz nekončí znakem $ (znak pro konec řetězce), ale lomítkem. Když se Django setká s include(), oddělí část URL, která se do té doby shodovala a zbytek odešle do odkazovaného souboru URLconf pro další zpracování.

Kdyby uživatel zadal adresu “/polls/34/”, stalo by se toto:

  • Django najde shodu v ‘^polls/’
  • Pak vyjme shodující se text (“polls/”) a odešle jeho zbytek — “34/” — do souboru ‘mysite.polls.urls’ pro další zpracování.

Teď ještě musíme odstranit “polls/” z každého řádku URLconf v souboru ‘mysite.polls.urls’ a smazat řádky registrující administrační rozhraní:

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'),
)

Myšlenka rozddělení souboru URLconf a použití include() spočívá tom, že zjednoduší zapojení URL adres kdekoli. Teď, když mají ankety svůj vlastní URLconf, mohou být umístěny za “/polls/”, nebo “/fun_polls/”, nebo “/content/polls/”, nebo za jakýkoli jiný URL prefix a aplikace bude stále fungovat.

Aplikace se stará jen o relativní adresy a ne o absolutní.

Až budete se svými views spokojeni, přečtěte si čtvrtou část tohoto návodu, kde se dozvíte něco o zpracování jednoduchých formulářů a o generických views.