Open Source

Twoja pierwsza aplikacja w Django, część 3

Ta część tutoriala zaczyna się w miejscu, w którym skończyła się część druga. Będziemy kontynuować budowę aplikacji webowej - sondy - i skupimy się na tworzeniu interfejsu użytkownika - widokach.

Założenia

Widok (ang. view) jest typem strony w Twojej aplikacji Django który generalnie spełnia konkretną funkcję i posiada swój szablon. Na przykład w blogu możesz mieć następujące widoki:

  • strona główna - wyświetla kilka ostatnich wpisów;
  • strona z wpisem - strona zawierająca kompletny pojedynczy wpis;
  • archiwum, strona roczna - zawiera listę wszystkich miesięcy z wpisami z danego roku;
  • archiwum, strona miesięczna - j.w., ale z listą dni;
  • archiwum, strona jednodniowa - strona z listą wpisów z danego dnia;
  • dodawanie komentarza - pozwala na dodawanie komentarzy przez czytelników.

W naszej sondzie musimy stworzyć cztery widoki:

  • archiwum - zawiera kilka ostatnich sond;
  • strona z sondą - wyświetla samą sondę;
  • wyniki - podgląd wyników danej sondy;
  • głosowanie - pozwala oddać głos w wybranej sondzie.

W Django każdy widok jest realizowany przez funkcję pojedynczą Pythona.

Opracuj schemat adresów (URL)

Pierwszym krokiem do napisania widoków jest opracowanie struktury adresów. Wykonuje się to poprzez stworzenie modułu Pythona, zwanego URLconf. URLconf instruuje Django która napisana przez nas funkcja obsługuje który URL.

Kiedy Django dostanie żądanie udostępnienia którejś z podstron, system sprawdza ustawienie ROOT_URLCONF zawierające nazwę modułu. Django ładuje ten moduł i szuka zmiennej urlpatterns, która jest listą krotek (tupli) w formacie:

(wyrażenie regularne, nazwa funkcji Pythona [, opcjonalnie słownik])

Django porównuje URL do którego użytkownik zażądał dostępu z listą wyrażeń regularnych zaczynając od pierwszego na liście, i kończąc na pierwszym pasującym.

Kiedy znajdzie odpowiednie wyrażenie regularne, wywołuje pasującą funkcję Pythona z referencją do obiektu HTTPRequest jako pierwszym argumentem, wszystkie wychwycone z wyrażenia regularnego wartości jako argumenty kluczowe, i argumenty z opcjonalnego słownika (trzecia opcja w tupli).

Aby dowiedzieć się więcej o HTTPRequest zajrzyj do dokumentacji żądań i odpowiedzi (en) Django. Więcej informacji na temat URFconfs zobacz dokumentację URLconf (en).

Kiedy wywołałeś django-admin.py startproject mysite na początku pierwszej części tutoriala, stworzyłeś domyślne ustawienie URLconf w mysite/urls.py. Równocześnie ustawia zmienną ROOT_URLCONF aby wskazywała na plik:

ROOT_URLCONF = 'mysite.urls'

Czas na jakiś przykład. Wyedytuj plik mysite/urls.py żeby zawierał:

from django.conf.urls.defaults import *

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

Warto się temu przyjrzeć. Kiedy ktoś zażąda którejś podstrony z Twojego serwisu - na przykład “/polls/23/”, Django załaduje moduł mysite.urls ponieważ jest on wskazywany przez ustawienie ROOT_URLCONF. Tam wyszukuje zmienną urlpatterns i przeszukuje ją w poszukiwaniu pasującego wyrażenia regularnego. Kiedy je znajdzie - r'^polls/(?P<poll_id>\d+)/$' - ładuje wskazany moduł lub pakiet: mysite.polls.views.detail. To odpowiada funkcji detail() w mysite/polls/views.py. Na koniec wywołana zostaje znaleziona funkcja detail() w następujący sposób:

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

Paramter poll_id=23 pochodzi z (?P<poll_id>\d+). Używając nawiasów wokół wzorca (?P<nazwa>wzorzec) Django “wyłapuje” pasujący tekst do wzorca i przekazuje do wywołania funkcji jako nazwany parametr. Dla przykładu ?P<poll_id> definiuje nazwę, a \d+ jest wzorcem który wpasowuje się w ciąg cyfr (tworzy w tym przypadki liczbę).

Ponieważ URL patterns są wyrażeniami regularnymi, tak naprawdę nie ma żadnych ograniczeń z tym co za ich pomocą można uzyskać. I nie ma potrzeby dodawania URL cruft jak na przykład .php - dopóki nie chcesz się wykazać odrobiną humoru, w przypadku którego możesz zrobić tak:

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

