Använda mixins med klassbaserade vyer¶
Varning
This is an advanced topic. A working knowledge of Django’s class-based views is advised before exploring these techniques.
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.
TemplateResponseMixinEvery built in view which returns a
TemplateResponsewill call therender_to_response()method thatTemplateResponseMixinprovides. Most of the time this will be called for you (for instance, it is called by theget()method implemented by bothTemplateViewandDetailView); similarly, it’s unlikely that you’ll need to override it, although if you want your response to return something not rendered via a Django template then you’ll want to do it. For an example of this, see the JSONResponseMixin example.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.ContextMixinEvery built in view which needs context data, such as for rendering a template (including
TemplateResponseMixinabove), should callget_context_data()passing any data they want to ensure is in there as keyword arguments.get_context_data()returns a dictionary; inContextMixinit returns its keyword arguments, but it is common to override this to add more members to the dictionary. You can also use theextra_contextattribute.
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.
To get the object, DetailView
relies on SingleObjectMixin,
which provides a
get_object()
method that figures out the object based on the URL of the request (it
looks for pk and slug keyword arguments as declared in the
URLConf, and looks the object up either from the
model attribute
on the view, or the
queryset
attribute if that’s provided). SingleObjectMixin also overrides
get_context_data(),
which is used across all Django’s built in class-based views to supply
context data for template renders.
To then make a TemplateResponse,
DetailView uses
SingleObjectTemplateResponseMixin, which
extends TemplateResponseMixin, overriding
get_template_names() as
discussed above. It actually provides a fairly sophisticated set of options,
but the main one that most people are going to use is
<app_label>/<model_name>_detail.html. The _detail part can be changed
by setting
template_name_suffix
on a subclass to something else. (For instance, the generic edit views use _form for create and
update views, and _confirm_delete for delete views.)
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 also overrides
get_context_data() to
include appropriate context variables for pagination (providing
dummies if pagination is disabled). It relies on object_list being
passed in as a keyword argument, which ListView arranges for
it.
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.
We’ll demonstrate this with the Author model we used in the generic
class-based views introduction.
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.Publisherqueryset for use inget_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
Each of your views should use only mixins or views from one of the groups
of generic class-based views: detail, list, editing and date. For example it’s
fine to combine TemplateView (built in view) with
MultipleObjectMixin (generic list), but
you’re likely to have problems combining SingleObjectMixin (generic
detail) with MultipleObjectMixin (generic list).
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})
Finally we bring this together in a new AuthorView view. We
already know that calling as_view() on
a class-based view gives us something that behaves exactly like a function
based view, so we can do that at the point we choose between the two subviews.
You can pass through keyword arguments to
as_view() in the same way you
would in your URLconf, such as if you wanted the AuthorInterestFormView
behavior to also appear at another URL but using a different template:
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.
This mixin provides a render_to_json_response() method with the same
signature as
render_to_response().
To use it, we need to mix it into a TemplateView for example, and override
render_to_response() to call render_to_json_response() instead:
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.
If you want to be really adventurous, you could even mix a
DetailView subclass that is able
to return both HTML and JSON content, depending on some property of
the HTTP request, such as a query argument or an HTTP header. Mix in both the
JSONResponseMixin and a
SingleObjectTemplateResponseMixin,
and override the implementation of
render_to_response()
to defer to the appropriate rendering method depending on the type of response
that the user requested:
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)
Because of the way that Python resolves method overloading, the call to
super().render_to_response(context) ends up calling the
render_to_response()
implementation of TemplateResponseMixin.