Tworzymy przykładowego bloga - część 2

Druga część wprowadzająca do Django. Rozbudowa aplikacji wiadomości o szablony i stronicowanie.

Teraz stworzymy działającą aplikację wiadomości - listującą najnowsze wpisy, czy wyświetlającą wybrany wpis. W tym artykule zaprezentuję kolejną porcję możliwości Django jak i sposób tworzenia aplikacji w tym frameworku (dostępne rozwiązania i sposób ich użycia).

Widok ze stronicowaniem wpisów

W poprzednim artykule stworzyliśmy prosty widok listujący wszystkie wiadomości. Wykorzystaliśmy do tego prosty widok-funkcję:

def index(request):
    news = News.objects.all().order_by('-posted_date')
    return render_to_response('index.html',
            {'news': news},
            context_instance=RequestContext(request))

W nowszych wersjach Django dostępne są także widoki-klasy (class based views). Jest to zbiór klas, na bazie których można szybciej tworzyć widoki określonego typu - np. widok listujący, widok szczegółowy pojedynczego wpisu, widok dodawania, czy edycji itd. Można oczywiście osiągnąć tą samą funkcjonalność używając widoku-funkcji, ale może to być mniej czytelne niż w przypadku widoków-klas.

Jako widok listujący wpisy użyjemy ListView:
# -*- coding: utf-8 -*-
from django.views import generic

from news import models


class NewsList(generic.ListView):
    model = models.News
    paginate_by = 10
    context_object_name = 'news_list'

news_list = NewsList.as_view()

Dziedzicząc klasę ListView otrzymujemy całą potrzebną funkcjonalność. Wystarczy podać model. Dodatkowo określiłem paginate_by - ilość wiadomości wyświetlanych na jednej stronie, oraz context_object_name zawierającą nazwę zmiennej, pod którą lista wiadomości będzie dostępna w szablonie.

Szablon index.html także ulega zmianie. Domyślna nazwa szablonu dla tego widoku to news/news_list.html - i na taką zmieniłem nazwę/położenie tego pliku. Sam szablon zyskuje stronicowanie:

<h1>Moja Strona </h1>
{% for entry in news_list %}
    <h3>{{ entry.title }}</h3>
    {{ entry.text|safe }}<br />
    {{ entry.posted_date|date:"Y.m.d H:i" }}
{% endfor  %}


{% if is_paginated %}
    <hr>
    {% if page_obj.has_next %}
        <a href="./?page={{ page_obj.next_page_number }}">starsze</a>
    {% endif %}
    {% if page_obj.has_previous %}
        <a href="./?page={{ page_obj.previous_page_number }}">nowsze</a>
    {% endif %}
{% endif %}

Zmienna is_paginated będzie ustawiona na True, jeżeli będzie więcej niż jedna strona stronicowania. Obiekt page_obj odpowiada za stronicowanie i możemy z niego wyciągnąć informacje takie jak poprzedni i następny numer strony.

Odnośniki do widoków

Linki do stronicowania są ustawione na sztywno w szablonie. Jeżeli zmienimy w urls.py przypisany widokowi adres URL to te linki przestaną działać. Framework dostarcza nam system generowania adresów URL na podstawie podanych nazw widoków.

W blog/urls.py mamy zmapowany nasz widok:

url(r'^/?$', 'news.views.news_list'),
Dla ułatwienia nadam mu krótszą nazwę:
url(r'^/?$', 'news.views.news_list', name='news-list'),
W szablonie możemy użyć teraz taga url by otrzymać dynamicznie generoway adres URL do widoku:
{% if page_obj.has_next %}
    <a href="{% url "news-list" %}?page={{ page_obj.next_page_number }}">starsze</a>
{% endif %}
{% if page_obj.has_previous %}
    <a href="{% url "news-list" %}?page={{ page_obj.previous_page_number }}">nowsze</a>
{% endif %}
Stronicowanie obsługuje także numer strony podany w samym adresie URL (a nie przez zmienną GET). Wystarczy dodać odpowiednią regułę do urls.py:
url(r'^(?P<page>[0-9]+)/$', 'news.views.news_list', name='news-list'),
url(r'^/?$', 'news.views.news_list', name='news-list'),
Co poprawi nasz kod HTML do postaci:
<a href="{% url "news-list" page_obj.next_page_number %}">starsze</a>
Po nazwie odnośnika podajemy argumenty. W tym przypadku będzie to numer strony. Stosowanie taga "url", czy funkcji "reverse" w kodzie Pythona jest gorąco zalecane. Stosowanie sztywnych adresów URL prędzej, czy później się zemści.

Widok szczegółowy

