Tworzymy przykładowego bloga - część 2
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:
{% 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 %}
url(r'^(?P<page>[0-9]+)/$', 'news.views.news_list', name='news-list'),
url(r'^/?$', 'news.views.news_list', name='news-list'),
<a href="{% url "news-list" page_obj.next_page_number %}">starsze</a>
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()
url(r'^news/(?P<slug>[\w\-_]+)/$', 'news.views.news_detail', name='news-detail'),
<h3><a href="{% url "news-detail" entry.slug %}">{{ entry.title }}</a></h3>
<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 %}
<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.

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 %}
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.
Comment article