Using mixins with class-based views

警告

This is an advanced topic. A working knowledge of Django'sclass-based views is advised before exploring thesetechniques.

Django's built-in class-based views provide a lot of functionality,but some of it you may want to use separately. For instance, you maywant to write a view that renders a template to make the HTTPresponse, but you can't useTemplateView; perhaps you need torender a template only on POST, with GET doing something elseentirely. While you could useTemplateResponse directly, thiswill likely result in duplicate code.

For this reason, Django also provides a number of mixins that providemore discrete functionality. Template rendering, for instance, isencapsulated in theTemplateResponseMixin. The Djangoreference documentation contains full documentation of all themixins.

Context and template responses

Two central mixins are provided that help in providing a consistentinterface to working with templates in class-based views.

  • TemplateResponseMixin
  • Every built in view which returns aTemplateResponse will call therender_to_response()method that TemplateResponseMixin provides. Most of the time thiswill be called for you (for instance, it is called by the get() methodimplemented by both TemplateView andDetailView); similarly, it's unlikelythat you'll need to override it, although if you want your response toreturn something not rendered via a Django template then you'll want to doit. For an example of this, see the JSONResponseMixin example.

render_to_response() itself callsget_template_names(),which by default will just look uptemplate_name onthe class-based view; two other mixins(SingleObjectTemplateResponseMixinandMultipleObjectTemplateResponseMixin)override this to provide more flexible defaults when dealing with actualobjects.

  • ContextMixin
  • Every built in view which needs context data, such as for rendering atemplate (including TemplateResponseMixin above), should callget_context_data() passingany data they want to ensure is in there as keyword arguments.get_context_data() returns a dictionary; in ContextMixin itsimply returns its keyword arguments, but it is common to override this toadd more members to the dictionary. You can also use theextra_context attribute.

Building up Django's generic class-based views

Let's look at how two of Django's generic class-based views are builtout of mixins providing discrete functionality. We'll considerDetailView, which renders a"detail" view of an object, andListView, which will render a listof objects, typically from a queryset, and optionally paginatethem. This will introduce us to four mixins which between them provideuseful functionality when working with either a single Django object,or multiple objects.

There are also mixins involved in the generic edit views(FormView, and the model-specificviews CreateView,UpdateView andDeleteView), and in thedate-based generic views. These arecovered in the mixin referencedocumentation.

DetailView: working with a single Django object

To show the detail of an object, we basically need to do two things:we need to look up the object and then we need to make aTemplateResponse with a suitable template,and that object as context.

To get the object, DetailViewrelies on SingleObjectMixin,which provides aget_object()method that figures out the object based on the URL of the request (itlooks for pk and slug keyword arguments as declared in theURLConf, and looks the object up either from themodel attributeon the view, or thequerysetattribute if that's provided). SingleObjectMixin also overridesget_context_data(),which is used across all Django's built in class-based views to supplycontext data for template renders.

To then make a TemplateResponse,DetailView usesSingleObjectTemplateResponseMixin,which extends TemplateResponseMixin,overridingget_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 changedby settingtemplate_name_suffixon a subclass to something else. (For instance, the generic editviews use _form for create and update views, and_confirm_delete for delete views.)

ListView: working with many Django objects

Lists of objects follow roughly the same pattern: we need a (possiblypaginated) list of objects, typically aQuerySet, and then we need to make aTemplateResponse with a suitable templateusing that list of objects.

To get the objects, ListView usesMultipleObjectMixin, whichprovides bothget_queryset()andpaginate_queryset(). Unlikewith SingleObjectMixin, there's no needto key off parts of the URL to figure out the queryset to work with, so thedefault just uses thequeryset ormodel attributeon the view class. A common reason to overrideget_queryset()here would be to dynamically vary the objects, such as depending onthe current user or to exclude posts in the future for a blog.

MultipleObjectMixin also overridesget_context_data() toinclude appropriate context variables for pagination (providingdummies if pagination is disabled). It relies on object_list beingpassed in as a keyword argument, which ListView arranges forit.

To make a TemplateResponse,ListView then usesMultipleObjectTemplateResponseMixin;as with SingleObjectTemplateResponseMixinabove, this overrides get_template_names() to provide a range of
options
,with the most commonly-used being<app_label>/<model_name>_list.html, with the _list part againbeing taken from thetemplate_name_suffixattribute. (The date based generic views use suffixes such as _archive,_archive_year and so on to use different templates for the variousspecialized date-based list views.)

Using Django's class-based view mixins

Now we've seen how Django's generic class-based views use the providedmixins, let's look at other ways we can combine them. Of course we'restill going to be combining them with either built-in class-basedviews, or other generic class-based views, but there are a range ofrarer problems you can solve than are provided for by Django out ofthe box.

警告

Not all mixins can be used together, and not all generic classbased views can be used with all other mixins. Here we present afew examples that do work; if you want to bring together otherfunctionality then you'll have to consider interactions betweenattributes and methods that overlap between the different classesyou're using, and how method resolution order will affect whichversions of the methods will be called in what order.

The reference documentation for Django's class-basedviews and class-based viewmixins will help you inunderstanding which attributes and methods are likely to causeconflict between different classes and mixins.

If in doubt, it's often better to back off and base your work onView or TemplateView, perhaps withSingleObjectMixin andMultipleObjectMixin. Although youwill probably end up writing more code, it is more likely to be clearlyunderstandable to someone else coming to it later, and with fewerinteractions to worry about you will save yourself some thinking. (Ofcourse, you can always dip into Django's implementation of the genericclass-based views for inspiration on how to tackle problems.)