Ale nie rób tego. To nic nie daje.

Zwróć uwagę, że te wyrażenie regularne nie przeszukują parametrów GET i POST. Nie przeszukują również nazwy domeny. Dla przykładu, w zapytaniu na adres http://www.example.com/myapp/, URLconf przeszukuje tylko /myapp/. W zapytaniu o adresie http://www.example.com/myapp/?page=3, przeszukiwanie będzie również tylko ciąg znaków /myapp/.

Jeśli potrzebujesz pomocy w stosowaniu wyrażeń regularnych, zajrzyj na stronę Wikipedii lub dokumentacji Pythona (en). Możesz także zainteresować się fantastyczną książką wydawnictwa O’Reilly “Matsering Regular Expressions” autorstwa Jefrrey’a Friedla.

Na koniec krótko o wydajności: te wyrażenia regularne są kompilowane podczas pierwszego załadowania modułu URLconf, dzięki czemu są bardzo szybkie.

Pierwszy widok

Mamy na razie stworzony poprawny URLconf, ale nie mamy jeszcze żadnego widoku. Zanim zaczniemy, trzeba sprawdzić czy Django poprawnie interpretuje nasze ustawienia.

Uruchom wbudowany w Django serwer WWW:

python manage.py runserver

Teraz idź do adresu “http://localhost:8000/polls/ w swojej przeglądarce. Powinieneś ujrzeć ładnie sformatowaną stronę błędu z następującym komunikatem:

ViewDoesNotExist at /polls/

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

Ten błąd wystąpił ponieważ nie stworzyłeś jeszcze funkcji index() w pliku mysite/polls/views.py.

Spróbuj także wywołać “/polls/23/”, “/polls/23/results/” i “/polls/23/vote/”. Wyświetlany błąd informuje Cię który widok Django próbował dopasować do danego wywołania (i oczywiście mu się nie udało, ponieważ jeszcze nie napisaliśmy żadnego widoku).

Czas na napisanie pierwszego widoku. Otwórz plik mysite/polls/views.py i wpisz tam następujący kod:

from django.http import HttpResponse

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

To jest najprostszy z możliwych widoków. Odwiedź teraz “/polls/” w przeglądarce - powinieneś zobaczyć tam powyższy tekst.

Teraz dodaj tam kolejny widok (funkcje widoku). Wygląda nieco inaczej, ponieważ pobiera argument (pamiętaj że argumenty są wyłapywane za pomocą wyrażeń regularnych z URLconf):

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

Zajrzyj ponownie pod adres “/polls/34/”. Wyświetli on jakiekolwiek wpisane przez Ciebie w adresie ID sondy.

Piszemy widoki, które coś robią

Każdy widok musi wykonać jedną z dwóch akcji: zwrócić obiekt HttpResponse zawierający treść żądanej strony, lub rzucenie wyjątku takiego jak Http404. Reszta jest pozostawiona Tobie.

Widok może odczytywać dane z bazy danych, ale nie musi. Możesz użyć systemu szablonów Django lub też innych. Może generować plik PDF, zwracać XML, stworzyć w locie archiwum ZIP, lub cokolwiek zechcesz, używając jakich chcesz bibliotek.

Jedyne czego oczekuje Django, to HttpResponse, lub wyjątek.

Ze względu na wygodę, będziemy się posługiwać wbudowanym w Django API dostępu do bazy danych, tak jak robiliśmy to pierwszej części tutoriala. To jest przykładowa implementacja widoku index(), który wyświetla ostatnie 5 pytań (sond) będących w systemie, oddzielone przecinkami w kolejności w jakiej zostały opublikowane:

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)

Występuje tam jeden problem, mianowicie: wygląd strony jest na sztywno zakodowany w funkcji widoku. Jeśli chciałbyś teraz zmienić wygląd strony, musisz poddać edycji kod widoku. Lepiej jest jednak użyć systemu szablonów (ang. templates) Django aby oddzielić logikę aplikacji od jej wyglądu:

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

Powyższy kod pobiera szablon “polls/index.html” i przypisuje mu wartości. Jako wartości podaje mu słownik mapujący nazwy zmiennych z szablonu na obiekty Pythona.

Odśwież stronę. Teraz powinieneś ujrzeć błąd:

TemplateDoesNotExist: Your TEMPLATE_DIRS settings is empty. Change it to
point to at least one template directory.

No tak, przecież nie mamy jeszcze żadnego szablonu. Najpierw musimy stworzyć katalog, gdzieś w systemie plików, do którego zawartości Django będzie miał dostęp (Django działa z uprawnieniami użytkownika który go uruchomił). Nie umieszczaj tego katalogu jako publicznie dostępnego ze względów bezpieczeństwa.

