Django DetailView - podstawowy widok generyczny


DetailView w Django to podstawowy widok generyczny. Poznaj go koniecznie!


Django pozwala na bardzo łatwe budowanie aplikacji. Jeśli aplikacja ma być wystawiona szybko i jest stosunkowo generyczna, to ten framework w Python nadaje się do tego idealnie. Od kilku lat zawodowo pracuję w tym frameworku i nieraz grzebałem w jego bebechach, dlatego znam go prawie na wylot i dziś przedstawię Ci wszystko co musisz wiedzieć aby efektywnie wykorzystać generyczny widok DetailView w Django.

Klasowe widoki mają swoich zwolenników i przeciwników. Osoby, które uwielbiają programy zorientowane obiektowo naturalnie wybierają widoki generyczne, bo są dla nich bardziej przyjazne. Z drugiej strony osoby, które wolą funkcję z widoków class-based korzystają tylko jeśli przyniesie to uzasadnione uproszczenie. Ja uważam, że warto trzymać się jednej konwencji, więc jeśli zamierzasz mieć w swojej aplikacji kilka widoków, to lepiej żeby w miarę możliwości wszystkie były klasowe, lub wszystkie funkcyjne. A jako, że klasowe dają znacznie więcej możliwości, to naturalnie mój wybór pada właśnie na widoki generyczne.

Przejdźmy zatem do omówienia pierwszego z nich i poznajmy generyczny widok DetailView.

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.

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

Mamy model, więc zabierzemy się za napisanie prostego widoku. Widok jest banalnie prosty, a najważniejsze kwestie to zaimporotwanie klasy, z której będziemy dziedziczyć oraz modelu, który rejestrujemy.

from django.views.generic.detail import DetailView

from .models import Post


class PostDetailView(DetailView):
    model = Post

Rejestracja widoku w urls.py

Skoro mamy widok, to musimy teraz go podpiąć, a wszystkie adresy podpinamy w pliku urls.py. Na potrzeby jednego widoku nie będziemy wydzielać tego do pliku do aplikacji. Jeśli wpis okaże się wartościowy i powstanie z tego seria to w podsumowaniu zrobimy refaktoring :)

from django.urls import path
from .views import PostDetailView

urlpatterns = [
    ... 
    path('blog/<slug:slug>/', PostDetailView.as_view(), name='post-detail'),
    ...
]

Rejestracja widoku po polu pk

Widok można prosto zarejestrować również po polu pk, co jest widoczne niżej.

from django.urls import path
from .views import PostDetailView

urlpatterns = [
    ... 
    path('blog/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
    ...
]

Widok generyczny DetailView jest tak sprytnie skonstruowany, że przewiduje domyślnie zarówno pobieranie po pk oraz po slug .

Niestandardowy slug_field

Zdarza się, że musisz dopisać widok do istniejącego już modelu. Jeśli w modelu tym jest pole typu SlugField ale nie nazywa się slug i chcesz utworzyć adres właśnie po tym polu to nie musisz panikować ani kombinować. Wystarczy nadpisać atrybut slug_field .

class PostDetailView(DetailView):
    model = Post
    slug_field = 'custom_slug_field'

Szablon

Domyślnie Django szuka szablonu w katalogu <nazwa_aplikacji>/<nazwa_modelu>_detail.html, czyli u nas będzie to blog/post_detail.html . Stwórzmy zatem ten uproszczony szablon.

<h1>{{ object.title }}</h1>
<span class='lead'>{{ object.lead }} </span>
<div class='body'>{{ object.body|safe }} </div>

W zasadzie te 3 proste kroki pozwolą Ci zbudować prosty serwis. W dalszej części wpisu przedstawię dodatkowe możliwości jakie dają generyczne widoki, które warto znać i mogą się przydać nie raz.

Niestandardowe pobieranie obiektu

Czasem zachodzi potrzeba niestandardowego pobierania obiektu, na przykład z zewnętrznego API. Można to bardzo prosto zrobić poprzez nadpisanie metody get_object . My zróbmy prosty przykład i ręcznie pobierzmy obiekt po polu slug . ( Średnio ma to sens, bo taka obsługa jest wbudowana ale nie chcę komplikować ;) )

from .exceptions import PostDoesNotExist


class PostDetailView(DetailView):

    def get_object(self, queryset=None):
        slug = self.kwargs.get(self.slug_url_kwarg, None)
        try:
            return queryset.get(slug=slug)
        except PostDoesNotExist:
            raise Http404('Oj, chyba nie ma obiektu ;)')

