Django: Prezentace dat podruhé

Používání generických pohledů, které jsme si představili minule, je pohodlné, ale u složitějších prezentací dat si s nimi bohužel nevystačíme vždy. Proto se v dnešním díle podíváme na několik netriviálních příkladů pohledů a šablon.

Seriál: Hrajeme si s Djangem (16 dílů)

  1. Django: Úvod a instalace 14.8.2009
  2. Django: Nastavení projektu a první pokusy 21.8.2009
  3. Django: Databázový model 28.8.2009
  4. Django: Databázový model podruhé 4.9.2009
  5. Django: Administrace 11.9.2009
  6. Django: Prezentace dat 18.9.2009
  7. Django: Prezentace dat podruhé 25.9.2009
  8. Django: Zpracovávání formulářů 2.10.2009
  9. Django: Autentizace a autorizace 9.10.2009
  10. Django: Nahrávání souborů 16.10.2009
  11. Django: Zabudované aplikace 23.10.2009
  12. Django: Rozšiřování možností Djanga 30.10.2009
  13. Django: Internacionalizace 6.11.2009
  14. Django: Nasazování projektu 13.11.2009
  15. Django: Kešování a škálování 20.11.2009
  16. Django: Závěr 27.11.2009

Jak si jistě dobře vzpomínáte, náš ukázkový projekt obsahoval kromě jiného i tabulku s filmy. Katalog filmů je klíčovou součástí webových stránek videopůjčovny. Sestává se ze seznamu položek a detailů jednotlivých titulů.

Seznam položek

Idea pro vytvoření seznamu filmů je taková: protože by katalog výhledově obsahoval i tisíce položek, uživatel je musí mít možnost jednoduše vyhledávat podle názvů titulu a případně listovat v abecedním seznamu. Po zadání hledaného výrazu do formuláře nebo kliknutí na počáteční písmeno názvu se zobrazí odpovídající záznamy. Opět začneme nejprve šablonou, kterou uložíme do souboru templates/film_list.html:

{% extends "base.html" %}

{% block content %}
<h1>{% block title %}Nabídka titulů{% endblock %}</h1>

<div class="letters">
{% for letter in letters %}
  {% ifequal letter prefix|upper %}
    {{ letter }}
  {% else %}
    <a href="/nabidka/{{ letter|lower }}/">{{ letter }}</a>
  {% endifequal %}
{% endfor %}
</div>

{% if films %}
  <table>
  {% for film in films %}
    <tr>
    <td><a href="/nabidka/detail/{{ film.id }}/">{{ film.name_czech }}</a></td>
    <td>{{ film.name_english }}</td>
    <td>({{ film.year }})</td>
    </tr>
  {% endfor %}
  </table>
{% else %}
  <form action="/nabidka/" method="GET">
  <input type="text" name="search" value="{% if search %}{{ search }}{% endif %}">
  <input type="submit" value="Vyhledat">
  </form>

  {% if prefix or search %}
  <p>Nenalezeno.</p>
  {% endif %}
{% endif %}
{% endblock %}

Prvním cyklem for (7. řádek) vypíšeme počáteční písmena názvů filmů a přidělíme jim příslušné odkazy. Podmínkou ifequal zajistíme, aby se odkaz nepřidal, pokud je uživatel na stránce s daným písmenem. Jsou zde použity filtry lower a upper, které z daného písmena vytvoří minusku či verzálku (tj. malé či velké písmeno). Jestliže našemu zadání odpovídají nějaké filmy, vypíšeme je druhým cyklem for (18. řádek). V opačném případě zobrazíme vyhledávací formulář (27. řádek) a případně chybovou hlášku. Mimochodem, v Djangu 1.1 a novějším by zde šel použít namísto kombinace if a for přehlednější zápis for-empty.

Pozornější z vás si mohli povšimnout, že do formuláře vyhledávání vypisujeme zadaný řetězec, takže by na tomto místě mohl nastat potenciální XSS útok. Bezpečnost je v Djangu brána velmi vážně a všechny proměnné v šablonách jsou automaticky escapovány. Významně se tak snižuje riziko tohoto typu útoku. V případě potřeby můžeme automatické escapování vypnout pomocí značky autoescape.

