Django ListView - podstawowy widok generyczny


Wyświetlanie listy, paginacja, filtrowanie, czy sortowanie to zaledwie początek tego co oferuje generyczny widok ListView w Django. Przekonaj się sam!


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">&laquo; 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 &raquo;</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.

Mar 28, 2020

Najnowsze wpisy

Zobacz wszystkie