Zmień TEMPLATE_DIRS w Twoim pliku ustawień (settings.py) aby poinformować Django gdzie ma szukać szablonów - tak samo jak w drugiej części tutoriala w “upiększanie panelu administracyjnego”.

Kiedy już to zrobisz, załóż katalog polls w swoim katalogu z szablonami. Następnie stwórz tam plik index.html.Zauważ, że loader.get_template(``polls/index.html)`` odpowiada plikowi “[katalog szablonów]/polls/index.html” w systemie plików.

Wypełnij szablon “polls/index.html”:

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

Odśwież stronę w przeglądarce - powinieneś zobaczyć listę zawierającą jedną pozycję: “What’s up” z części pierwszej tutoriala.

Skrót: render_to_response()

Bardzo częstą wykonywaną czynnością jest załadowanie szablonu, wypełnienie go treścią i zwrócenie obiektu HttpResponse z wyrenderowanym szablonem. Django pozwala uprościć tę procedurę. Poniżej jest nowa wersje widoku index():

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})

Zwróć uwagę, że nie ma potrzeby ładowania modułu loader, Context czy HttpResponse.

Funkcja render_to_response() przyjmuje jako argumenty nazwę szablonu (pierwszy z argumentów) oraz słownik jako drugi argument. Zwraca HttpResponse podanego szablonu z uzupełnioną treścią.

Wyjątek 404

Zajmijmy się teraz stroną na której wyświetlana jest sonda. Oto potrzebny widok:

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})

Nowością jest tutaj rzucenie wyjątku django.http.Http404 jeżeli sondy o podanym ID nie ma w bazie danych.

Skrót: get_object_or_404()

Inną często wykonywaną czynnością jest próba pobrania obiektu przy użyciu get() i rzucenie wyjątku Http404 jeśli dany obiekt nie istnieje. Django pozwala na skrócenie tej operacji. Oto wersja widoku detail() z uwzględnieniem skrótu:

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})

Funkcja get_object_or_404() pobiera jako pierwszy argument model Django, a jako drugi - określoną liczbę argumentów kluczowych, które następnie są przekazywane do funkcji get(). Jeśli szukany obiekt nie istnieje, rzuca wyjątek Http404.

Założenia

Dlaczego używamy funkcji get_object_or_404() zamiast automatycznie przechwytywać wyjątki DoesNotExist na wyższym poziomie, lub też dlaczego nasze API automatycznie nie rzuca wyjątkiem Http404 zamiast DoesNotExist?

Ponieważ to mieszałoby warstwę modelu z warstwą widoku. Jednym z głównych założeń projektowych Django jest zachowanie luźnych powiązań pomiędzy należącymi do projektu elementami składowymi.

Istnieje także funkcja get_list_or_404() która działa podobnie jak get_object_or_404(), z tym wyjątkiem, że używa filter() zamiast get(). Rzuca wyjątkiem Http404 jeśli lista jest pusta.

Widok 404 (Page not found)

Kiedy zostanie wywołany wyjątek Http404 z jakiegoś widoku, Django ładuje wtedy specjalny widok przeznaczony do obsługi błędu 404. Jest on znajdowany poprzez ustawienie zmiennej handler404 która zawiera łańcuch znakowy w takim samym formacie jakiego używa zmienna URLconf. Sam widok 404 jest zwykłym widokiem, takim jak każdy inny.

Zazwyczaj nie będziesz potrzebował samodzielnie pisać tego widoku. Domyślnie, URLconf ma zawartą następującą linię:

from django.conf.urls.defaults import *

Zwróć uwagę na ustawienie handler404 w bieżącym module. Jak widzisz w django/conf/urls/defaults.py, handler404 zawiera domyślnie django.views.defaults.page_not_found.

Trzy informacje dotyczące widoku 404:

  • jest ładowany także wtedy, gdy Django nie udało się przypasować do żadnego wyrażenia regularnego zawartego w URLconf;
  • jeśli nie zmienisz domyślnego widoku dla wyjątku 404, co zalecamy, cały czas pozostaje Ci do stworzenia szablon dla tego widoku: utwórz plik szablonu o nazwie``404.html`` głównym katalogu szablonów, ten szablon domyślnie zostanie użyty do obsługi wszystkich błędów 404;
  • jeżeli masz w swoich ustawieniach DEBUG ustawione na True, wtedy widok 404 nigdy nie będzie używany, tylko zamiast niego zostanie wyświetlony ślad błędu.

Widok 500 (server error)