Mamy widok listujący wiadomości. Zróbmy teraz widok wyświetlający jeden konkretny wpis. Użyjemy do tego innego widoku-klasy - DetailView:

class NewsDetailView(generic.DetailView):
    model = models.News

news_detail = NewsDetailView.as_view()
Kod jest naprawdę prosty. Dziedziczymy DetailView, podajemy model i tyle. W urls.py mapujemy widok na adres:
url(r'^news/(?P<slug>[\w\-_]+)/$', 'news.views.news_detail', name='news-detail'),
W szablonie news/news_list.html dodajemy link:
<h3><a href="{% url "news-detail" entry.slug %}">{{ entry.title }}</a></h3>
Oraz tworzymy szablon dla widoku - news/news_detail.html:
<h1>Moja strona</h1>
<h2>{{ news.title }}</h2>
{{ news.text }}

Szablon jest prosty, ale wyświetli dane dotyczące wybranej wiadomości. Tutaj wykorzystujemy wbudowaną funkcjonalność DetailView - klasa ta potrafi pobrać wpis po podanym numerze ID lub właśnie po "slugu" - unikalnym tekstowym identyfikatorze. Nie musimy pisać dodatkowego kodu pobierającego rekord.

Lista wiadomości z danej kategorii

W naszych modelach są też kategorie. Każda wiadomość może być przypisana do wielu kategorii. Zróbmy teraz widok listujący wiadomości z wybranej kategorii. Musimy więc napisać drugi widok ListView, który będzie wyświetlał tylko pasujące wiadomości. Oto jedna z możliwych wersji widoku:

class CategoryNewsList(NewsList):
    template_name = 'news/category_news_list.html'

    def get_context_data(self, **kwargs):
        context = super(CategoryNewsList, self).get_context_data(**kwargs)
        context['category'] = self._get_category()
        return context

    def get_queryset(self):
        category = self._get_category()
        return models.News.objects.filter(categories=category)

    def _get_category(self):
        return models.Category.objects.get(slug=self.kwargs['slug'])

category_news_list = CategoryNewsList.as_view()

To rozwiązanie jest trochę inne od poprzednich. Zamiast dziedziczyć ListView wybrałem NewsList (który to dziedziczy ListView). Dziedziczenie już istniejącej klasy widoku pozwala czasami uniknąć sporych duplikacji kodu. W tym prostym przypadku dziedziczymy podany model i ilość wpisów stronicowanych na jednej stronie.

W tym widoku pojawiają się nowe metody. Jeżeli czytałeś artykuł o widokach opartych o klasy to od razu się połapiesz. Metoda get_context_data pozwala przekazać dane do szablonu. Operator super pozwala nam pobrać istniejący kontekst i dodać do niego rekord kategorii. W metodzie get_queryset określamy jakie wiadomości mają być wyświetlane - te przypisane do wybranej kategorii. Dodatkowo podałem nazwę szablonu - template_name. Gdybym tego nie zrobił używany byłby szablon dziedziczonej listy wiadomości (news_list.html).

W urls.py mapujemy widok:

url(r'^category/(?P<slug>[\w\-_]+)/(?P<page>[0-9]+)/$', 'news.views.category_news_list', name='category-news-list'),
url(r'^category/(?P<slug>[\w\-_]+)/$', 'news.views.category_news_list', name='category-news-list'),

Jak widzimy w adresie URL przekazujemy "slug" kategorii. Dlatego w widoku możemy użyć self.kwargs['slug'] by otrzymać wartość sluga wybranej kategorii. Co ważne - jeżeli kategoria o podanym slugu nie będzie istniała to kod rzuci wyjątek. O obsłudze takich przypadków napiszę nieco dalej.

Teraz dodajmy listowanie kategorii, do jakich został przypisany news:
{% for entry in news_list %}
    <h3><a href="{% url "news-detail" entry.slug %}">{{ entry.title }}</a></h3>
    {{ entry.text|safe }}<br />
    {{ entry.date|date:"Y.m.d H:i" }}
    {% for category in entry.categories.all %}
        <a href="{% url "category-news-list" category.slug %}">{{ category.name }}</a>{% if not forloop.last %}, {% endif %}
    {% endfor %}
{% endfor  %}
Szablon news/category_news_list.html wygląda podobnie do szablonu news_list:
<h1>Moja Strona - wpisy z kategorii {{ category.name }}</h1>
{% for entry in news_list %}
    <h3><a href="{% url "news-detail" entry.slug %}">{{ entry.title }}</a></h3>
    {{ entry.text|safe }}<br />
    {{ entry.date|date:"Y.m.d H:i" }}
    {% for category in entry.categories.all %}
        <a href="{% url "category-news-list" category.slug %}">{{ category.name }}</a>{% if not forloop.last %}, {% endif %}
    {% endfor %}
{% endfor  %}