Using SingleObjectMixin with View

If we want to write a simple class-based view that responds only toPOST, we'll subclass View andwrite a post() method in the subclass. However if we want ourprocessing to work on a particular object, identified from the URL,we'll want the functionality provided bySingleObjectMixin.

We'll demonstrate this with the Author model we used in thegeneric class-based views introduction.

views.py

  1. from django.http import HttpResponseForbidden, HttpResponseRedirect
  2. from django.urls import reverse
  3. from django.views import View
  4. from django.views.generic.detail import SingleObjectMixin
  5. from books.models import Author
  6.  
  7. class RecordInterest(SingleObjectMixin, View):
  8. """Records the current user's interest in an author."""
  9. model = Author
  10.  
  11. def post(self, request, *args, **kwargs):
  12. if not request.user.is_authenticated:
  13. return HttpResponseForbidden()
  14.  
  15. # Look up the author we're interested in.
  16. self.object = self.get_object()
  17. # Actually record interest somehow here!
  18.  
  19. return HttpResponseRedirect(reverse('author-detail', kwargs={'pk': self.object.pk}))

In practice you'd probably want to record the interest in a key-valuestore rather than in a relational database, so we've left that bitout. The only bit of the view that needs to worry about usingSingleObjectMixin is where we want tolook up the author we're interested in, which it just does with a simple callto self.get_object(). Everything else is taken care of for us by themixin.

We can hook this into our URLs easily enough:

urls.py

  1. from django.urls import path
  2. from books.views import RecordInterest
  3.  
  4. urlpatterns = [
  5. #...
  6. path('author/<int:pk>/interest/', RecordInterest.as_view(), name='author-interest'),
  7. ]

Note the pk named group, whichget_object() usesto look up the Author instance. You could also use a slug, orany of the other features ofSingleObjectMixin.

Using SingleObjectMixin with ListView

ListView provides built-inpagination, but you might want to paginate a list of objects that areall linked (by a foreign key) to another object. In our publishingexample, you might want to paginate through all the books by aparticular publisher.

One way to do this is to combine ListView withSingleObjectMixin, so that the querysetfor the paginated list of books can hang off the publisher found as the singleobject. In order to do this, we need to have two different querysets:

  • Book queryset for use by ListView
  • Since we have access to the Publisher whose books we want to list, wesimply override get_queryset() and use the Publisher’sreverse foreign key manager.
  • Publisher queryset for use in get_object()
  • We'll rely on the default implementation of get_object() to fetch thecorrect Publisher object.However, we need to explicitly pass a queryset argument becauseotherwise the default implementation of get_object() would callget_queryset() which we have overridden to return Book objectsinstead of Publisher ones.