Podobnie jak w poprzednim punkcie, w URLconf możesz ustawić zmienną handler500 która wskazuje na widok który należy wywołać w przypadku błędów serwera. Błąd “Server error” zdarza się gdy są jakieś błędy w kodzie uniemożliwiające jego uruchomienie.

Używanie szablonów

Wróćmy do widoku detail() w naszej aplikacji. Jeśli przekażemy wartość poll do kontekstu nasz szablon (ang. template) może wyglądać tak:

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

System szablonów używa składni “kropkowej” w stylu Pythona aby uzyskać dostęp do zmiennych. W przykładzie {{ poll.question }} Django przeszukuje słownik obiektu poll. Gdy to się nie powiedzie, wyszukuje atrybuty - co w tym przypadku zakończy się powodzeniem. Jeśli atrybut nie zostałby znaleziony, Django spróbowałby wywołać metodę question() w tym obiekcie.

Wywoływanie metod działa w pętli {% for %}: poll.choice_set.all jest interpretowane jako kod Pythona poll.choice_set.all(), który zwraca listę obiektów Choice i jest odpowiednie dla iteracji w tagu {% for %}.

Zajrzyj do przewodnika po szablonach (en) jeśli chcesz uzyskać więcej informacji o szablonach.

Upraszczanie URLconfa

Pobaw się przez jakiś czas widokami wraz z systemem szablonów. Gdy edytujesz URLconf, możesz zauważyć że sporo części się w nim powtarza:

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

Na przykład część mysite.polls.views jest w każdym odwołaniu.

Ponieważ jest to dość częsty przypadek, szkielet URLconf dostarcza sposobu na skrócenie przedrostków. Możesz usunąć wszystkie przedrostki i wstawić je jako pierwszy argument do patterns():

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

Powyższy zapis funkcjonalnie jest identyczny z poprzednim, ale jest znacznie krótszy i czytelniejszy.

Dzielenie URLconfs

Skoro już przy tym jesteśmy, powinniśmy teraz rozdzielić adresy naszej sondy od konfiguracji całego projektu. Aplikacje Django powinny być rozdzielne - powinna być możliwość przeniesienia każdej z nich do innej instalacji Django przy minimalnym nakładzie sił.

Nasza sonda jest jak na razie ładnie oddzielona od samego projektu dzięki ścisłej strukturze katalogów która została utworzona przez python manage.py startapp, ale jedna jej część jest ustawiana razem z innymi ustawieniami Django: URLconf.

Konfigurowaliśmy ustawienia adresów w mysite/urls.py, ale adresy przygotowane dla aplikacji są specyficzne dla tej właśnie aplikacji, a nie dla całego projektu - dlatego przenieśmy je do katalogu aplikacji.

Skopiuj plik mysite/urls.py do mysite/polls/urls.py`. Teraz z ``mysite/urls.py usuń wszystkie adresy specyficzne dla sondy i wstaw tam wywołanie include():

(r'^polls/', include('mysite.polls.urls')),

Funkcja include() zwraca zawartość innego URLconfa. Zauważ że wyrażenia regularne nie posiadają kończącego $ (znak końca linii) ale posiada kończący ukośnik. Kiedy Django napotyka wywołanie include(), usuwa pasującą część adresu i przekazuje pozostałą część do dołączanego instrukcją include() URLconfa.

Oto co się dzieje gdy użytkownik wywoła adres /polls/34/:

  • Django znajdzie pasujące '^polls/'
  • usunie z wywołania pasujący tekst
  • przekaże pozostały tekst (34/) do URLconfa ‘mysite.polls.urls’ w celu dalszego przetworzenia.

Teraz, kiedy już mamy to rozłączone, musimy uprościć URLconf ‘mysite.polls.urls’ poprzez usunięcie początkowego “polls/” z każdej linii:

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

Ideą stojącą za stworzeniem funkcji include() oraz rozbiciem URLconfa było ułatwienie łączenia różnych aplikacji w całość. Teraz, kiedy sonda ma swojego własnego URLconfa, możemy ją łatwo umieścić w katalogu “/polls/”, lub “/polls_fun/”, “/content/polls/”, lub jakikolwiek innym, a nasza aplikacja ciągle będzie działać.

Wszystkie aplikacje muszą się zajmować swoimi relatywnymi URLami, nie absolutnymi.

Kiedy już będziesz dobrze orientował się w temacie tworzenia widoków, przeczytaj czwartą część tutoriala aby nauczyć się tworzenia prostych formularzy oraz ogólnych widoków (generic view).

Pytania/Wsparcie

Jeżeli zauważyłeś błędy w tłumaczeniu dokumentacji proszę zgłoś je nam.