Använda mixins med klassbaserade vyer¶
Varning
Detta är ett avancerat ämne. Du bör ha praktiska kunskaper om Djangos klassbaserade vyer 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.
TemplateResponseMixinVarje inbyggd vy som returnerar en
TemplateResponseanropar metodenrender_to_response()somTemplateResponseMixintillhandahåller. För det mesta anropas den åt dig (den anropas till exempel av metodenget()som implementeras av bådeTemplateViewochDetailView); på samma sätt är det osannolikt att du behöver åsidosätta den. Om du däremot vill att ditt svar ska returnera något som inte renderas via en Django-mall behöver du göra det. Se exemplet med JSONResponseMixin för ett exempel.render_to_response()själv anroparget_template_names(), som standard kommer att leta upptemplate_namepå den klassbaserade vyn; två andra mixins (SingleObjectTemplateResponseMixinochMultipleObjectTemplateResponseMixin) åsidosätter detta för att ge mer flexibla standardvärden när man hanterar faktiska objekt.ContextMixinVarje inbyggd vy som behöver kontextdata, till exempel för att rendera en mall (inklusive
TemplateResponseMixinovan), bör anropaget_context_data()och skicka med de data den vill säkerställa finns där som nyckelordsargument.get_context_data()returnerar en ordbok; iContextMixinreturnerar den sina nyckelordsargument, men det är vanligt att åsidosätta detta för att lägga till fler poster 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 metoden get_object() som avgör objektet utifrån begärans URL (den letar efter nyckelordsargumenten pk och slug enligt deklarationen i URL-konfigurationen och slår upp objektet antingen från attributet model på vyn eller från attributet queryset om det anges). SingleObjectMixin åsidosätter också get_context_data(), som används i alla Djangos inbyggda klassbaserade vyer för att tillhandahålla kontextdata vid mallrendering.
För att sedan skapa en TemplateResponse använder DetailView SingleObjectTemplateResponseMixin, som utökar TemplateResponseMixin och åsidosätter get_template_names() enligt beskrivningen ovan. Den tillhandahåller faktiskt en ganska sofistikerad uppsättning alternativ, men det huvudsakliga alternativet som de flesta använder är <app_label>/<model_name>_detail.html. Delen _detail kan ändras genom att sätta template_name_suffix till något annat i en underklass. (Till exempel använder de generiska redigeringsvyerna _form för skapa- och uppdateringsvyer och _confirm_delete för borttagningsvyer.)
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 (och tillhandahåller platshållare om paginering är inaktiverad). Den förlitar sig på att object_list skickas med som ett nyckelordsargument, vilket ListView ordnar.
För att skapa en TemplateResponse använder ListView sedan MultipleObjectTemplateResponseMixin. Precis som SingleObjectTemplateResponseMixin ovan åsidosätter denna get_template_names() för att tillhandahålla en rad alternativ. Det vanligaste är <app_label>/<model_name>_list.html, där delen _list å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 visar detta med modellen Author som vi använde i introduktionen till generiska klassbaserade vyer.
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:
Bookqueryset för användning avListViewEftersom vi har tillgång till det förlag vars böcker vi vill lista, åsidosätter vi
get_queryset()och använder förlagets reverse foreign key manager.Publisher-queryset som används 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 mixiner eller vyer från en av grupperna med generiska klassbaserade vyer: detalj och lista, redigering och datum. Det går till exempel bra att kombinera TemplateView (inbyggd vy) med MultipleObjectMixin (generisk lista), men du kommer sannolikt att få problem om du kombinerar SingleObjectMixin (generisk detaljvy) med MultipleObjectMixin (generisk listvy).
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 när vi först introducerade AuthorDetailView; 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})
Till sist sammanför vi detta i en ny vy, AuthorView. Vi vet redan att ett anrop till as_view() på en klassbaserad vy ger oss något som beter sig precis som en funktionsbaserad vy, så det kan vi göra där vi väljer mellan de två undervyerna.
Du kan skicka vidare nyckelordsargument till as_view() på samma sätt som i URL-konfigurationen, till exempel om du vill att beteendet hos 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 metod render_to_json_response() med samma signatur som render_to_response(). För att använda den behöver vi till exempel blanda in den i en TemplateView och åsidosätta render_to_response() så att den i stället anropar render_to_json_response():
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 in en underklass till DetailView som kan returnera både HTML- och JSON-innehåll, beroende på någon egenskap hos HTTP-begäran, till exempel ett frågeargument eller en HTTP-header. Blanda in både JSONResponseMixin och SingleObjectTemplateResponseMixin, och åsidosätt implementationen av render_to_response() så att den delegerar till lämplig renderingsmetod beroende på vilken svarstyp 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 hur Python löser metodöverladdning kommer anropet till super().render_to_response(context) till slut att anropa implementationen av render_to_response() i TemplateResponseMixin.