mirror of
https://github.com/django/django.git
synced 2024-12-27 11:35:53 +00:00
606 lines
25 KiB
Plaintext
606 lines
25 KiB
Plaintext
|
===================================
|
||
|
Using mixins with class-based views
|
||
|
===================================
|
||
|
|
||
|
.. versionadded:: 1.3
|
||
|
|
||
|
.. 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 :class:`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.base.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 :class:`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 :class:`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:`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). :class:`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:`TemplateResponse`, :class:`DetailView` uses
|
||
|
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
|
||
|
which extends
|
||
|
:class:`~django.views.generic.base.TemplateResponseMixin`, overriding
|
||
|
:meth:`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:`QuerySet`, and then we need
|
||
|
to make a :class:`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:`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:`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:`TemplateResponse`, :class:`ListView` then uses
|
||
|
:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`;
|
||
|
as with :class:`SingleObjectTemplateResponseMixin` above, this
|
||
|
overrides :meth:`get_template_names()` to provide :meth:`a range of
|
||
|
options
|
||
|
<~django.views.generic.list.MultipleObjectTempalteResponseMixin>`,
|
||
|
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
|
||
|
specialised 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:`SimpleObjectMixin` and
|
||
|
:class:`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(View, SingleObjectMixin):
|
||
|
"""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:`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:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
# urls.py
|
||
|
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 :class:`Author` instance. You could also use a slug, or
|
||
|
any of the other features of :class:`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:`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:
|
||
|
|
||
|
**Publisher queryset for use in get_object**
|
||
|
We'll set that up directly when we call :meth:`get_object()`.
|
||
|
|
||
|
**Book queryset for use by ListView**
|
||
|
We'll figure that out ourselves in :meth:`get_queryset()` so we
|
||
|
can take into account the Publisher we're looking at.
|
||
|
|
||
|
.. highlightlang:: python
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
We have to think carefully about :meth:`get_context_data()`.
|
||
|
Since both :class:`SingleObjectMixin` and :class:`ListView` will
|
||
|
put things in the context data under the value of
|
||
|
:attr:`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 :class:`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_context_data(self, **kwargs):
|
||
|
kwargs['publisher'] = self.object
|
||
|
return super(PublisherDetail, self).get_context_data(**kwargs)
|
||
|
|
||
|
def get_queryset(self):
|
||
|
self.object = self.get_object(Publisher.objects.all())
|
||
|
return self.object.book_set.all()
|
||
|
|
||
|
Notice how we set ``self.object`` within :meth:`get_queryset` so we
|
||
|
can use it again later in :meth:`get_context_data`. If you don't set
|
||
|
:attr:`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:`SingleObjectMixin`, so
|
||
|
it doesn't have any clue this view is anything to do with a Publisher.
|
||
|
|
||
|
.. highlightlang:: html+django
|
||
|
|
||
|
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::
|
||
|
|
||
|
{% 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 :class:`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:`MultipleObjectMixin` (generic list), but you're likely to
|
||
|
have problems combining :class:`SingleObjectMixin` (generic
|
||
|
detail) with :class:`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:`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:`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:`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 :class:`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:`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:`FormMixin` and implement :meth:`post()` ourselves
|
||
|
rather than try to mix :class:`DetailView` with :class:`FormView`
|
||
|
(which provides a suitable :meth:`post()` already) because both of
|
||
|
the views implement :meth:`get()`, and things would get much more
|
||
|
confusing.
|
||
|
|
||
|
Our new :class:`AuthorDetail` looks like this:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
# 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
|
||
|
|
||
|
class AuthorInterestForm(forms.Form):
|
||
|
message = forms.CharField()
|
||
|
|
||
|
class AuthorDetail(DetailView, FormMixin):
|
||
|
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):
|
||
|
form_class = self.get_form_class()
|
||
|
form = self.get_form(form_class)
|
||
|
context = {
|
||
|
'form': form
|
||
|
}
|
||
|
context.update(kwargs)
|
||
|
return super(AuthorDetail, self).get_context_data(**context)
|
||
|
|
||
|
def post(self, request, *args, **kwargs):
|
||
|
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):
|
||
|
if not self.request.user.is_authenticated():
|
||
|
return HttpResponseForbidden()
|
||
|
self.object = self.get_object()
|
||
|
# record the interest using the message in form.cleaned_data
|
||
|
return super(AuthorDetail, self).form_valid(form)
|
||
|
|
||
|
:meth:`get_success_url()` is just providing somewhere to redirect to,
|
||
|
which gets used in the default implementation of
|
||
|
:meth:`form_valid()`. We have to provide our own :meth:`post()` as
|
||
|
noted earlier, and override :meth:`get_context_data()` to make the
|
||
|
:class:`Form` available in the context data.
|
||
|
|
||
|
A better solution
|
||
|
-----------------
|
||
|
|
||
|
It should be obvious that the number of subtle interactions between
|
||
|
:class:`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 :meth:`post()`
|
||
|
method yourself, keeping :class:`DetailView` as the only generic
|
||
|
functionality, although writing :class:`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:`Form` added to the context
|
||
|
data), and ``POST`` requests should get the :class:`FormView`. Let's
|
||
|
set up those views first.
|
||
|
|
||
|
The :class:`AuthorDisplay` view is almost the same as :ref:`when we
|
||
|
first introduced AuthorDetail<generic-views-extra-work>`; we have to
|
||
|
write our own :meth:`get_context_data()` to make the
|
||
|
:class:`AuthorInterestForm` available to the template. We'll skip the
|
||
|
:meth:`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):
|
||
|
|
||
|
queryset = Author.objects.all()
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = {
|
||
|
'form': AuthorInterestForm(),
|
||
|
}
|
||
|
context.update(kwargs)
|
||
|
return super(AuthorDisplay, self).get_context_data(**context)
|
||
|
|
||
|
Then the :class:`AuthorInterest` is a simple :class:`FormView`, but we
|
||
|
have to bring in :class:`SingleObjectMixin` so we can find the author
|
||
|
we're talking about, and we have to remember to set
|
||
|
:attr:`template_name` to ensure that form errors will render the same
|
||
|
template as :class:`AuthorDisplay` is using on ``GET``.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from django.views.generic import FormView
|
||
|
from django.views.generic.detail import SingleObjectMixin
|
||
|
|
||
|
class AuthorInterest(FormView, SingleObjectMixin):
|
||
|
template_name = 'books/author_detail.html'
|
||
|
form_class = AuthorInterestForm
|
||
|
model = Author
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = {
|
||
|
'object': self.get_object(),
|
||
|
}
|
||
|
return super(AuthorInterest, self).get_context_data(**context)
|
||
|
|
||
|
def get_success_url(self):
|
||
|
return reverse(
|
||
|
'author-detail',
|
||
|
kwargs = {'pk': self.object.pk},
|
||
|
)
|
||
|
|
||
|
def form_valid(self, form):
|
||
|
if not self.request.user.is_authenticated():
|
||
|
return HttpResponseForbidden()
|
||
|
self.object = self.get_object()
|
||
|
# record the interest using the message in form.cleaned_data
|
||
|
return super(AuthorInterest, self).form_valid(form)
|
||
|
|
||
|
Finally we bring this together in a new :class:`AuthorDetail` view. We
|
||
|
already know that calling :meth:`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:`as_view()`
|
||
|
in the same way you would in your URLconf, such as if you wanted the
|
||
|
:class:`AuthorInterest` behaviour 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.
|