{% if is_paginated %}
    <hr>
    {% if page_obj.has_next %}
        <a href="{% url "category-news-list" category.slug page_obj.next_page_number %}">starsze</a>
    {% endif %}
    {% if page_obj.has_previous %}
        <a href="{% url "category-news-list" category.slug page_obj.previous_page_number %}">nowsze</a>
    {% endif %}
{% endif %}

Duplikuje nam się trochę kodu. Zajmiemy się tym za chwilę. Mamy już obecnie trzy widoki - lista wszystkich wiadomości, lista wiadomości z wybranej kategorii i widok szczegółowy wiadomości. Nie musieliśmy ani pisać zapytań SQL do bazy danych, ani pisać wielu linii kodu Pythona.

Lista wiadomości w naszej aplikacji

Szablony Django

W tej chwili nasze szablony są bardzo "słabe". Listy wiadomości duplikują kod, jak i żaden szablon nie ma poprawnej struktury. Szablony Django oferują nam m.in. bloki oraz dziedziczenie szablonów. Dzięki temu w łatwy sposób jesteśmy w stanie zarządzać wieloma szablonami bez zbędnej duplikacji kodu.

W katalogu news/templates stworzyłem szablon base.html:

<html>
<head>
    <title>{% block title %}Moja strona{% endblock %}</title>
</head>
<body>
<h1>{% block page-name %}Moja strona{% endblock %}</h1>
{% block content %}
{% endblock %}
</body>
</html>

Mamy tutaj prosty szablon bazowy z strukturą strony HTML. Dodatkowo pojawiło się kilka bloków Django - na tytuł strony (zarówno ten w nagłówku jak i wyświetlany na stronie) oraz treść.

Szablon news/news_detail.html może wyglądać teraz tak:

{% extends "base.html" %}

{% block title %}{{ news.title }}{% endblock %}
{% block page-name %}{{ news.title }}{% endblock %}

{% block content %}
{{ news.text }}
{% endblock %}

Na samym początku pliku używamy taga extends i podajemy nazwę szablonu jaki dziedziczymy. Następnie wypełniamy bloki treścią i gotowe. Django weźmie szablon bazowy i wypełni jego bloki podanymi przez nas danymi.

A co z duplikacją kodu w news_list.html i category_news_list.html, gdzie mamy identyczny kod listujący wiadomości? Możemy przenieść kod z pętlą do oddzielnego szablonu i użyć taga include do jego załączenia w obu szablonach. Ja stworzyłem news/parts/news_list.html i następnie załączyłem go w szablonach listy:

{% extends "base.html" %}

{% block content %}
{% include "news/parts/news_list.html" %}

{% if is_paginated %}
    <hr>
    {% if page_obj.has_next %}
        <a href="{% url "news-list" page_obj.next_page_number %}">starsze</a>
    {% endif %}
    {% if page_obj.has_previous %}
        <a href="{% url "news-list" page_obj.previous_page_number %}">nowsze</a>
    {% endif %}
{% endif %}
{% endblock %}
Kod pętli się nie dubluje dzięki zastosowaniu include.

Pliki statyczne

Pliki statyczne to wszystkie pliki frontendowe - CSS, JS, grafiki itp. W Django pliki statyczne dzielą się na dwie grupy - "statyka" i "media". Media to pliki stworzone/przesłane przez użytkowników - np. ikony kategorii. "Statyka" to pliki związane z wyglądem strony (CSS, JS, pliki graficzne) i inne statyczne pliki wykorzystywane w szablonach (np. regulamin jako plik PDF).

Pliki "statyki" umieszczamy w katalogu static w danej aplikacji Django. Można stworzyć oddzielną aplikację Django na podstawowe szablony i pliki, można też np. umieścić je w najważniejszej" aplikacji projektu. W szablonach ścieżka do statyki dostępna jest poprzez zmienną STATIC_URL, a media poprzez MEDIA_URL. Przykład:

<link rel="stylesheet" href="{{ STATIC_URL }}style.css" type="text/css" />

Co dalej?

Tematów jest jeszcze ogrom, ale po tym wprowadzeniu powinieneś mieć obraz programowania z Django, tworzenia aplikacji internetowych za jego pomocą. Warto teraz przejrzeć dokumentacje i artykuły poświęcone widokom opartym o klasy, rozbudować nasz przykładowy blog o formularze komentarzy, dodać obsługę nieistniejących newsów/kategorii poprzez rzucanie 404 - braku strony. Można też stworzyć kanał RSS, czy mapę Sitemap za pomocą wbudowanych w Django komponentów. Użyj tego projektu jako poligon doświadczalny.

blog comments powered by Disqus

Kategorie

Strony