Niestandardowe pobieranie queryset

Obiekt, który wyświetlany jest jednym z wielu w bazie, czasem chcemy mieć niestandardowe mechanizmy odfiltrowania na przykład nieaktywnych jeszcze obiektów lub dodania select_related jeśli korzystamy z dowiązań do innych klas. To również jest bardzo proste i można to zrobić na przykład korzystając z atrybutu queryset.

Zobaczmy na przykładzie jak wyświetlić tylko aktywne wpisy. Załóżmy, że obiekt ten ma dowiązanie do modelu zdjęcia.

class PostDetailView(DetailView):
    model = Post
    queryset = Post.objects.filter(active=True).select_related('photo')

Metoda get_queryset

Jeśli z różnych przyczyn nie chcesz korzystać z atrybutu, to również jest możliwe. W widoku jest metoda get_queryset , która domyślnie zwraca wartość własnie z opisaneo wcześniej atrybutu ale możesz ją spokojnie nadpisać i zwrócić własny listing. Przydaje się jeśli chcesz pobierać obiekty nie z bazy, ale na przykład po jakimś API. Niżej przykład prostego nadpisania takiej metody.

from .api import PostApi


class PostDetailView(DetailView):
    model = Post
    
    def get_queryset(self):
        api = PostApi()
        return api.get_posts()

Oczywiście jest tu spore uproszczenie i nie podawałem implementacji Api, bo to jest tu nieistotne. Ważny jest fakt, że można swobodnie nadpisać tę metodę. Można też zrobić swoistą hybrydę, która queryset pobierze z bazy a już szczegóły po API, albo odwrotnie. Spróbuj sam i daj znać czy znalazłeś jakieś ciekawe zastosowanie.

Praktyczne nadpisanie metody get_queryset.

Długo zastanawiałem się jakie może być praktyczne zastosowanie nadpisania metody get_queryset. Dopiero dziś w pracy przy sprawdzaniu MR dosłownie mnie olśniło. Jeśli mamy strukturę nieco bardziej złożoną i chcemy aby w adresie URL wpisu była widoczna kategoria to możemy to bardzo prosto osiągnąć. Spróbujmy zatem dodać taki slug do adresu URL.

urlpatterns = [
    ... 
    path('blog/<slug:category_slug>/<slug:slug>/', PostDetailView.as_view(), name='post-detail'),
    ...
]

Czy to już wszystko? No nie do końca. W ten sposób nie zabezpieczamy w żaden sposób adresu i nie sprawdzamy czy kategoria występuje w adresie. Jeśli tego nie obsłużymy to istnieje ryzyko, że pod wieloma adresami będzie świecić się ta sama treść, co nie jest najlepszym rozwiązaniem pod Google.

Tu rozwiązania widzę dwa. Pierwszym jest przekierowanie na poprawną stronę. W naszym przypadku nie chcemy takiego przekierowania i wystarczy wyrzucenie 404, a te z kolei zostanie wyrzucone jeśli nie będzie obiektu. Nadpiszmy zatem metodę tak, żeby filtrowała wpisy po kategorii z adresu.

class PostDetailView(DetailView):
    model = Post
    
    def get_queryset(self):
        category = self.kwargs.get('category_slug', '')
        q = super().get_queryset()
        return q.filter(category__slug=category).select_related('category')

Oczywiście cały czas pamiętamy o select_related . I tak bardzo prosto dodaliśmy ciekawą poprawkę o której łatwo zapomnieć. Oczywiście wszystko przy założeniu, że nasz model ma kategorię :)

Nazwa obiektu w szablonie

Zapewne zauważyłeś, że obiekt w szablonie nazywa się object. Czasem może to być mylące i możesz mieć potrzebę to zmienić. Nic trudnego, wystarczy nadpisać atrybut context_object_name . Od teraz w szablonie możesz korzystać z nowej nazwy twojego obiektu.

class PostDetailView(DetailView):
    model = Post
    context_object_name = 'post'

Po takiej zmianie możemy poprawić szablon

<h1>{{ post.title }}</h1>
<span class='lead'>{{ post.lead }} </span>
<div class='body'>{{ post.body|safe }} </div>

Alternatywą również jest skorzystanie z metody get_context_object_name , ale moim zdaniem jest ona tutaj niepotrzebna, bo opisany atrybut wystarcza w zupełności.

Alternatywny szablon