注解

We have to think carefully about get_context_data().Since both SingleObjectMixin andListView willput things in the context data under the value ofcontext_object_name if it's set, we'll instead explicitlyensure the Publisher is in the context data. ListViewwill add in the suitable page_obj and paginator for usproviding we remember to call super().

Now we can write a new PublisherDetail:

  1. from django.views.generic import ListView
  2. from django.views.generic.detail import SingleObjectMixin
  3. from books.models import Publisher
  4.  
  5. class PublisherDetail(SingleObjectMixin, ListView):
  6. paginate_by = 2
  7. template_name = "books/publisher_detail.html"
  8.  
  9. def get(self, request, *args, **kwargs):
  10. self.object = self.get_object(queryset=Publisher.objects.all())
  11. return super().get(request, *args, **kwargs)
  12.  
  13. def get_context_data(self, **kwargs):
  14. context = super().get_context_data(**kwargs)
  15. context['publisher'] = self.object
  16. return context
  17.  
  18. def get_queryset(self):
  19. return self.object.book_set.all()

Notice how we set self.object within get() so wecan use it again later in get_context_data() and get_queryset().If you don't set template_name, the template will default to the normalListView choice, which in this case would be"books/book_list.html" because it's a list of books;ListView knows nothing aboutSingleObjectMixin, so it doesn't haveany clue this view is anything to do with a Publisher.

The paginate_by is deliberately small in the example so you don'thave to create lots of books to see the pagination working! Here's thetemplate you'd want to use:

  1. {% extends "base.html" %}
  2.  
  3. {% block content %}
  4. <h2>Publisher {{ publisher.name }}</h2>
  5.  
  6. <ol>
  7. {% for book in page_obj %}
  8. <li>{{ book.title }}</li>
  9. {% endfor %}
  10. </ol>
  11.  
  12. <div class="pagination">
  13. <span class="step-links">
  14. {% if page_obj.has_previous %}
  15. <a href="?page={{ page_obj.previous_page_number }}">previous</a>
  16. {% endif %}
  17.  
  18. <span class="current">
  19. Page {{ page_obj.number }} of {{ paginator.num_pages }}.
  20. </span>
  21.  
  22. {% if page_obj.has_next %}
  23. <a href="?page={{ page_obj.next_page_number }}">next</a>
  24. {% endif %}
  25. </span>
  26. </div>
  27. {% endblock %}

Avoid anything more complex

Generally you can useTemplateResponseMixin andSingleObjectMixin when you needtheir functionality. As shown above, with a bit of care you can evencombine SingleObjectMixin withListView. However things getincreasingly complex as you try to do so, and a good rule of thumb is:

提示

Each of your views should use only mixins or views from one of thegroups of generic class-based views: detail,list, editing anddate. For example it's fine to combineTemplateView (built in view) withMultipleObjectMixin (generic list), butyou're likely to have problems combining SingleObjectMixin (genericdetail) with MultipleObjectMixin (generic list).

To show what happens when you try to get more sophisticated, we showan example that sacrifices readability and maintainability when thereis a simpler solution. First, let's look at a naive attempt to combineDetailView withFormMixin to enable us toPOST a Django Form to the same URL as we'redisplaying an object using DetailView.

Using FormMixin with DetailView

Think back to our earlier example of using View andSingleObjectMixin together. We wererecording a user's interest in a particular author; say now that we want tolet them leave a message saying why they like them. Again, let's assume we'renot going to store this in a relational database but instead insomething more esoteric that we won't worry about here.

At this point it's natural to reach for a Form toencapsulate the information sent from the user's browser to Django. Say alsothat we're heavily invested in REST, so we want to use the same URL fordisplaying the author as for capturing the message from theuser. Let's rewrite our AuthorDetailView to do that.

We'll keep the GET handling from DetailView, althoughwe'll have to add a Form into the context data so we canrender it in the template. We'll also want to pull in form processingfrom FormMixin, and write a bit ofcode so that on POST the form gets called appropriately.

