Använda mixins med klassbaserade vyer¶
Varning
Detta är ett avancerat ämne. En fungerande kunskap om Djangos klassbaserade vyer rekommenderas innan du utforskar dessa tekniker.
Djangos inbyggda klassbaserade vyer ger en hel del funktionalitet, men en del av den kanske du vill använda separat. Till exempel kanske du vill skriva en vy som renderar en mall för att skapa HTTP-svaret, men du kan inte använda TemplateView
; kanske behöver du bara rendera en mall på POST
, med GET
som gör något helt annat. Även om du kan använda TemplateResponse
direkt, kommer detta sannolikt att resultera i duplicerad kod.
Av denna anledning tillhandahåller Django också ett antal mixins som ger mer diskret funktionalitet. Mallrendering, till exempel, är inkapslad i TemplateResponseMixin
. Djangos referensdokumentation innehåller full dokumentation av alla mixins.
Sammanhang och svarsmallar¶
Två centrala mixins tillhandahålls som hjälper till att tillhandahålla ett konsekvent gränssnitt för att arbeta med mallar i klassbaserade vyer.
TemplateResponseMixin
Varje inbyggd vy som returnerar en
TemplateResponse
kommer att anroparender_to_response()
-metoden somTemplateResponseMixin
tillhandahåller. För det mesta kommer detta att anropas åt dig (till exempel anropas det avget()
-metoden som implementeras av bådeTemplateView
ochDetailView
); på samma sätt är det osannolikt att du behöver åsidosätta det, men om du vill att ditt svar ska returnera något som inte renderas via en Django-mall så vill du göra det. För ett exempel på detta, se JSONResponseMixin exempel.render_to_response()
själv anroparget_template_names()
, som standard kommer att leta upptemplate_name
på den klassbaserade vyn; två andra mixins (SingleObjectTemplateResponseMixin
ochMultipleObjectTemplateResponseMixin
) åsidosätter detta för att ge mer flexibla standardvärden när man hanterar faktiska objekt.ContextMixin
Varje inbyggd vy som behöver kontextdata, till exempel för att rendera en mall (inklusive
TemplateResponseMixin
ovan), bör anropaget_context_data()
och skicka alla data som de vill säkerställa finns där som nyckelordsargument.get_context_data()
returnerar en ordbok; iContextMixin
returnerar den sina nyckelordsargument, men det är vanligt att åsidosätta detta för att lägga till fler medlemmar i ordboken. Du kan också använda attributetextra_context
.
Uppbyggnad av Djangos generiska klassbaserade vyer¶
Låt oss titta på hur två av Djangos generiska klassbaserade vyer är uppbyggda av mixins som ger diskret funktionalitet. Vi kommer att överväga DetailView
, som renderar en ”detaljerad” vy av ett objekt, och ListView
, som kommer att rendera en lista över objekt, vanligtvis från en queryset, och eventuellt paginera dem. Detta kommer att introducera oss till fyra mixins som tillsammans ger användbar funktionalitet när man arbetar med antingen ett enda Django-objekt eller flera objekt.
Det finns också mixins involverade i de generiska redigeringsvyerna (FormView
, och de modellspecifika vyerna CreateView
, UpdateView
och DeleteView
), och i de datumbaserade generiska vyerna. Dessa behandlas i mixin referensdokumentation.
DetailView
: att arbeta med ett enda Django-objekt¶
För att visa detaljerna i ett objekt måste vi i princip göra två saker: vi måste leta upp objektet och sedan måste vi skapa en TemplateResponse
med en lämplig mall och det objektet som kontext.
För att hämta objektet förlitar sig DetailView
på SingleObjectMixin
, som tillhandahåller en get_object()
-metod som räknar ut objektet baserat på webbadressen för begäran (den letar efter pk
och slug
nyckelordsargument som deklareras i URLConf, och letar upp objektet antingen från attributet model
på vyn, eller attributet queryset
om det tillhandahålls). SingleObjectMixin
åsidosätter också get_context_data()
, som används i alla Djangos inbyggda klassbaserade vyer för att tillhandahålla kontextdata för mallåtergivning.
För att sedan skapa en TemplateResponse
, använder DetailView
SingleObjectTemplateResponseMixin
, som utökar TemplateResponseMixin
, och åsidosätter get_template_names()
som diskuterats ovan. Det ger faktiskt en ganska sofistikerad uppsättning alternativ, men det viktigaste som de flesta kommer att använda är <app_label>/<model_name>_detail.html
. Delen _detail
kan ändras genom att ställa in template_name_suffix
på en underklass till något annat. (Till exempel använder :doc:generic edit views<generic-editing>` ``_form
för att skapa och uppdatera vyer, och _confirm_delete
för att ta bort vyer)
ListView
: arbeta med många Django-objekt¶
Listor med objekt följer ungefär samma mönster: vi behöver en (eventuellt paginerad) lista med objekt, vanligtvis en QuerySet
, och sedan måste vi skapa en TemplateResponse
med en lämplig mall som använder den listan med objekt.
För att få objekten använder ListView
MultipleObjectMixin
, som tillhandahåller både get_queryset()
och paginate_queryset()
. Till skillnad från SingleObjectMixin
finns det inget behov av att ange delar av URL:en för att räkna ut vilken queryset som ska användas, så standardvärdet använder attributet queryset
eller model
på vyklassen. En vanlig anledning att åsidosätta get_queryset()
här skulle vara att dynamiskt variera objekten, till exempel beroende på den aktuella användaren eller att utesluta inlägg i framtiden för en blogg.
MultipleObjectMixin
åsidosätter också get_context_data()
för att inkludera lämpliga kontextvariabler för paginering (tillhandahåller dummies om paginering är inaktiverad). Den förlitar sig på att object_list
skickas in som ett nyckelordsargument, vilket ListView
ordnar för den.
För att göra en TemplateResponse
, använder ListView
sedan MultipleObjectTemplateResponseMixin
; som med SingleObjectTemplateResponseMixin
ovan, åsidosätter detta get_template_names()
för att tillhandahålla en rad alternativ
, där det vanligaste är <app_label>/<model_name>_list.html
, där _list
-delen återigen hämtas från attributet template_name_suffix
. (De datumbaserade generiska vyerna använder suffix som _archive
, _archive_year
och så vidare för att använda olika mallar för de olika specialiserade datumbaserade listvyerna)
Använda Djangos klassbaserade vy-mixins¶
Nu har vi sett hur Djangos generiska klassbaserade vyer använder de medföljande mixinerna, låt oss titta på andra sätt vi kan kombinera dem. Vi kommer fortfarande att kombinera dem med antingen inbyggda klassbaserade vyer eller andra generiska klassbaserade vyer, men det finns en rad sällsynta problem du kan lösa än vad som tillhandahålls av Django från början.
Varning
Alla mixins kan inte användas tillsammans, och alla generiska klassbaserade vyer kan inte användas med alla andra mixins. Här presenterar vi några exempel som fungerar; om du vill sammanföra annan funktionalitet måste du ta hänsyn till interaktioner mellan attribut och metoder som överlappar mellan de olika klasserna du använder, och hur method resolution order påverkar vilka versioner av metoderna som ska anropas i vilken ordning.
Referensdokumentationen för Djangos class-based views och class-based view mixins hjälper dig att förstå vilka attribut och metoder som sannolikt kommer att orsaka konflikt mellan olika klasser och mixins.
Om du är osäker är det ofta bättre att backa och basera ditt arbete på View
eller TemplateView
, kanske med SingleObjectMixin
och MultipleObjectMixin
. Även om du förmodligen kommer att skriva mer kod, är det mer sannolikt att den är tydligt förståelig för någon annan som kommer till den senare, och med färre interaktioner att oroa sig för sparar du dig själv en del tänkande. (Naturligtvis kan du alltid dyka in i Djangos implementering av de generiska klassbaserade vyerna för inspiration om hur du ska ta itu med problem)
Använda SingleObjectMixin
med View¶
Om vi vill skriva en klassbaserad vy som bara svarar på POST
, subklassar vi View
och skriver en post()
-metod i subklassen. Men om vi vill att vår bearbetning ska fungera på ett visst objekt, identifierat från URL, vill vi ha den funktionalitet som tillhandahålls av SingleObjectMixin
.
Vi ska demonstrera detta med modellen Author
som vi använde i :doc:generic class-based views introduction<generic-display>
.
Vyer.py
¶from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
class RecordInterestView(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
# Look up the author we're interested in.
self.object = self.get_object()
# Actually record interest somehow here!
return HttpResponseRedirect(
reverse("author-detail", kwargs={"pk": self.object.pk})
)
I praktiken skulle du förmodligen vilja registrera intresset i en nyckelvärdesbutik snarare än i en relationsdatabas, så vi har utelämnat den biten. Den enda delen av vyn som behöver oroa sig för att använda SingleObjectMixin
är där vi vill leta upp den författare vi är intresserade av, vilket den gör med ett anrop till self.get_object()
. Allt annat tas om hand för oss av mixin.
Vi kan enkelt koppla in detta i våra webbadresser:
urls.py
¶from django.urls import path
from books.views import RecordInterestView
urlpatterns = [
# ...
path(
"author/<int:pk>/interest/",
RecordInterestView.as_view(),
name="author-interest",
),
]
Notera gruppen med namnet pk
, som get_object()
använder för att leta upp instansen Author
. Du kan också använda en slug, eller någon av de andra funktionerna i SingleObjectMixin
.
Använda SingleObjectMixin
med ListView
¶
ListView
ger inbyggd paginering, men du kanske vill paginera en lista med objekt som alla är länkade (med en främmande nyckel) till ett annat objekt. I vårt publiceringsexempel kanske du vill paginera genom alla böcker från en viss utgivare.
Ett sätt att göra detta är att kombinera ListView
med SingleObjectMixin
, så att queryset för den paginerade listan med böcker kan hänga på förlaget som hittas som ett enda objekt. För att kunna göra detta måste vi ha två olika querysets:
Book
queryset för användning av :klass:`~django.views.generic.list.ListView`Eftersom vi har tillgång till det förlag vars böcker vi vill lista, åsidosätter vi
get_queryset()
och använder förlagets :ref:reverse foreign key manager<backwards-related-objects>
.Publisher
queryset för användning iget_object()
Vi kommer att förlita oss på standardimplementeringen av
get_object()
för att hämta rättPublisher
-objekt. Vi måste dock uttryckligen skicka ettqueryset
-argument eftersom standardimplementeringen avget_object()
annars skulle anropaget_queryset()
som vi har åsidosatt för att returneraBook
-objekt istället förPublisher
-objekt.
Observera
Vi måste tänka noga på get_context_data()
. Eftersom både SingleObjectMixin
och ListView
kommer att lägga saker i kontextdata under värdet av context_object_name
om det är inställt, kommer vi istället att uttryckligen se till att Publisher
finns i kontextdata. ListView
kommer att lägga till lämplig page_obj
och paginator
åt oss förutsatt att vi kommer ihåg att anropa super()
.
Nu kan vi skriva en ny PublisherDetailView
:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(SingleObjectMixin, ListView):
paginate_by = 2
template_name = "books/publisher_detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Publisher.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["publisher"] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
Lägg märke till hur vi anger self.object
inom get()
så att vi kan använda det igen senare i get_context_data()
och get_queryset()
. Om du inte anger template_name
, kommer mallen som standard att välja det normala ListView
-valet, som i det här fallet skulle vara "books/book_list.html"
eftersom det är en lista över böcker; ListView
vet ingenting om SingleObjectMixin`
, så den har ingen aning om att den här vyn har något att göra med en Publisher
.
paginate_by
är avsiktligt liten i exemplet så att du inte behöver skapa massor av böcker för att se att pagineringen fungerar! Här är den mall du vill använda:
{% extends "base.html" %}
{% block content %}
<h2>Publisher {{ publisher.name }}</h2>
<ol>
{% for book in page_obj %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endblock %}
Undvik allt som är mer komplicerat¶
Generellt kan du använda TemplateResponseMixin
och SingleObjectMixin
när du behöver deras funktionalitet. Som visas ovan kan du med lite försiktighet till och med kombinera SingleObjectMixin
med ListView
. Saker och ting blir dock alltmer komplexa när du försöker göra det, och en bra tumregel är:
Råd
Var och en av dina vyer bör endast använda mixins eller vyer från en av grupperna av generiska klassbaserade vyer: detail, list, editing och date. Det går till exempel bra att kombinera TemplateView
(inbyggd vy) med MultipleObjectMixin
(generisk lista), men du kommer sannolikt att få problem med att kombinera SingleObjectMixin
(generisk detalj) med MultipleObjectMixin
(generisk lista).
För att visa vad som händer när man försöker bli mer sofistikerad, visar vi ett exempel som offrar läsbarhet och underhåll när det finns en enklare lösning. Låt oss först titta på ett naivt försök att kombinera DetailView
med FormMixin
för att göra det möjligt för oss att POST
en Django Form
till samma URL som vi visar ett objekt med DetailView
.
Använda FormMixin
med DetailView
¶
Tänk tillbaka på vårt tidigare exempel där vi använde View
och SingleObjectMixin
tillsammans. Vi registrerade en användares intresse för en viss författare; säg nu att vi vill låta dem lämna ett meddelande där de säger varför de gillar dem. Återigen, låt oss anta att vi inte kommer att lagra detta i en relationsdatabas utan istället i något mer esoteriskt som vi inte kommer att oroa oss för här.
Vid denna tidpunkt är det naturligt att nå en Form
för att kapsla in den information som skickas från användarens webbläsare till Django. Säg också att vi är starkt investerade i REST, så vi vill använda samma URL för att visa författaren som för att fånga meddelandet från användaren. Låt oss skriva om vår AuthorDetailView
för att göra det.
Vi behåller GET
-hanteringen från DetailView
, även om vi måste lägga till en Form
i kontextdata så att vi kan rendera den i mallen. Vi kommer också att vilja dra in formulärbehandling från FormMixin
, och skriva lite kod så att formuläret på POST
anropas på lämpligt sätt.
Observera
Vi använder FormMixin
och implementerar post()
själva istället för att försöka blanda DetailView
med FormView
(som redan tillhandahåller en lämplig post()
) eftersom båda vyerna implementerar get()
, och saker och ting skulle bli mycket mer förvirrande.
Vår nya AuthorDetailView
ser ut så här:
# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.
from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
# Here, we would record the user's interest using the message
# passed in form.cleaned_data['message']
return super().form_valid(form)
get_success_url()
ger någonstans att omdirigera till, som används i standardimplementeringen av form_valid()
. Vi måste tillhandahålla vår egen post()
som tidigare nämnts.
En bättre lösning¶
Antalet subtila interaktioner mellan FormMixin
och DetailView
testar redan vår förmåga att hantera saker. Det är osannolikt att du skulle vilja skriva den här typen av klass själv.
I det här fallet kan du skriva post()
-metoden själv och behålla DetailView
som den enda generiska funktionaliteten, även om det innebär mycket dubbelarbete att skriva Form
-hanteringskod.
Alternativt skulle det fortfarande vara mindre arbete än ovanstående tillvägagångssätt att ha en separat vy för bearbetning av formuläret, som kan använda FormView
skilt från DetailView
utan problem.
En alternativ bättre lösning¶
Vad vi egentligen försöker göra här är att använda två olika klassbaserade vyer från samma URL. Så varför inte göra just det? Vi har en mycket tydlig uppdelning här: GET
förfrågningar bör få DetailView
(med Form
läggs till i kontextdata), och POST
förfrågningar bör få FormView
. Låt oss ställa in dessa vyer först.
Vyn AuthorDetailView
är nästan densamma som :ref:när vi först introducerade AuthorDetailView<generic-views-extra-work>`; vi måste skriva vår egen ``get_context_data()
för att göra AuthorInterestForm
tillgänglig för mallen. Vi hoppar över get_object()
överskrivningen från tidigare för tydlighetens skull:
from django import forms
from django.views.generic import DetailView
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = AuthorInterestForm()
return context
Då är AuthorInterestFormView
en FormView
, men vi måste ta in SingleObjectMixin
så att vi kan hitta den författare vi pratar om, och vi måste komma ihåg att ställa in template_name
för att säkerställa att formulärfel kommer att göra samma mall som AuthorDetailView
använder på GET
:
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterestFormView(SingleObjectMixin, FormView):
template_name = "books/author_detail.html"
form_class = AuthorInterestForm
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
Slutligen sammanför vi detta i en ny vy AuthorView
. Vi vet redan att anrop av as_view()
på en klassbaserad vy ger oss något som beter sig exakt som en funktionsbaserad vy, så vi kan göra det vid den punkt där vi väljer mellan de två undervyerna.
Du kan skicka nyckelordsargument till as_view()
på samma sätt som du skulle göra i din URLconf, till exempel om du vill att beteendet AuthorInterestFormView
också ska visas på en annan URL men med en annan mall:
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.as_view()
return view(request, *args, **kwargs)
Detta tillvägagångssätt kan också användas med alla andra generiska klassbaserade vyer eller dina egna klassbaserade vyer som ärver direkt från View
eller TemplateView
, eftersom det håller de olika vyerna så separata som möjligt.
Mer än bara HTML¶
Klassbaserade vyer är perfekta när du vill göra samma sak många gånger. Anta att du skriver ett API och att varje vy ska returnera JSON istället för renderad HTML.
Vi kan skapa en mixin-klass som kan användas i alla våra vyer och hantera konverteringen till JSON en gång.
Till exempel kan en JSON-mixin se ut ungefär så här:
from django.http import JsonResponse
class JSONResponseMixin:
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(self.get_data(context), **response_kwargs)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
Observera
Kolla in Serialisering av Django-objekt-dokumentationen för mer information om hur man korrekt omvandlar Django-modeller och querysets till JSON.
Denna mixin tillhandahåller en render_to_json_response()
metod med samma signatur som render_to_response()
. För att använda den måste vi blanda in den i en TemplateView
till exempel, och åsidosätta render_to_response()
för att anropa render_to_json_response()
istället:
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
På samma sätt kan vi använda vår mixin med en av de generiska vyerna. Vi kan göra vår egen version av DetailView
genom att blanda JSONResponseMixin
med BaseDetailView
– (DetailView
innan mallrenderingsbeteendet har blandats in):
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Denna vy kan sedan distribueras på samma sätt som alla andra DetailView
, med exakt samma beteende - förutom svarets format.
Om du vill vara riktigt äventyrlig kan du till och med blanda en DetailView
-underklass som kan returnera både HTML- och JSON-innehåll, beroende på någon egenskap i HTTP-begäran, till exempel ett frågeargument eller en HTTP-header. Blanda in både JSONResponseMixin
och en SingleObjectTemplateResponseMixin
, och åsidosätt implementeringen av render_to_response()
för att hänvisa till lämplig renderingsmetod beroende på vilken typ av svar som användaren begärde:
from django.views.generic.detail import SingleObjectTemplateResponseMixin
class HybridDetailView(
JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get("format") == "json":
return self.render_to_json_response(context)
else:
return super().render_to_response(context)
På grund av det sätt som Python löser metodöverbelastning slutar anropet till super().render_to_response(context)
med att anropa render_to_response()
-implementeringen av TemplateResponseMixin
.