mirror of
https://github.com/django/django.git
synced 2024-11-29 22:56:46 +01:00
698 lines
30 KiB
Plaintext
698 lines
30 KiB
Plaintext
===================================
|
|
Using mixins with class-based views
|
|
===================================
|
|
|
|
.. caution::
|
|
|
|
This is an advanced topic. A working knowledge of :doc:`Django's
|
|
class-based views<index>` is advised before exploring these
|
|
techniques.
|
|
|
|
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 may
|
|
want to write a view that renders a template to make the HTTP
|
|
response, but you can't use
|
|
:class:`~django.views.generic.base.TemplateView`; perhaps you need to
|
|
render a template only on ``POST``, with ``GET`` doing something else
|
|
entirely. While you could use
|
|
:class:`~django.template.response.TemplateResponse` directly, this
|
|
will likely result in duplicate code.
|
|
|
|
For this reason, Django also provides a number of mixins that provide
|
|
more discrete functionality. Template rendering, for instance, is
|
|
encapsulated in the
|
|
:class:`~django.views.generic.base.TemplateResponseMixin`. The Django
|
|
reference documentation contains :doc:`full documentation of all the
|
|
mixins</ref/class-based-views/mixins>`.
|
|
|
|
Context and template responses
|
|
==============================
|
|
|
|
Two central mixins are provided that help in providing a consistent
|
|
interface to working with templates in class-based views.
|
|
|
|
:class:`~django.views.generic.base.TemplateResponseMixin`
|
|
Every built in view which returns a
|
|
:class:`~django.template.response.TemplateResponse` will call the
|
|
:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`
|
|
method that ``TemplateResponseMixin`` provides. Most of the time this
|
|
will be called for you (for instance, it is called by the ``get()`` method
|
|
implemented by both :class:`~django.views.generic.base.TemplateView` and
|
|
:class:`~django.views.generic.detail.DetailView`); 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 :ref:`JSONResponseMixin example
|
|
<jsonresponsemixin-example>`.
|
|
|
|
``render_to_response()`` itself calls
|
|
:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`,
|
|
which by default will just look up
|
|
:attr:`~django.views.generic.base.TemplateResponseMixin.template_name` on
|
|
the class-based view; two other mixins
|
|
(:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`
|
|
and
|
|
:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`)
|
|
override this to provide more flexible defaults when dealing with actual
|
|
objects.
|
|
|
|
.. versionadded:: 1.5
|
|
|
|
:class:`~django.views.generic.base.ContextMixin`
|
|
Every built in view which needs context data, such as for rendering a
|
|
template (including ``TemplateResponseMixin`` above), should call
|
|
:meth:`~django.views.generic.base.ContextMixin.get_context_data()` passing
|
|
any data they want to ensure is in there as keyword arguments.
|
|
``get_context_data()`` returns a dictionary; in ``ContextMixin`` it
|
|
simply returns its keyword arguments, but it is common to override this to
|
|
add more members to the dictionary.
|
|
|
|
Building up Django's generic class-based views
|
|
==============================================
|
|
|
|
Let's look at how two of Django's generic class-based views are built
|
|
out of mixins providing discrete functionality. We'll consider
|
|
:class:`~django.views.generic.detail.DetailView`, which renders a
|
|
"detail" view of an object, and
|
|
:class:`~django.views.generic.list.ListView`, which will render a list
|
|
of objects, typically from a queryset, and optionally paginate
|
|
them. This will introduce us to four mixins which between them provide
|
|
useful functionality when working with either a single Django object,
|
|
or multiple objects.
|
|
|
|
There are also mixins involved in the generic edit views
|
|
(:class:`~django.views.generic.edit.FormView`, and the model-specific
|
|
views :class:`~django.views.generic.edit.CreateView`,
|
|
:class:`~django.views.generic.edit.UpdateView` and
|
|
:class:`~django.views.generic.edit.DeleteView`), and in the
|
|
date-based generic views. These are
|
|
covered in the :doc:`mixin reference
|
|
documentation</ref/class-based-views/mixins>`.
|
|
|
|
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 a
|
|
:class:`~django.template.response.TemplateResponse` with a suitable template,
|
|
and that object as context.
|
|
|
|
To get the object, :class:`~django.views.generic.detail.DetailView`
|
|
relies on :class:`~django.views.generic.detail.SingleObjectMixin`,
|
|
which provides a
|
|
:meth:`~django.views.generic.detail.SingleObjectMixin.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
|
|
:attr:`~django.views.generic.detail.SingleObjectMixin.model` attribute
|
|
on the view, or the
|
|
:attr:`~django.views.generic.detail.SingleObjectMixin.queryset`
|
|
attribute if that's provided). ``SingleObjectMixin`` also overrides
|
|
:meth:`~django.views.generic.base.ContextMixin.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 :class:`~django.template.response.TemplateResponse`,
|
|
:class:`DetailView` uses
|
|
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
|
|
which extends :class:`~django.views.generic.base.TemplateResponseMixin`,
|
|
overriding
|
|
:meth:`~django.views.generic.base.TemplateResponseMixin.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>/<object_name>_detail.html``. The ``_detail`` part can be changed
|
|
by setting
|
|
:attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix`
|
|
on a subclass to something else. (For instance, the :doc:`generic edit
|
|
views<generic-editing>` 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 (possibly
|
|
paginated) list of objects, typically a
|
|
:class:`~django.db.models.query.QuerySet`, and then we need to make a
|
|
:class:`~django.template.response.TemplateResponse` with a suitable template
|
|
using that list of objects.
|
|
|
|
To get the objects, :class:`~django.views.generic.list.ListView` uses
|
|
:class:`~django.views.generic.list.MultipleObjectMixin`, which
|
|
provides both
|
|
:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`
|
|
and
|
|
:meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`. Unlike
|
|
with :class:`~django.views.generic.detail.SingleObjectMixin`, there's no need
|
|
to key off parts of the URL to figure out the queryset to work with, so the
|
|
default just uses the
|
|
:attr:`~django.views.generic.list.MultipleObjectMixin.queryset` or
|
|
:attr:`~django.views.generic.list.MultipleObjectMixin.model` attribute
|
|
on the view class. A common reason to override
|
|
:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`
|
|
here would be to dynamically vary the objects, such as depending on
|
|
the current user or to exclude posts in the future for a blog.
|
|
|
|
:class:`~django.views.generic.list.MultipleObjectMixin` also overrides
|
|
:meth:`~django.views.generic.base.ContextMixin.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 :class:`ListView` arranges for
|
|
it.
|
|
|
|
To make a :class:`~django.template.response.TemplateResponse`,
|
|
:class:`ListView` then uses
|
|
:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`;
|
|
as with :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`
|
|
above, this overrides ``get_template_names()`` to provide :meth:`a range of
|
|
options <django.views.generic.list.MultipleObjectTemplateResponseMixin>`,
|
|
with the most commonly-used being
|
|
``<app_label>/<object_name>_list.html``, with the ``_list`` part again
|
|
being taken from the
|
|
:attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix`
|
|
attribute. (The date based generic views use suffixes such as ``_archive``,
|
|
``_archive_year`` and so on to use different templates for the various
|
|
specialized date-based list views.)
|
|
|
|
Using Django's class-based view mixins
|
|
======================================
|
|
|
|
Now we've seen how Django's generic class-based views use the provided
|
|
mixins, let's look at other ways we can combine them. Of course we're
|
|
still going to be combining them with either built-in class-based
|
|
views, or other generic class-based views, but there are a range of
|
|
rarer problems you can solve than are provided for by Django out of
|
|
the box.
|
|
|
|
.. warning::
|
|
|
|
Not all mixins can be used together, and not all generic class
|
|
based views can be used with all other mixins. Here we present a
|
|
few examples that do work; if you want to bring together other
|
|
functionality then you'll have to consider interactions between
|
|
attributes and methods that overlap between the different classes
|
|
you're using, and how `method resolution order`_ will affect which
|
|
versions of the methods will be called in what order.
|
|
|
|
The reference documentation for Django's :doc:`class-based
|
|
views</ref/class-based-views/index>` and :doc:`class-based view
|
|
mixins</ref/class-based-views/mixins>` will help you in
|
|
understanding which attributes and methods are likely to cause
|
|
conflict between different classes and mixins.
|
|
|
|
If in doubt, it's often better to back off and base your work on
|
|
:class:`View` or :class:`TemplateView`, perhaps with
|
|
:class:`~django.views.generic.detail.SingleObjectMixin` and
|
|
:class:`~django.views.generic.list.MultipleObjectMixin`. Although you
|
|
will probably end up writing more code, it is more likely to be clearly
|
|
understandable to someone else coming to it later, and with fewer
|
|
interactions to worry about you will save yourself some thinking. (Of
|
|
course, you can always dip into Django's implementation of the generic
|
|
class based views for inspiration on how to tackle problems.)
|
|
|
|
.. _method resolution order: http://www.python.org/download/releases/2.3/mro/
|
|
|
|
|
|
Using SingleObjectMixin with View
|
|
---------------------------------
|
|
|
|
If we want to write a simple class-based view that responds only to
|
|
``POST``, we'll subclass :class:`~django.views.generic.base.View` and
|
|
write a ``post()`` method in the subclass. However if we want our
|
|
processing to work on a particular object, identified from the URL,
|
|
we'll want the functionality provided by
|
|
:class:`~django.views.generic.detail.SingleObjectMixin`.
|
|
|
|
We'll demonstrate this with the publisher modelling we used in the
|
|
:doc:`generic class-based views introduction<generic-display>`.
|
|
|
|
.. code-block:: python
|
|
|
|
# views.py
|
|
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
|
from django.core.urlresolvers import reverse
|
|
from django.views.generic import View
|
|
from django.views.generic.detail import SingleObjectMixin
|
|
from books.models import Author
|
|
|
|
class RecordInterest(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}))
|
|
|
|
In practice you'd probably want to record the interest in a key-value
|
|
store rather than in a relational database, so we've left that bit
|
|
out. The only bit of the view that needs to worry about using
|
|
:class:`~django.views.generic.detail.SingleObjectMixin` is where we want to
|
|
look up the author we're interested in, which it just does with a simple call
|
|
to ``self.get_object()``. Everything else is taken care of for us by the
|
|
mixin.
|
|
|
|
We can hook this into our URLs easily enough::
|
|
|
|
# urls.py
|
|
from django.conf.urls import patterns, url
|
|
from books.views import RecordInterest
|
|
|
|
urlpatterns = patterns('',
|
|
#...
|
|
url(r'^author/(?P<pk>\d+)/interest/$', RecordInterest.as_view(), name='author-interest'),
|
|
)
|
|
|
|
Note the ``pk`` named group, which
|
|
:meth:`~django.views.generic.detail.SingleObjectMixin.get_object` uses
|
|
to look up the ``Author`` instance. You could also use a slug, or
|
|
any of the other features of
|
|
:class:`~django.views.generic.detail.SingleObjectMixin`.
|
|
|
|
Using SingleObjectMixin with ListView
|
|
-------------------------------------
|
|
|
|
:class:`~django.views.generic.list.ListView` provides built-in
|
|
pagination, but you might want to paginate a list of objects that are
|
|
all linked (by a foreign key) to another object. In our publishing
|
|
example, you might want to paginate through all the books by a
|
|
particular publisher.
|
|
|
|
One way to do this is to combine :class:`ListView` with
|
|
:class:`~django.views.generic.detail.SingleObjectMixin`, so that the queryset
|
|
for the paginated list of books can hang off the publisher found as the single
|
|
object. In order to do this, we need to have two different querysets:
|
|
|
|
``Book`` queryset for use by :class:`~django.views.generic.list.ListView`
|
|
Since we have access to the ``Publisher`` whose books we want to list, we
|
|
simply override ``get_queryset()`` and use the ``Publisher``'s
|
|
:ref:`reverse foreign key manager<backwards-related-objects>`.
|
|
|
|
``Publisher`` queryset for use in :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()`
|
|
We'll rely on the default implementation of ``get_object()`` to fetch the
|
|
correct ``Publisher`` object.
|
|
However, we need to explicitly pass a ``queryset`` argument because
|
|
otherwise the default implementation of ``get_object()`` would call
|
|
``get_queryset()`` which we have overridden to return ``Book`` objects
|
|
instead of ``Publisher`` ones.
|
|
|
|
.. note::
|
|
|
|
We have to think carefully about ``get_context_data()``.
|
|
Since both :class:`~django.views.generic.detail.SingleObjectMixin` and
|
|
:class:`ListView` will
|
|
put things in the context data under the value of
|
|
``context_object_name`` if it's set, we'll instead explictly
|
|
ensure the ``Publisher`` is in the context data. :class:`ListView`
|
|
will add in the suitable ``page_obj`` and ``paginator`` for us
|
|
providing we remember to call ``super()``.
|
|
|
|
Now we can write a new ``PublisherDetail``::
|
|
|
|
from django.views.generic import ListView
|
|
from django.views.generic.detail import SingleObjectMixin
|
|
from books.models import Publisher
|
|
|
|
class PublisherDetail(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(PublisherDetail, self).get(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(PublisherDetail, self).get_context_data(**kwargs)
|
|
context['publisher'] = self.object
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
return self.object.book_set.all()
|
|
|
|
Notice how we set ``self.object`` within ``get()`` so we
|
|
can use it again later in ``get_context_data()`` and ``get_queryset()``.
|
|
If you don't set ``template_name``, the template will default to the normal
|
|
:class:`ListView` choice, which in this case would be
|
|
``"books/book_list.html"`` because it's a list of books;
|
|
:class:`ListView` knows nothing about
|
|
:class:`~django.views.generic.detail.SingleObjectMixin`, so it doesn't have
|
|
any clue this view is anything to do with a ``Publisher``.
|
|
|
|
The ``paginate_by`` is deliberately small in the example so you don't
|
|
have to create lots of books to see the pagination working! Here's the
|
|
template you'd want to use:
|
|
|
|
.. code-block:: html+django
|
|
|
|
{% 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 %}
|
|
|
|
Avoid anything more complex
|
|
===========================
|
|
|
|
Generally you can use
|
|
:class:`~django.views.generic.base.TemplateResponseMixin` and
|
|
:class:`~django.views.generic.detail.SingleObjectMixin` when you need
|
|
their functionality. As shown above, with a bit of care you can even
|
|
combine ``SingleObjectMixin`` with
|
|
:class:`~django.views.generic.list.ListView`. However things get
|
|
increasingly complex as you try to do so, and a good rule of thumb is:
|
|
|
|
.. hint::
|
|
|
|
Each of your views should use only mixins or views from one of the
|
|
groups of generic class-based views: :doc:`detail,
|
|
list<generic-display>`, :doc:`editing<generic-editing>` and
|
|
date. For example it's fine to combine
|
|
:class:`TemplateView` (built in view) with
|
|
:class:`~django.views.generic.list.MultipleObjectMixin` (generic list), but
|
|
you're likely to have problems combining ``SingleObjectMixin`` (generic
|
|
detail) with ``MultipleObjectMixin`` (generic list).
|
|
|
|
To show what happens when you try to get more sophisticated, we show
|
|
an example that sacrifices readability and maintainability when there
|
|
is a simpler solution. First, let's look at a naive attempt to combine
|
|
:class:`~django.views.generic.detail.DetailView` with
|
|
:class:`~django.views.generic.edit.FormMixin` to enable use to
|
|
``POST`` a Django :class:`~django.forms.Form` to the same URL as we're
|
|
displaying an object using :class:`DetailView`.
|
|
|
|
Using FormMixin with DetailView
|
|
-------------------------------
|
|
|
|
Think back to our earlier example of using :class:`View` and
|
|
:class:`~django.views.generic.detail.SingleObjectMixin` together. We were
|
|
recording a user's interest in a particular author; say now that we want to
|
|
let them leave a message saying why they like them. Again, let's assume we're
|
|
not going to store this in a relational database but instead in
|
|
something more esoteric that we won't worry about here.
|
|
|
|
At this point it's natural to reach for a :class:`~django.forms.Form` to
|
|
encapsulate the information sent from the user's browser to Django. Say also
|
|
that we're heavily invested in `REST`_, so we want to use the same URL for
|
|
displaying the author as for capturing the message from the
|
|
user. Let's rewrite our ``AuthorDetailView`` to do that.
|
|
|
|
.. _REST: http://en.wikipedia.org/wiki/Representational_state_transfer
|
|
|
|
We'll keep the ``GET`` handling from :class:`DetailView`, although
|
|
we'll have to add a :class:`~django.forms.Form` into the context data so we can
|
|
render it in the template. We'll also want to pull in form processing
|
|
from :class:`~django.views.generic.edit.FormMixin`, and write a bit of
|
|
code so that on ``POST`` the form gets called appropriately.
|
|
|
|
.. note::
|
|
|
|
We use :class:`~django.views.generic.edit.FormMixin` and implement
|
|
``post()`` ourselves rather than try to mix :class:`DetailView` with
|
|
:class:`FormView` (which provides a suitable ``post()`` already) because
|
|
both of the views implement ``get()``, and things would get much more
|
|
confusing.
|
|
|
|
Our new ``AuthorDetail`` looks like this::
|
|
|
|
# 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.core.urlresolvers 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 AuthorDetail(FormMixin, DetailView):
|
|
model = Author
|
|
form_class = AuthorInterestForm
|
|
|
|
def get_success_url(self):
|
|
return reverse('author-detail', kwargs={'pk': self.object.pk})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(AuthorDetail, self).get_context_data(**kwargs)
|
|
form_class = self.get_form_class()
|
|
context['form'] = self.get_form(form_class)
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if not request.user.is_authenticated():
|
|
return HttpResponseForbidden()
|
|
self.object = self.get_object()
|
|
form_class = self.get_form_class()
|
|
form = self.get_form(form_class)
|
|
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(AuthorDetail, self).form_valid(form)
|
|
|
|
``get_success_url()`` is just providing somewhere to redirect to,
|
|
which gets used in the default implementation of
|
|
``form_valid()``. We have to provide our own ``post()`` as
|
|
noted earlier, and override ``get_context_data()`` to make the
|
|
:class:`~django.forms.Form` available in the context data.
|
|
|
|
A better solution
|
|
-----------------
|
|
|
|
It should be obvious that the number of subtle interactions between
|
|
:class:`~django.views.generic.edit.FormMixin` and :class:`DetailView` is
|
|
already testing our ability to manage things. It's unlikely you'd want to
|
|
write this kind of class yourself.
|
|
|
|
In this case, it would be fairly easy to just write the ``post()``
|
|
method yourself, keeping :class:`DetailView` as the only generic
|
|
functionality, although writing :class:`~django.forms.Form` handling code
|
|
involves a lot of duplication.
|
|
|
|
Alternatively, it would still be easier than the above approach to
|
|
have a separate view for processing the form, which could use
|
|
:class:`~django.views.generic.edit.FormView` distinct from
|
|
:class:`DetailView` without concerns.
|
|
|
|
An alternative better solution
|
|
------------------------------
|
|
|
|
What we're really trying to do here is to use two different class
|
|
based views from the same URL. So why not do just that? We have a very
|
|
clear division here: ``GET`` requests should get the
|
|
:class:`DetailView` (with the :class:`~django.forms.Form` added to the context
|
|
data), and ``POST`` requests should get the :class:`FormView`. Let's
|
|
set up those views first.
|
|
|
|
The ``AuthorDisplay`` view is almost the same as :ref:`when we
|
|
first introduced AuthorDetail<generic-views-extra-work>`; we have to
|
|
write our own ``get_context_data()`` to make the
|
|
``AuthorInterestForm`` available to the template. We'll skip the
|
|
``get_object()`` override from before for clarity.
|
|
|
|
.. code-block:: python
|
|
|
|
from django.views.generic import DetailView
|
|
from django import forms
|
|
from books.models import Author
|
|
|
|
class AuthorInterestForm(forms.Form):
|
|
message = forms.CharField()
|
|
|
|
class AuthorDisplay(DetailView):
|
|
model = Author
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(AuthorDisplay, self).get_context_data(**kwargs)
|
|
context['form'] = AuthorInterestForm()
|
|
return context
|
|
|
|
Then the ``AuthorInterest`` is a simple :class:`FormView`, but we
|
|
have to bring in :class:`~django.views.generic.detail.SingleObjectMixin` so we
|
|
can find the author we're talking about, and we have to remember to set
|
|
``template_name`` to ensure that form errors will render the same
|
|
template as ``AuthorDisplay`` is using on ``GET``.
|
|
|
|
.. code-block:: python
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django.http import HttpResponseForbidden
|
|
from django.views.generic import FormView
|
|
from django.views.generic.detail import SingleObjectMixin
|
|
|
|
class AuthorInterest(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(AuthorInterest, self).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 ``AuthorDetail`` view. We
|
|
already know that calling :meth:`~django.views.generic.base.View.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 of course pass through keyword arguments to
|
|
:meth:`~django.views.generic.base.View.as_view()` in the same way you
|
|
would in your URLconf, such as if you wanted the ``AuthorInterest`` behavior
|
|
to also appear at another URL but using a different template.
|
|
|
|
.. code-block:: python
|
|
|
|
from django.views.generic import View
|
|
|
|
class AuthorDetail(View):
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
view = AuthorDisplay.as_view()
|
|
return view(request, *args, **kwargs)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
view = AuthorInterest.as_view()
|
|
return view(request, *args, **kwargs)
|
|
|
|
This approach can also be used with any other generic class-based
|
|
views or your own class-based views inheriting directly from
|
|
:class:`View` or :class:`TemplateView`, as it keeps the different
|
|
views as separate as possible.
|
|
|
|
.. _jsonresponsemixin-example:
|
|
|
|
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 of
|
|
rendered HTML.
|
|
|
|
We can create a mixin class to use in all of our views, handling the
|
|
conversion to JSON once.
|
|
|
|
For example, a simple JSON mixin might look something like this::
|
|
|
|
import json
|
|
from django.http import HttpResponse
|
|
|
|
class JSONResponseMixin(object):
|
|
"""
|
|
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 HttpResponse(
|
|
self.convert_context_to_json(context),
|
|
content_type='application/json',
|
|
**response_kwargs
|
|
)
|
|
|
|
def convert_context_to_json(self, context):
|
|
"Convert the context dictionary into a JSON object"
|
|
# 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 json.dumps(context)
|
|
|
|
.. note::
|
|
|
|
Check out the :doc:`/topics/serialization` documentation for more
|
|
information on how to correctly transform Django models and querysets into
|
|
JSON.
|
|
|
|
This mixin provides a ``render_to_json_response()`` method with the same signature
|
|
as :func:`~django.views.generic.base.TemplateResponseMixin.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::
|
|
|
|
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)
|
|
|
|
Equally we could use our mixin with one of the generic views. We can make our
|
|
own version of :class:`~django.views.generic.detail.DetailView` by mixing
|
|
``JSONResponseMixin`` with the
|
|
``django.views.generic.detail.BaseDetailView`` -- (the
|
|
:class:`~django.views.generic.detail.DetailView` before template
|
|
rendering behavior has been mixed 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)
|
|
|
|
This view can then be deployed in the same way as any other
|
|
:class:`~django.views.generic.detail.DetailView`, with exactly the
|
|
same behavior -- except for the format of the response.
|
|
|
|
If you want to be really adventurous, you could even mix a
|
|
:class:`~django.views.generic.detail.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 a HTTP header. Just mix
|
|
in both the ``JSONResponseMixin`` and a
|
|
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
|
|
and override the implementation of
|
|
:func:`~django.views.generic.base.TemplateResponseMixin.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(HybridDetailView, self).render_to_response(context)
|
|
|
|
Because of the way that Python resolves method overloading, the call to
|
|
``super(HybridDetailView, self).render_to_response(context)`` ends up
|
|
calling the
|
|
:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`
|
|
implementation of :class:`~django.views.generic.base.TemplateResponseMixin`.
|