注解

We use FormMixin and implementpost() ourselves rather than try to mix DetailView withFormView (which provides a suitable post() already) becauseboth of the views implement get(), and things would get much moreconfusing.

Our new AuthorDetail looks like this:

  1. # CAUTION: you almost certainly do not want to do this.
  2. # It is provided as part of a discussion of problems you can
  3. # run into when combining different generic class-based view
  4. # functionality that is not designed to be used together.
  5.  
  6. from django import forms
  7. from django.http import HttpResponseForbidden
  8. from django.urls import reverse
  9. from django.views.generic import DetailView
  10. from django.views.generic.edit import FormMixin
  11. from books.models import Author
  12.  
  13. class AuthorInterestForm(forms.Form):
  14. message = forms.CharField()
  15.  
  16. class AuthorDetail(FormMixin, DetailView):
  17. model = Author
  18. form_class = AuthorInterestForm
  19.  
  20. def get_success_url(self):
  21. return reverse('author-detail', kwargs={'pk': self.object.pk})
  22.  
  23. def get_context_data(self, **kwargs):
  24. context = super().get_context_data(**kwargs)
  25. context['form'] = self.get_form()
  26. return context
  27.  
  28. def post(self, request, *args, **kwargs):
  29. if not request.user.is_authenticated:
  30. return HttpResponseForbidden()
  31. self.object = self.get_object()
  32. form = self.get_form()
  33. if form.is_valid():
  34. return self.form_valid(form)
  35. else:
  36. return self.form_invalid(form)
  37.  
  38. def form_valid(self, form):
  39. # Here, we would record the user's interest using the message
  40. # passed in form.cleaned_data['message']
  41. return super().form_valid(form)

get_success_url() is just providing somewhere to redirect to,which gets used in the default implementation ofform_valid(). We have to provide our own post() asnoted earlier, and override get_context_data() to make theForm available in the context data.

A better solution

It should be obvious that the number of subtle interactions betweenFormMixin and DetailView isalready testing our ability to manage things. It's unlikely you'd want towrite this kind of class yourself.

In this case, it would be fairly easy to just write the post()method yourself, keeping DetailView as the only genericfunctionality, although writing Form handling codeinvolves a lot of duplication.

Alternatively, it would still be easier than the above approach tohave a separate view for processing the form, which could useFormView distinct fromDetailView without concerns.

An alternative better solution

What we're really trying to do here is to use two different classbased views from the same URL. So why not do just that? We have a veryclear division here: GET requests should get theDetailView (with the Form added to the contextdata), and POST requests should get the FormView. Let'sset up those views first.

The AuthorDisplay view is almost the same as when wefirst introduced AuthorDetail; we have towrite our own get_context_data() to make theAuthorInterestForm available to the template. We'll skip theget_object() override from before for clarity:

  1. from django import forms
  2. from django.views.generic import DetailView
  3. from books.models import Author
  4.  
  5. class AuthorInterestForm(forms.Form):
  6. message = forms.CharField()
  7.  
  8. class AuthorDisplay(DetailView):
  9. model = Author
  10.  
  11. def get_context_data(self, **kwargs):
  12. context = super().get_context_data(**kwargs)
  13. context['form'] = AuthorInterestForm()
  14. return context

Then the AuthorInterest is a simple FormView, but wehave to bring in SingleObjectMixin so wecan find the author we're talking about, and we have to remember to settemplate_name to ensure that form errors will render the sametemplate as AuthorDisplay is using on GET:

  1. from django.http import HttpResponseForbidden
  2. from django.urls import reverse
  3. from django.views.generic import FormView
  4. from django.views.generic.detail import SingleObjectMixin
  5.  
  6. class AuthorInterest(SingleObjectMixin, FormView):
  7. template_name = 'books/author_detail.html'
  8. form_class = AuthorInterestForm
  9. model = Author
  10.  
  11. def post(self, request, *args, **kwargs):
  12. if not request.user.is_authenticated:
  13. return HttpResponseForbidden()
  14. self.object = self.get_object()
  15. return super().post(request, *args, **kwargs)
  16.  
  17. def get_success_url(self):
  18. return reverse('author-detail', kwargs={'pk': self.object.pk})

