Använda mixins med klassbaserade vyerLink to this heading

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 svarsmallarLink to this heading

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 anropa render_to_response()-metoden som TemplateResponseMixin tillhandahåller. För det mesta kommer detta att anropas åt dig (till exempel anropas det av get()-metoden som implementeras av både TemplateView och DetailView); 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 anropar get_template_names(), som standard kommer att leta upp template_name på den klassbaserade vyn; två andra mixins (SingleObjectTemplateResponseMixin och MultipleObjectTemplateResponseMixin) å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 anropa get_context_data() och skicka alla data som de vill säkerställa finns där som nyckelordsargument. get_context_data() returnerar en ordbok; i ContextMixin 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 attributet extra_context.

Uppbyggnad av Djangos generiska klassbaserade vyerLink to this heading

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-objektLink to this heading

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 DetailViewSingleObjectMixin, 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 generic edit views _form för att skapa och uppdatera vyer, och _confirm_delete för att ta bort vyer)

ListView: arbeta med många Django-objektLink to this heading

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-mixinsLink to this heading

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.

Använda SingleObjectMixin med ViewLink to this heading

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 generic class-based views introduction.

Vyer.py
Python
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
Python
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 ListViewLink to this heading

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 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 reverse foreign key manager.

Publisher queryset för användning i get_object()

Vi kommer att förlita oss på standardimplementeringen av get_object() för att hämta rätt Publisher-objekt. Vi måste dock uttryckligen skicka ett queryset-argument eftersom standardimplementeringen av get_object() annars skulle anropa get_queryset() som vi har åsidosatt för att returnera Book-objekt istället för Publisher-objekt.

Nu kan vi skriva en ny PublisherDetailView:

Code
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:

Django template
{% 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 kompliceratLink to this heading

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:

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 DetailViewLink to this heading

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.

Vår nya AuthorDetailView ser ut så här:

Code
# 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ösningLink to this heading

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ösningLink to this heading

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:

Code
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:

Code
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:

Code
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 HTMLLink to this heading

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:

Code
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

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:

Code
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):

Code
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:

Code
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.