Kromě šablony potřebujeme ještě pohled, kterým předáme šabloně data. Pro zobrazení katalogu nepoužijeme generický pohled, ale napíšeme si svůj vlastní, který nám umožní vyhledávání a listování podle písmen. Otevřeme si soubor s pohledy video_store/views.py a vložíme do něj následující kód:

from models import Film
from django.shortcuts import render_to_response
from django.template import RequestContext

def film_list(request, prefix=None):
    search = request.GET.get('search')

    if prefix is not None:
        films = Film.objects.filter(name_czech__istartswith=prefix)
    elif search is not None:
        films = Film.objects.filter(name_czech__icontains=search)
    else:
        films = None

    letters = [chr(c) for c in range(ord('A'), ord('Z') + 1)]

    return render_to_response('film_list.html', {'search': search, 'prefix': prefix, 'films': films, 'letters': letters},
                              context_instance=RequestContext(request))

Námi definovaný pohled film_list má kromě objektu request další parametr, nazvaný prefix. Pomocí něj můžeme pohledu předat počáteční písmeno názvu filmu. Pokud žádné písmeno nevybereme, použije se výchozí hodnota None. Jak uvidíme za chvíli, předávání parametrů má na starosti mapování URL. Na dalším řádku z objektu request vybíráme případný GET parametr search, který nám posílá vyhledávací formulář. Tento parametr se také zobrazí v URL, ovšem až za otazníkem, tedy např. v URL http://127.0.0.1:8000/nabidka/?search=term se přiřadí proměnné search řetězec 'term'. Metoda get zajistí, že pokud daný klíč ve slovníku neexistuje, místo vyhození výjimky je vrácena hodnota None.

Poté z databáze vybíráme filmy podle zadaných kritérií. Opět někteří z vás zbystřili a ptají se, jestli je daný kód chráněn proti SQL injekci. Odpověď zní ano, ORM Djanga se o to postará a vstupní data automaticky ošetří. Dále jsme do proměnné letters přiřadili pomocí generátoru seznamu písmena z rozsahu AZ. Stejného výsledku bychom docílili použitím „obyčejného“ cyklu for nebo vypsáním seznamu písmen ručně, tohle je jenom úspornější zápis.

Na posledním řádku posíláme šabloně naše parametry. V případech jako je tento si můžeme ušetřit práci použitím funkce locals, která z lokálních proměnných sestaví slovník. Požadavkem je, že se proměnné budou jmenovat stejně jako klíče slovníku, což je tady splněno. Poslední řádek bychom tak mohli nahradit za:

return render_to_response('film_list.html', locals(), context_instance=RequestContext(request))

… a dosáhli bychom stejného výsledku. Teď nám zbývá správně nastavit URL. Otevřeme si soubor urls.py v adresáři projektu a upravíme první výskyt proměnné urlpatterns:

urlpatterns = patterns('hrajeme_si',
    (r'^time/$', 'pokus.views.current_time'),
    (r'^nabidka/$', 'video_store.views.film_list'),
    (r'^nabidka/([a-z])/$', 'video_store.views.film_list'),
)

Vidíme zde dvojnásobné přiřazení URL k našemu pohledu. V prvním případě se pohledu nebude předávat žádný argument, ve druhém ano, podle použitého regulárního výrazu. Když do prohlížeče zadáme adresu http://127.0.0.1:8000/nabidka/, použije se první definice, proměnná prefix bude mít hodnotu None a nezobrazí se žádné záznamy.

Django 7

V případě, kdy přejdeme např. na adresu http://127.0.0.1:8000/nabidka/h/, bude použita druhá definice, protože písmeno h vyhovuje regulárnímu výrazu [a-z], takže bude předáno našemu pohledu a vypíšou se odpovídající záznamy.

Django 7

Jestliže URL obsahuje více regulárních výrazů, je vhodné si je pojmenovat. V našem případě bychom místo ([a-z]) napsali (?P<prefix>[a-z]).

Pokud zadaná adresa danému regulárnímu výrazu neodpovídá, zobrazí se chyba 404 (nenalezeno). Regulární výraz tak ošetří všechny nečekané vstupy. Zbývá poznamenat, že je tento příklad dost zjednodušený, názvy filmů obyčejně začínají i na jiné znaky, kupříkladu čísla.

Detail konkrétní položky