Co jeśli chcę mieć na przykład dwa różne widoki, bo obsługują one inną logikę ale sposób wyświetlania danych jest identyczny? Wiele razy się z tym borykałem i uwierz przy starszych wersjach django, jak np 1.2 trzeba było trochę kombinować. Dziś natomiast wystarczy nadpisać atrybut template_name na dowolny szablon.

class PostDetailView(DetailView):
    model = Post
    template_name = 'my_template.html'

Dekoratory na widokach klasowych

Django ma rozbudowaną gamę różnych dekoratorów. Od takiego, co potrafi wyłączyć cache na konkretnych stronach po taki co wymusza bycie zalogowanym. Żeby zapiąć taki dekorator należy nałożyć go na metodę dispatch w widoku albo na urls.py. Niżej trzy możliwości

from django.views.decorators.cache import never_cache


class PostDetailView(DetailView):
    model = Post

    @never_cache
    def dispatch(self, *args, **kwargs):
        return super(PostDetailView, self).dispatch(*args, **kwargs)     

Dużo wygodniej robi się to na klasie przez wskazanie metody

from django.views.decorators.cache import never_cache


@method_decorator(never_cache, name='dispatch')
class PostDetailView(DetailView):
    model = Post

Jeśli natomiast nie chcemy nadpisywać żadnej metody to zróbmy to bezpośrednio w pliku urls.py.

from django.views.decorators.cache import never_cache

urlpatterns = [
    ... 
    path('blog/<slug:slug>/', never_cache(PostDetailView.as_view()), name='post-detail'),
    ...
]

Dodatkowe dane w kontekst

Nie raz będziesz mieć sytuację w której do szablonu będziesz musiał przekazać dodatkową wartość. Możliwości jest bardzo dużo i wszystko zależy od tego co chcesz przekazać.

Można to bowiem zrobić na przykład przez middleware lub też templatetagi.
Inną możliwością jest również skorzystanie z faktu, że widok przekazuje do szablonu konkretny kontekst, a ten jest niczym innym jak słownikiem. W widoku generycznym przekazanie dodatkowej wartości do szablonu jest równie proste co poprzednie operacje i pewnie się domyślasz, że wystarczy jedynie nadpisać odpowiedni atrybut. Spróbujmy zatem przekazać do kontekstu dodatkowo 3 posty.

from django.views.decorators.cache import never_cache


class PostDetailView(DetailView):
    model = Post
    extra_context = {'latest': Post.objects.all()[:3]}

Wypiszmy jeszcze w szablonie te ostatnie wpisy

<h1>{{ post.title }}</h1>-
<span class='lead'>{{ object.lead }} </span>
<div class='body'>{{ object.body|safe }} </div>

<ul class='latest'>
{% for post in latest %} <li>{{ post.pk}}. {{ post.title }}</li>{% endfor %}
</ul>

Bonus - pobieranie obiektów bez slug i pk

Najczęściej do pobierania obiektu potrzebny jest jakiś klucz, czyli jakaś wartość po której musimy dany obiekt zdefiniować. Ale nie zawsze tak jest, czego przykładem może być pobieranie czegoś z sesji albo z samego request. Dobrym przykładem może być tu pobranie na przykład użytkownika. Zobaczmy to na przykładzie

urlpatterns = [
    ...
    path('blog/', UserDetailView.as_view(), name='user-detail'),
    ...
]

A w widoku mały hak

from django.views.generic.detail import DetailView
from django.shortcuts import get_object_or_404                                  


class UserDetailView(DetailView):
    
    def get_object(self):
        return get_object_or_404(User, id=self.request.user.id)
        # return get_object_or_404(User, id=self.request.session.get('user'))

Ten widok akurat ma średni sens, bo user w request jest wbudowany ale jeśli nie korzystamy z domyslnego logowania i sami obsługujemy to poprzez zapis w sesji to ten zakomentowany kod będzie już miał tego sensu więcej. Taki przypadek może być praktycznie wykorzystany na przykład na stronie edycji swojego konta jednak największy potencjał takiego podejścia odkryjemy w ListView.

Podsumowanie

Właśnie poznałeś jeden z trzech podstawowych widoków generycznych w Django, na których można zbudować wiele aplikacji. Jeśli jesteś ciekaw dwóch pozostałych podziel się wpisem z innymi i zostaw komentarz, a gwarantuję, że po tej mini serii będziesz w stanie zbudować swojego bloga i być może w przyszłości również dzielić się wiedzą z innymi.

Nie są to oczywiście wszystkie, bo tych jest znacznie więcej, jednak ta trójka to swoisty kodeks, który trzeba znać.

Lis 12, 2019

Najnowsze wpisy

Zobacz wszystkie