Podstawowy model
Widok generyczny
Rejestracja widoku w urls.py
Rejestracja widoku po polu pk
Niestandardowy slug_field
Szablon
Niestandardowe pobieranie obiektu
Niestandardowe pobieranie queryset
Metoda get_queryset
Praktyczne nadpisanie metody get_queryset.
Nazwa obiektu w szablonie
Alternatywny szablon
Dekoratory na widokach klasowych
Dodatkowe dane w kontekst
Bonus - pobieranie obiektów bez slug i pk
Podsumowanie
Spis treści
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 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 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łaśnie z opisanego 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. Przy okazji dodam, że nazwa post jest tu mało trafna, bo Django domyślnie tworzy alias w postaci nazwy modelu.
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 domyślnego 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ć.