Finally we bring this together in a new AuthorDetail view. Wealready know that calling as_view() ona class-based view gives us something that behaves exactly like a functionbased view, so we can do that at the point we choose between the two subviews.

You can of course pass through keyword arguments toas_view() in the same way youwould in your URLconf, such as if you wanted the AuthorInterest behaviorto also appear at another URL but using a different template:

  1. from django.views import View
  2.  
  3. class AuthorDetail(View):
  4.  
  5. def get(self, request, *args, **kwargs):
  6. view = AuthorDisplay.as_view()
  7. return view(request, *args, **kwargs)
  8.  
  9. def post(self, request, *args, **kwargs):
  10. view = AuthorInterest.as_view()
  11. return view(request, *args, **kwargs)

This approach can also be used with any other generic class-basedviews or your own class-based views inheriting directly fromView or TemplateView, as it keeps the differentviews as separate as possible.

More than just HTML

Where class-based views shine is when you want to do the same thing many times.Suppose you're writing an API, and every view should return JSON instead ofrendered HTML.

We can create a mixin class to use in all of our views, handling theconversion to JSON once.

For example, a simple JSON mixin might look something like this:

  1. from django.http import JsonResponse
  2.  
  3. class JSONResponseMixin:
  4. """
  5. A mixin that can be used to render a JSON response.
  6. """
  7. def render_to_json_response(self, context, **response_kwargs):
  8. """
  9. Returns a JSON response, transforming 'context' to make the payload.
  10. """
  11. return JsonResponse(
  12. self.get_data(context),
  13. **response_kwargs
  14. )
  15.  
  16. def get_data(self, context):
  17. """
  18. Returns an object that will be serialized as JSON by json.dumps().
  19. """
  20. # Note: This is *EXTREMELY* naive; in reality, you'll need
  21. # to do much more complex handling to ensure that arbitrary
  22. # objects -- such as Django model instances or querysets
  23. # -- can be serialized as JSON.
  24. return context

注解

Check out the Serializing Django objects documentation for moreinformation on how to correctly transform Django models and querysets intoJSON.

This mixin provides a render_to_json_response() method with the same signatureas render_to_response().To use it, we simply need to mix it into a TemplateView for example,and override render_to_response() to call render_to_json_response() instead:

  1. from django.views.generic import TemplateView
  2.  
  3. class JSONView(JSONResponseMixin, TemplateView):
  4. def render_to_response(self, context, **response_kwargs):
  5. return self.render_to_json_response(context, **response_kwargs)

Equally we could use our mixin with one of the generic views. We can make ourown version of DetailView by mixingJSONResponseMixin with thedjango.views.generic.detail.BaseDetailView — (theDetailView before templaterendering behavior has been mixed in):

  1. from django.views.generic.detail import BaseDetailView
  2.  
  3. class JSONDetailView(JSONResponseMixin, BaseDetailView):
  4. def render_to_response(self, context, **response_kwargs):
  5. return self.render_to_json_response(context, **response_kwargs)

This view can then be deployed in the same way as any otherDetailView, with exactly thesame behavior — except for the format of the response.

If you want to be really adventurous, you could even mix aDetailView subclass that is ableto return both HTML and JSON content, depending on some property ofthe HTTP request, such as a query argument or a HTTP header. Just mixin both the JSONResponseMixin and aSingleObjectTemplateResponseMixin,and override the implementation ofrender_to_response()to defer to the appropriate rendering method depending on the type of responsethat the user requested:

  1. from django.views.generic.detail import SingleObjectTemplateResponseMixin
  2.  
  3. class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
  4. def render_to_response(self, context):
  5. # Look for a 'format=json' GET argument
  6. if self.request.GET.get('format') == 'json':
  7. return self.render_to_json_response(context)
  8. else:
  9. return super().render_to_response(context)

Because of the way that Python resolves method overloading, the call tosuper().render_to_response(context) ends up calling therender_to_response()implementation of TemplateResponseMixin.