Podstawowy model
Widok generyczny ListView
Rejestracja widoku w urls.py
Szablon
Nazwa listy w szablonie
Nadpisanie context_object_name
Pusta lista
Dodatkowa logika na pustej liście
Paginacja w ListView
Paginate_orphans
Sortowanie w ListView
Niestandardowe sortowanie
Queryset w ListView
Get_queryset
Podsumowanie
Spis treści
Jak pisałem w poprzednim wpisie , Django jest rewelacyjne to budowania prostych aplikacji. O ile rozumiem podejście funkcyjne przy prostych widokach bez rozbudowanej logiki, o tyle nie wyobrażam sobie teraz pisać obsługi listy bez podstawowego widoku generycznego, jakim jest ListView. Nie jest to bardzo skomplikowane, jednak jest bardzo dużo elementów, o których trzeba pamiętać. Dziś zaprezentuję Ci te funkcjonalności, z których sam korzystałem najczęściej.
Podstawowy model
Widok wyświetla dane, ale te trzeba przygotować. Przygotujmy zatem model danych, który chcemy wyświetlić. Niech będzie to model Post, z podstawowymi danymi jak tytuł, slug, jakaś treść i zajawka, dokładnie taki sam jak we wpisie z DetailView .
from django.db import models
class Post(models.Model):
active = models.BooleanField(verbose_name='aktywny', default=False)
title = models.CharField(max_length=255, verbose_name='tytuł')
slug = models.SlugField(unique=True)
body = models.TextField(verbose_name='treść')
lead = models.TextField(verbose_name='zajawka')
def __str__(self):
return self.title
Widok generyczny ListView
Mamy model, więc zabierzemy się za napisanie prostego widoku. Generyczny widok ListView jest banalnie prosty, a najważniejsze kwestie to skorzystanie z klasy, z której będziemy dziedziczyć oraz modelu, który rejestrujemy.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
Rejestracja widoku w urls.py
Skoro mamy widok, to musimy teraz go podpiąć, a wszystkie adresy podpinamy w pliku urls.py.
from django.urls import path
from .views import PostListView
urlpatterns = [
...
path('blog/', PostListView.as_view(), name='post-list'),
...
]
Szablon
Do pełni szczęścia brakuje nam jedynie szablonu. Domyślnie Django szuka szablonu w katalogu <nazwa_aplikacji>/<nazwa_modelu>_list.html, czyli u nas będzie to blog/post_list.html. Stwórzmy zatem ten szablon.
{% for post in object_list %}
<h1>{{ post.title }}</h1>
<span class='lead'>{{ post.lead }} </span>
<div class='body'>{{ post.body|safe }} </div>
{% endfor %}
Nazwa listy w szablonie
Używanie nazw typu object , czy object_list jest mało intuicyjne. Dlatego znacznie lepiej korzystać z nazwy modelu z członem _list . Wystarczy drobna zmiana, a kod będzie łatwiejszy w utrzymaniu. Sami zobaczcie i oceńcie, co jest bardziej jednoznaczne.
{% for post in post_list %}
<h1>{{ post.title }}</h1>
<span class='lead'>{{ post.lead }} </span>
<div class='body'>{{ post.body|safe }} </div>
{% endfor %}
Nadpisanie context_object_name
Wcześniej myślałem, że ta funkcjonalność jest niepotrzebna. Wszystko zmieniło się gdy pisałem widok do klasy pośredniczącej, o nazwie SubscriberInSubscription. Jak widzicie to bardzo długa nazwa i korzystanie z niej w szablonie wcale nie zwiększało czytelności. Wtedy doceniłem tę prostą funkcjonalność. Korzysta się z niej dokładnie tak samo jak w widoku DetailView.
class PostListview(ListView):
model = Post
context_object_name = 'posts'
{% for post in posts %}
<h1>{{ post.title }}</h1>
<span class='lead'>{{ post.lead }} </span>
<div class='body'>{{ post.body|safe }} </div>
{% endfor %}
Pusta lista
Zastanawiałeś się kiedyś, tworząc widok z listingiem, co zrobić w przypadku pustego szablonu? Jeśli nie, to nie przejmij się, nie jesteś jedyny. Niedawno startowaliśmy z nowym projektem i wspierałem jednego z naszych programistów. Zapomnieli o tym dosłownie wszyscy, nie tylko programiści. W zasadzie możliwości są 2, no może 3 na upartego. Pierwsza to serwować inną treść w szablonie w zależności od tego, czy lista jest pusta, czy też nie. Ogólnie nie jestem zwolennikiem wrzucania logiki do szablonów, dlatego bardzo przypadł mi do gustu atrybut allow_empty , który to decyduje o tym, czy strona może zostać wyświetlona w przypadku pustej listy. Domyślnie zezwala, jednak jeśli zmienimy jego wartość, to możemy spowodować, że przy pustej liście strona wyrzuci 404. Czasem jest to pożądane i warto wiedzieć, że można to zrobić bardzo prosto.
class PostListView(ListView):
model = Post
allow_empty = False
Dodatkowa logika na pustej liście
Kilka razy musiałem zaimplementować strony, które inaczej działały na stronach po zalogowaniu. I tak na przykład dla zalogowanych przy pustej liście miał się wyświetlać komunikat, a dla niezalogowanych miało wyrzucać 404. Jeśli uważnie czytaliście ten i poprzedni wpis , to pewnie domyślacie się, że istnieje metoda, która pobiera wspomniany wcześniej atrybut allow_empty . Macie rację. skorzystałem z niego i zrobiłem to w bardzo prosty sposób.
class PostListView(ListView):
model = Post
def get_allow_empty(self):
return self.request.user.is_authenticated
Tu bardzo ważna uwaga! To rozwiązanie nie jest dobrą praktyką na stronach, które mają dużą liczbę wyświetleń, bo odwołanie się do user z request powoduje ustawienie VaryCokie, a to z kolei może powodować problemy z cache, ale to temat na inny wpis. Najważniejsze, żebyście wiedzieli, że coś takiego jest i znali ewentualne zagrożenia.
Paginacja w ListView
Nawet zwolennicy widoków funkcyjnych przyznają, że generyczny ListView jest wygodny głównie ze względu na paginację. Paginacja sama w sobie nie jest bardzo skomplikowana, jednak jest wiele elementów o których łatwo zapomnieć. Tu Django idzie programistom na ręke i obsługuje całą paginację, a jedyne co potrzeba uzupełnić to atrybut określający ile elementów na stronie mamy wyświetlić, czyli paginate_by. Stwórzmy więc widok, który wyświetla 3 elementy na stronie
from django.views.generic import ListView
class PostListView(ListView):
model = Post
paginate_by = 3
Przy paginacji do szablonu przekazywany jest również atrybut page_obj , który będzie bardzo pomocny. Sposobów paginacji jest bardzo dużo, bo można to obsłużyć na przykład przez doładowanie, przy prostych przyciskach (poprzedni/następny) albo przy bardziej rozbudowanej obsłudze stronicowania. Zróbmy najprostszy sposób z przyciskami poprzedni/następny.
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
Paginate_orphans
Pracując z paginatorem warto pamiętać, że liczba elementów na stronie nie zawsze będzie układać się idealnie. Zwłaszcza jeśli mamy układ, który wymusza na nas, że aby strona dobrze się wyświetlała musi być kilka elementów. Do takich celów dobrze sprawdza się atrybut paginate_orphans , który pozwala nam sterować takimi przypadkami. Spróbujmy zaprezentować to na przykładzie
from django.views.generic import ListView
class PostListView(ListView):
model = Post
paginate_by = 10
paginate_orphans = 4
W takim przypadku, jeśli będziemy mieć 24 elementy, to pierwsza strona będzie miała 10 elementów a druga już 14, bo niejako pozwalamy, żeby 4 elementy ze strony kolejnej wpadły wcześniej. Osobiście nie stosuję tego zbyt często, ale widziałem że takie pytania pojawiają się na StackOverflow, więc lepiej kojarzyć, że coś takiego w ogóle istnieje :)
Sortowanie w ListView
Modele danych mają domyślne sortowanie. I to jest akurat dobre, bo nie trzeba się zastanawiać jakie sortowanie wybrać. Najczęściej jest to sortowanie po rosnącym ID/PK albo po dacie publikacji. Co jeśli chcemy mieć kilka listingów, z czego każdy inaczej sortowany? Na przykład ostatnio opublikowane oraz wszystkie, sortowane po tytule? Tu z pomocą przychodzi właśnie atrybut ordering, który otrzymuje nazwę atrybutu modelu po którym chcemy sortować. Przygotujmy zatem dwa widoku z dwoma różnymi sortowaniami.
from django.views.generic import ListView
class LatestPostListView(ListView): # domyślne sortowanie
model = Post
class PostListView(ListView):
model = Post
ordering = “name”
Niestandardowe sortowanie
Domyślne sortowanie jest bardzo praktyczne, jednak czasem zachodzi potrzeba dynamicznego sortowania. Widać to niemal na wszystkich portalach ogłoszeniowych. Jak zatem zrobić stronę, w której sortować można po wybranym parametrze? Dla uważnych czytelników mojego bloga nie powinno to być zaskoczeniem. Wystarczy bowiem skorzystać z metody get_ordering .
class PostListView(ListView):
model = Post
def get_ordering(self):
return self.request.GET.get('o', 'name')
W ten prosty sposób łatwo można obsłużyć sortowanie niemal po wszystkich polach. Oczywiście, nie jest to rozwiązanie idealne, bo jeśli ktoś zasymuluje wejście na nasz serwis z parametrem, którego nie mamy w modelu, to niestety wystąpił błąd serwera i warto to zabezpieczyć. Jednak do celów edukacyjnych, to w zupełności powinno wystarczyć. Odnośnie łapania wyjątków odsyłam do jednego z moich najlepszych wpisów .
Queryset w ListView
Jesteśmy już po całkiem sporej części tekstu a mimo to nie omówiliśmy najważniejszej kwestii. Generyczny widok ListView wymaga jednego z dwóch parametrów, model lub queryset. Sam najczęściej stosuję model, bo nie mam potrzeby serwowania kilku widoków dla tego samego typu danych, jednak całkiem niedawno pomagałem koledze z pracy, który musiał wyświetlić na jednej stronie dwie listy, jedna ze zgłoszeniami prywatnymi, a druga ze zgłoszeniami oficjalnymi. Całość była doładowana dynamicznie, jednak nie to było najważniejsze, najważniejsze bowiem było to, że w 10 minut uprościliśmy kod do 3 linjijek. Spróbujmy zaprezentować to odnosząc się do naszego przykładu
PostListView(ListView):
model = Post
class ActivePostListView(PostListView):
queryset = Post.objects.filter(active=True)
class InactivePostListView(PostListView):
queryset = Post.objects.filter(active=False)
Get_queryset
Czasem zachodzi potrzeba dynamicznej obsługi queryset. Do głowy przychodzą mi dwa praktyczne zastosowania takiego dynamicznego rozbudowywania queryset. Pierwszy to filtrowanie listy, które zresztą zastosowałem na swoim blogu . Nie wiem czemu, ale wiele osób ma z tym problemy, a w gruncie rzeczy implementacja jest stosunkowo prosta co widać niżej
class PostListView(ListView):
model = Post
def get_queryset(self):
q = self.request.GET.get('q', '')
return self.model.objects.published().filter(title__icontains=q)
Drugim zastosowaniem jest inne zachowania widoku dla zalogowanych użytkowników. Zróbmy zatem widok, który wyświetli tylko aktywne wpisy dla niezalogowanych, a dla zalogowanych wszystkie.
class PostListView(ListView):
model = Post
def get_queryset(self):
if self.if request.user.is_authenticated:
return self.model.objects.all()
else:
return self.model.objects.published()
Poznaliście już schemat, który możecie spokojnie dostosowywać, na przykład poprzez parametry w GET, czy inne zachowania. Zachęcam gorąco do eksperymentówania :)
Podsumowanie
Poznaliście właśnie ListView, czyli drugi widok generyczny obok DetailView, który jest przeze mnie najczęściej wykorzystywany. Osobiście najbardziej cenię go za obsługę paginacji i fakt, że dzięki zastosowaniu klas łatwo tworzyć nowe widoki z rozszerzonymi funkcjonalnościami. Nie wspominałem jeszcze o extra_context , ale to atrybut dostępny we wszystkich widokach generycznych, więc nie chciałem rozszerzać wpisu.
Mam nadzieję, że ten wpis przekonał Was do tego, Django ListView to podstawowy i bardzok prayktyczny widok generyczny.