Když už máme hotové hledání a procházení seznamem filmů, podíváme se ještě, jak by vypadalo detailní zobrazení titulu. K jednotlivým záznamům se dostaneme pomocí sloupce id, stačí správně namapovat URL a vybrat odpovídající položku z tabulky. Nejprve si však ukážeme odpovídající šablonu (templates/film_detail.html):

{% extends "base.html" %}

{% block content %}
<h1>{% block title %}{{ film.name_czech }} ({{ film.year }}){% endblock %}</h1>

<table>
<tr>
<th>Původní název</th>
<td>{{ film.name_original }}</td>
</tr>
<tr>
<th>Cena za půjčení</th>
<td>{{ film.price }}&nbsp;Kč</td>
</tr>
<tr>
<th>Formát</th>
<td>{{ film.format }}</td>
</tr>
<tr>
<th>Dostupné v provozovnách</th>
<td>
{% for store in film.store.all %}
  {{ store }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
</table>

<p>{{ film.description }}</p>

<p><a href="/nabidka/{{ film.name_czech|first|lower }}/">Zpět do nabídky</a></p>
{% endblock %}

Začátek šablony je celkem fádní. V nadpisu stránky zobrazíme název filmu a rok natočení, v tabulce pod tím další údaje. Mezi nimi je i atribut store, který je ve vztahu many-to-many s modelem Store, takže musíme pomocí cyklu for zobrazit jednotlivé navázané položky. Všimněte si, že v tomto cyklu data vybíráme z QuerySetu film.store.all, ne z atributu film.store. Při oddělování položek čárkou je využita zabudovaná proměnná forloop. Na předposledním řádku šablony se nachází odkaz pro návrat do seznamu titulů se stejným počátečním písmenem. Odkaz generujeme z názvu aktuálně vybraného filmu. Filtrem first vybereme první písmeno z názvu, které pak převedeme na minusku pomocí dalšího filtru, lower.

V tomto případě by se již dalo uvažovat o použití generického pohledu, konkrétně list_detail.object_detail. V jednom z dalších dílů však budeme šablonu upravovat a doplňovat další funkcionalitu. Takže si teď připíšeme na konec souboru video_store/views.py vlastní pohled:

from models import FORMAT_CHOICES
from django.http import HttpResponseRedirect

def film_detail(request, film_id):
    try:
        film = Film.objects.filter(id=film_id)[0]
    except IndexError:
        return HttpResponseRedirect('/nabidka/')

    film.format = FORMAT_CHOICES[film.format][1]

    return render_to_response('film_detail.html', {'film': film}, context_instance=RequestContext(request))

Dodatečný parametr film_id určuje požadované id záznamu. Pomocí tohoto parametru budeme z databáze vybírat náš objekt (lze použít i metodu get). Tento výběr je celý obalený blokem try-except. Větev except IndexError odchytí stav, kdy daný záznam nebude nalezen, a QuerySet bude tím pádem prázdný a dotaz bude přesměrován na stránku se seznamem filmů. Ještě bychom měli zajistit, aby se atribut format nezobrazoval jako číslo, ale jako text. Tento text je uložen v n-tici FORMAT_CHOICES (viz soubor video_store/models.py), ze které jej jednoduše získáme podle indexu.

K dokončení stačí přidat další položku do souboru urls.py a jsme hotovi:

urlpatterns = patterns('hrajeme_si',
    (r'^time/$', 'pokus.views.current_time'),
    (r'^nabidka/$', 'video_store.views.film_list'),
    (r'^nabidka/([a-z])/$', 'video_store.views.film_list'),
    (r'^nabidka/detail/(d+)/$', 'video_store.views.film_detail'),
)

Regulární výraz d+ zastupuje posloupnosti jedné a více číslic, což odpovídá jakékoliv hodnotě id. Přejdeme-li v prohlížeči na libovolný detail filmu, uvidíme očekávaný výpis.

Django 7

Související odkazy

V následujícím díle zjistíme, jak v Djangu bezbolestně generovat a zpracovávat formuláře.

Autor je dlouhodobým studentem Fakulty informatiky, webový nadšenec a programátor — nejraději programuje v jazycích Haskell a Python.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 3

Přehled komentářů

Jakub Vrána Chybné přesměrování
Pavel Dvořák Re: Chybné přesměrování
Jakub Vrána Re: Chybné přesměrování
Zdroj: https://www.zdrojak.cz/?p=3087