Fixed #18804 - Reorganized class based views docs a bit; thanks anthonyb for the initial patch.

This commit is contained in:
Tim Graham 2012-08-25 08:11:56 -04:00
parent 01b9c3d519
commit df7c1a13a0
2 changed files with 150 additions and 153 deletions

View File

@ -11,8 +11,7 @@ to structure your views and reuse code by harnessing inheritance and
mixins. There are also some generic views for simple tasks which we'll mixins. There are also some generic views for simple tasks which we'll
get to later, but you may want to design your own structure of get to later, but you may want to design your own structure of
reusable views which suits your use case. For full details, see the reusable views which suits your use case. For full details, see the
:doc:`class-based views reference :doc:`class-based views reference documentation</ref/class-based-views/index>`.
documentation</ref/class-based-views/index>`.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
@ -32,19 +31,35 @@ redirect, and :class:`~django.views.generic.base.TemplateView` extends the base
to make it also render a template. to make it also render a template.
Simple usage Simple usage in your URLconf
============ ============================
Class-based generic views (and any class-based views that inherit from The simplest way to use generic views is to create them directly in your
the base classes Django provides) can be configured in two URLconf. If you're only changing a few simple attributes on a class-based view,
ways: subclassing, or passing in arguments directly in the URLconf. you can simply pass them into the ``as_view`` method call itself::
When you subclass a class-based view, you can override attributes from django.conf.urls import patterns, url, include
(such as the ``template_name``) or methods (such as ``get_context_data``) from django.views.generic import TemplateView
in your subclass to provide new values or methods. Consider, for example,
a view that just displays one template, ``about.html``. Django has a urlpatterns = patterns('',
generic view to do this - :class:`~django.views.generic.base.TemplateView` - (r'^about/', TemplateView.as_view(template_name="about.html")),
so we can just subclass it, and override the template name:: )
Any arguments given will override the ``template_name`` on the
A similar overriding pattern can be used for the ``url`` attribute on
:class:`~django.views.generic.base.RedirectView`.
Subclassing generic views
=========================
The second, more powerful way to use generic views is to inherit from an
existing view and override attributes (such as the ``template_name``) or
methods (such as ``get_context_data``) in your subclass to provide new values
or methods. Consider, for example, a view that just displays one template,
``about.html``. Django has a generic view to do this -
:class:`~django.views.generic.base.TemplateView` - so we can just subclass it,
and override the template name::
# some_app/views.py # some_app/views.py
from django.views.generic import TemplateView from django.views.generic import TemplateView
@ -52,9 +67,10 @@ so we can just subclass it, and override the template name::
class AboutView(TemplateView): class AboutView(TemplateView):
template_name = "about.html" template_name = "about.html"
Then, we just need to add this new view into our URLconf. As the class-based Then we just need to add this new view into our URLconf.
views themselves are classes, we point the URL to the ``as_view`` class method `~django.views.generic.base.TemplateView` is a class, not a function, so we
instead, which is the entry point for class-based views:: point the URL to the ``as_view`` class method instead, which provides a
function-like entry to class-based views::
# urls.py # urls.py
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
@ -64,104 +80,6 @@ instead, which is the entry point for class-based views::
(r'^about/', AboutView.as_view()), (r'^about/', AboutView.as_view()),
) )
Alternatively, if you're only changing a few simple attributes on a
class-based view, you can simply pass the new attributes into the ``as_view``
method call itself::
from django.conf.urls import patterns, url, include
from django.views.generic import TemplateView
urlpatterns = patterns('',
(r'^about/', TemplateView.as_view(template_name="about.html")),
)
A similar overriding pattern can be used for the ``url`` attribute on
:class:`~django.views.generic.base.RedirectView`.
.. _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.
"""
response_class = HttpResponse
def render_to_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
response_kwargs['content_type'] = 'application/json'
return self.response_class(
self.convert_context_to_json(context),
**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)
Now we mix this into the base TemplateView::
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
pass
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
:class:`JSONResponseMixin` with the
:class:`~django.views.generic.detail.BaseDetailView` -- (the
:class:`~django.views.generic.detail.DetailView` before template
rendering behavior has been mixed in)::
class JSONDetailView(JSONResponseMixin, BaseDetailView):
pass
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 :class:`JSONResponseMixin` and a
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
and override the implementation of :func:`render_to_response()` to defer
to the appropriate subclass depending on the type of response that the user
requested::
class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format','html') == 'json':
return JSONResponseMixin.render_to_response(self, context)
else:
return SingleObjectTemplateResponseMixin.render_to_response(self, context)
Because of the way that Python resolves method overloading, the local
``render_to_response()`` implementation will override the versions provided by
:class:`JSONResponseMixin` and
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
For more information on how to use the built in generic views, consult the next For more information on how to use the built in generic views, consult the next
topic on :doc:`generic class based views</topics/class-based-views/generic-display>`. topic on :doc:`generic class based views</topics/class-based-views/generic-display>`.
@ -171,16 +89,15 @@ Decorating class-based views
.. highlightlang:: python .. highlightlang:: python
The extension of class-based views isn't limited to using mixins. You Since class-based views aren't functions, decorating them works differently
can use also use decorators. depending on if you're using ``as_view`` or creating a subclass.
Decorating in URLconf Decorating in URLconf
--------------------- ---------------------
The simplest way of decorating class-based views is to decorate the The simplest way of decorating class-based views is to decorate the
result of the :meth:`~django.views.generic.base.View.as_view` method. result of the :meth:`~django.views.generic.base.View.as_view` method.
The easiest place to do this is in the URLconf where you deploy your The easiest place to do this is in the URLconf where you deploy your view::
view::
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView from django.views.generic import TemplateView

View File

@ -69,7 +69,7 @@ interface to working with templates in class-based views.
add more members to the dictionary. add more members to the dictionary.
Building up Django's generic class-based views Building up Django's generic class-based views
=============================================== ==============================================
Let's look at how two of Django's generic class-based views are built Let's look at how two of Django's generic class-based views are built
out of mixins providing discrete functionality. We'll consider out of mixins providing discrete functionality. We'll consider
@ -222,8 +222,7 @@ we'll want the functionality provided by
:class:`~django.views.generic.detail.SingleObjectMixin`. :class:`~django.views.generic.detail.SingleObjectMixin`.
We'll demonstrate this with the publisher modelling we used in the We'll demonstrate this with the publisher modelling we used in the
:doc:`generic class-based views :doc:`generic class-based views introduction<generic-display>`.
introduction<generic-display>`.
.. code-block:: python .. code-block:: python
@ -233,11 +232,11 @@ introduction<generic-display>`.
from django.views.generic import View from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from books.models import Author from books.models import Author
class RecordInterest(View, SingleObjectMixin): class RecordInterest(View, SingleObjectMixin):
"""Records the current user's interest in an author.""" """Records the current user's interest in an author."""
model = Author model = Author
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not request.user.is_authenticated(): if not request.user.is_authenticated():
return HttpResponseForbidden() return HttpResponseForbidden()
@ -256,9 +255,7 @@ 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 ``self.get_object()``. Everything else is taken care of for us by the
mixin. mixin.
We can hook this into our URLs easily enough: We can hook this into our URLs easily enough::
.. code-block:: python
# urls.py # urls.py
from books.views import RecordInterest from books.views import RecordInterest
@ -294,8 +291,6 @@ object. In order to do this, we need to have two different querysets:
We'll figure that out ourselves in :meth:`get_queryset()` so we We'll figure that out ourselves in :meth:`get_queryset()` so we
can take into account the Publisher we're looking at. can take into account the Publisher we're looking at.
.. highlightlang:: python
.. note:: .. note::
We have to think carefully about :meth:`get_context_data()`. We have to think carefully about :meth:`get_context_data()`.
@ -311,15 +306,15 @@ Now we can write a new :class:`PublisherDetail`::
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher from books.models import Publisher
class PublisherDetail(SingleObjectMixin, ListView): class PublisherDetail(SingleObjectMixin, ListView):
paginate_by = 2 paginate_by = 2
template_name = "books/publisher_detail.html" template_name = "books/publisher_detail.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['publisher'] = self.object kwargs['publisher'] = self.object
return super(PublisherDetail, self).get_context_data(**kwargs) return super(PublisherDetail, self).get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
self.object = self.get_object(Publisher.objects.all()) self.object = self.get_object(Publisher.objects.all())
return self.object.book_set.all() return self.object.book_set.all()
@ -339,26 +334,26 @@ have to create lots of books to see the pagination working! Here's the
template you'd want to use:: template you'd want to use::
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Publisher {{ publisher.name }}</h2> <h2>Publisher {{ publisher.name }}</h2>
<ol> <ol>
{% for book in page_obj %} {% for book in page_obj %}
<li>{{ book.title }}</li> <li>{{ book.title }}</li>
{% endfor %} {% endfor %}
</ol> </ol>
<div class="pagination"> <div class="pagination">
<span class="step-links"> <span class="step-links">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a> <a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %} {% endif %}
<span class="current"> <span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}. Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span> </span>
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a> <a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %} {% endif %}
@ -428,9 +423,9 @@ code so that on ``POST`` the form gets called appropriately.
the views implement :meth:`get()`, and things would get much more the views implement :meth:`get()`, and things would get much more
confusing. confusing.
Our new :class:`AuthorDetail` looks like this: .. highlightlang:: python
.. code-block:: python Our new :class:`AuthorDetail` looks like this::
# CAUTION: you almost certainly do not want to do this. # CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can # It is provided as part of a discussion of problems you can
@ -455,7 +450,7 @@ Our new :class:`AuthorDetail` looks like this:
'author-detail', 'author-detail',
kwargs = {'pk': self.object.pk}, kwargs = {'pk': self.object.pk},
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
form_class = self.get_form_class() form_class = self.get_form_class()
form = self.get_form(form_class) form = self.get_form(form_class)
@ -464,7 +459,7 @@ Our new :class:`AuthorDetail` looks like this:
} }
context.update(kwargs) context.update(kwargs)
return super(AuthorDetail, self).get_context_data(**context) return super(AuthorDetail, self).get_context_data(**context)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form_class = self.get_form_class() form_class = self.get_form_class()
form = self.get_form(form_class) form = self.get_form(form_class)
@ -472,14 +467,14 @@ Our new :class:`AuthorDetail` looks like this:
return self.form_valid(form) return self.form_valid(form)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
def form_valid(self, form): def form_valid(self, form):
if not self.request.user.is_authenticated(): if not self.request.user.is_authenticated():
return HttpResponseForbidden() return HttpResponseForbidden()
self.object = self.get_object() self.object = self.get_object()
# record the interest using the message in form.cleaned_data # record the interest using the message in form.cleaned_data
return super(AuthorDetail, self).form_valid(form) return super(AuthorDetail, self).form_valid(form)
:meth:`get_success_url()` is just providing somewhere to redirect to, :meth:`get_success_url()` is just providing somewhere to redirect to,
which gets used in the default implementation of which gets used in the default implementation of
:meth:`form_valid()`. We have to provide our own :meth:`post()` as :meth:`form_valid()`. We have to provide our own :meth:`post()` as
@ -525,21 +520,21 @@ write our own :meth:`get_context_data()` to make the
from django.views.generic import DetailView from django.views.generic import DetailView
from django import forms from django import forms
from books.models import Author from books.models import Author
class AuthorInterestForm(forms.Form): class AuthorInterestForm(forms.Form):
message = forms.CharField() message = forms.CharField()
class AuthorDisplay(DetailView): class AuthorDisplay(DetailView):
queryset = Author.objects.all() queryset = Author.objects.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'form': AuthorInterestForm(), 'form': AuthorInterestForm(),
} }
context.update(kwargs) context.update(kwargs)
return super(AuthorDisplay, self).get_context_data(**context) return super(AuthorDisplay, self).get_context_data(**context)
Then the :class:`AuthorInterest` is a simple :class:`FormView`, but we Then the :class:`AuthorInterest` is a simple :class:`FormView`, but we
have to bring in :class:`SingleObjectMixin` so we can find the author have to bring in :class:`SingleObjectMixin` so we can find the author
we're talking about, and we have to remember to set we're talking about, and we have to remember to set
@ -550,7 +545,7 @@ template as :class:`AuthorDisplay` is using on ``GET``.
from django.views.generic import FormView from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
class AuthorInterest(FormView, SingleObjectMixin): class AuthorInterest(FormView, SingleObjectMixin):
template_name = 'books/author_detail.html' template_name = 'books/author_detail.html'
form_class = AuthorInterestForm form_class = AuthorInterestForm
@ -561,13 +556,13 @@ template as :class:`AuthorDisplay` is using on ``GET``.
'object': self.get_object(), 'object': self.get_object(),
} }
return super(AuthorInterest, self).get_context_data(**context) return super(AuthorInterest, self).get_context_data(**context)
def get_success_url(self): def get_success_url(self):
return reverse( return reverse(
'author-detail', 'author-detail',
kwargs = {'pk': self.object.pk}, kwargs = {'pk': self.object.pk},
) )
def form_valid(self, form): def form_valid(self, form):
if not self.request.user.is_authenticated(): if not self.request.user.is_authenticated():
return HttpResponseForbidden() return HttpResponseForbidden()
@ -588,13 +583,13 @@ using a different template.
.. code-block:: python .. code-block:: python
from django.views.generic import View from django.views.generic import View
class AuthorDetail(View): class AuthorDetail(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
view = AuthorDisplay.as_view() view = AuthorDisplay.as_view()
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
view = AuthorInterest.as_view() view = AuthorInterest.as_view()
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
@ -603,3 +598,88 @@ This approach can also be used with any other generic class-based
views or your own class-based views inheriting directly from views or your own class-based views inheriting directly from
:class:`View` or :class:`TemplateView`, as it keeps the different :class:`View` or :class:`TemplateView`, as it keeps the different
views as separate as possible. 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.
"""
response_class = HttpResponse
def render_to_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
response_kwargs['content_type'] = 'application/json'
return self.response_class(
self.convert_context_to_json(context),
**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)
Now we mix this into the base TemplateView::
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
pass
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
:class:`JSONResponseMixin` with the
:class:`~django.views.generic.detail.BaseDetailView` -- (the
:class:`~django.views.generic.detail.DetailView` before template
rendering behavior has been mixed in)::
class JSONDetailView(JSONResponseMixin, BaseDetailView):
pass
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 :class:`JSONResponseMixin` and a
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
and override the implementation of :func:`render_to_response()` to defer
to the appropriate subclass depending on the type of response that the user
requested::
class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get('format','html') == 'json':
return JSONResponseMixin.render_to_response(self, context)
else:
return SingleObjectTemplateResponseMixin.render_to_response(self, context)
Because of the way that Python resolves method overloading, the local
``render_to_response()`` implementation will override the versions provided by
:class:`JSONResponseMixin` and
:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.