From c4c7fbcc0d9264beb931b45969fc0d8d655c4f83 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Mon, 11 Jun 2012 10:34:00 +0200 Subject: Fixed #18451 -- Vastly improved class based view documentation. Many thanks to Daniel Greenfeld, James Aylett, Marc Tamlyn, Simon Williams, Danilo Bargen and Luke Plant for their work on this. --- docs/topics/auth.txt | 7 +- docs/topics/class-based-views.txt | 624 ---------------------- docs/topics/class-based-views/generic-display.txt | 432 +++++++++++++++ docs/topics/class-based-views/generic-editing.txt | 205 +++++++ docs/topics/class-based-views/index.txt | 233 ++++++++ docs/topics/class-based-views/mixins.txt | 605 +++++++++++++++++++++ docs/topics/forms/index.txt | 2 + docs/topics/http/generic-views.txt | 2 +- docs/topics/index.txt | 2 +- 9 files changed, 1483 insertions(+), 629 deletions(-) delete mode 100644 docs/topics/class-based-views.txt create mode 100644 docs/topics/class-based-views/generic-display.txt create mode 100644 docs/topics/class-based-views/generic-editing.txt create mode 100644 docs/topics/class-based-views/index.txt create mode 100644 docs/topics/class-based-views/mixins.txt (limited to 'docs/topics') diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 63986ea70f..c0e56dbba0 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1450,9 +1450,10 @@ The permission_required decorator Limiting access to generic views -------------------------------- -To limit access to a :doc:`class-based generic view `, -decorate the :meth:`View.dispatch ` -method on the class. See :ref:`decorating-class-based-views` for details. +To limit access to a :doc:`class-based generic view +`, decorate the :meth:`View.dispatch +` method on the class. See +:ref:`decorating-class-based-views` for details. .. _permissions: diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt deleted file mode 100644 index 13c2b994e4..0000000000 --- a/docs/topics/class-based-views.txt +++ /dev/null @@ -1,624 +0,0 @@ -========================= -Class-based generic views -========================= - -.. versionadded:: 1.3 - -.. note:: - Prior to Django 1.3, generic views were implemented as functions. The - function-based implementation has been removed in favor of the - class-based approach described here. - -Writing Web applications can be monotonous, because we repeat certain patterns -again and again. Django tries to take away some of that monotony at the model -and template layers, but Web developers also experience this boredom at the view -level. - -Django's *generic views* were developed to ease that pain. They take certain -common idioms and patterns found in view development and abstract them so that -you can quickly write common views of data without having to write too much -code. - -We can recognize certain common tasks, like displaying a list of objects, and -write code that displays a list of *any* object. Then the model in question can -be passed as an extra argument to the URLconf. - -Django ships with generic views to do the following: - -* Perform common "simple" tasks: redirect to a different page and - render a given template. - -* Display list and detail pages for a single object. If we were creating an - application to manage conferences then a ``TalkListView`` and a - ``RegisteredUserListView`` would be examples of list views. A single - talk page is an example of what we call a "detail" view. - -* Present date-based objects in year/month/day archive pages, - associated detail, and "latest" pages. - -* Allow users to create, update, and delete objects -- with or - without authorization. - -Taken together, these views provide easy interfaces to perform the most common -tasks developers encounter. - - -Simple usage -============ - -Class-based generic views (and any class-based views that inherit from -the base classes Django provides) can be configured in two -ways: subclassing, or passing in arguments directly in the URLconf. - -When you subclass a class-based view, you can 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 - from django.views.generic import TemplateView - - class AboutView(TemplateView): - template_name = "about.html" - -Then, we just need to add this new view into our URLconf. As the class-based -views themselves are classes, we point the URL to the ``as_view`` class method -instead, which is the entry point for class-based views:: - - # urls.py - from django.conf.urls import patterns, url, include - from some_app.views import AboutView - - urlpatterns = patterns('', - (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`, another simple -generic view. - - -Generic views of objects -======================== - -:class:`~django.views.generic.base.TemplateView` certainly is useful, -but Django's generic views really shine when it comes to presenting -views of your database content. Because it's such a common task, -Django comes with a handful of built-in generic views that make -generating list and detail views of objects incredibly easy. - -Let's take a look at one of these generic views: the "object list" view. We'll -be using these models:: - - # models.py - from django.db import models - - class Publisher(models.Model): - name = models.CharField(max_length=30) - address = models.CharField(max_length=50) - city = models.CharField(max_length=60) - state_province = models.CharField(max_length=30) - country = models.CharField(max_length=50) - website = models.URLField() - - class Meta: - ordering = ["-name"] - - def __unicode__(self): - return self.name - - class Book(models.Model): - title = models.CharField(max_length=100) - authors = models.ManyToManyField('Author') - publisher = models.ForeignKey(Publisher) - publication_date = models.DateField() - -To build a list page of all publishers, we'd use a URLconf along these lines:: - - from django.conf.urls import patterns, url, include - from django.views.generic import ListView - from books.models import Publisher - - urlpatterns = patterns('', - (r'^publishers/$', ListView.as_view( - model=Publisher, - )), - ) - -That's all the Python code we need to write. We still need to write a template, -however. We could explicitly tell the view which template to use -by including a ``template_name`` key in the arguments to as_view, but in -the absence of an explicit template Django will infer one from the object's -name. In this case, the inferred template will be -``"books/publisher_list.html"`` -- the "books" part comes from the name of the -app that defines the model, while the "publisher" bit is just the lowercased -version of the model's name. - -.. note:: - Thus, when (for example) the :class:`django.template.loaders.app_directories.Loader` - template loader is enabled in :setting:`TEMPLATE_LOADERS`, the template - location would be:: - - /path/to/project/books/templates/books/publisher_list.html - -.. highlightlang:: html+django - -This template will be rendered against a context containing a variable called -``object_list`` that contains all the publisher objects. A very simple template -might look like the following:: - - {% extends "base.html" %} - - {% block content %} -

Publishers

-
    - {% for publisher in object_list %} -
  • {{ publisher.name }}
  • - {% endfor %} -
- {% endblock %} - -That's really all there is to it. All the cool features of generic views come -from changing the "info" dictionary passed to the generic view. The -:doc:`generic views reference` documents all the generic -views and their options in detail; the rest of this document will consider -some of the common ways you might customize and extend generic views. - - -Extending generic views -======================= - -.. highlightlang:: python - -There's no question that using generic views can speed up development -substantially. In most projects, however, there comes a moment when the -generic views no longer suffice. Indeed, the most common question asked by new -Django developers is how to make generic views handle a wider array of -situations. - -This is one of the reasons generic views were redesigned for the 1.3 release - -previously, they were just view functions with a bewildering array of options; -now, rather than passing in a large amount of configuration in the URLconf, -the recommended way to extend generic views is to subclass them, and override -their attributes or methods. - - -Making "friendly" template contexts ------------------------------------ - -You might have noticed that our sample publisher list template stores -all the publishers in a variable named ``object_list``. While this -works just fine, it isn't all that "friendly" to template authors: -they have to "just know" that they're dealing with publishers here. - -Well, if you're dealing with a model object, this is already done for -you. When you are dealing with an object or queryset, Django is able -to populate the context using the verbose name (or the plural verbose -name, in the case of a list of objects) of the object being displayed. -This is provided in addition to the default ``object_list`` entry, but -contains exactly the same data. - -If the verbose name (or plural verbose name) still isn't a good match, -you can manually set the name of the context variable. The -``context_object_name`` attribute on a generic view specifies the -context variable to use. In this example, we'll override it in the -URLconf, since it's a simple change: - -.. parsed-literal:: - - urlpatterns = patterns('', - (r'^publishers/$', ListView.as_view( - model=Publisher, - **context_object_name="publisher_list",** - )), - ) - -Providing a useful ``context_object_name`` is always a good idea. Your -coworkers who design templates will thank you. - - -Adding extra context --------------------- - -Often you simply need to present some extra information beyond that -provided by the generic view. For example, think of showing a list of -all the books on each publisher detail page. The -:class:`~django.views.generic.detail.DetailView` generic view provides -the publisher to the context, but it seems there's no way to get -additional information in that template. - -However, there is; you can subclass -:class:`~django.views.generic.detail.DetailView` and provide your own -implementation of the ``get_context_data`` method. The default -implementation of this that comes with -:class:`~django.views.generic.detail.DetailView` simply adds in the -object being displayed to the template, but you can override it to show -more:: - - from django.views.generic import DetailView - from books.models import Publisher, Book - - class PublisherDetailView(DetailView): - - context_object_name = "publisher" - model = Publisher - - def get_context_data(self, **kwargs): - # Call the base implementation first to get a context - context = super(PublisherDetailView, self).get_context_data(**kwargs) - # Add in a QuerySet of all the books - context['book_list'] = Book.objects.all() - return context - -.. note:: - - Generally, get_context_data will merge the context data of all parent classes - with those of the current class. To preserve this behavior in your own classes - where you want to alter the context, you should be sure to call - get_context_data on the super class. When no two classes try to define the same - key, this will give the expected results. However if any class attempts to - override a key after parent classes have set it (after the call to super), any - children of that class will also need to explictly set it after super if they - want to be sure to override all parents. - -Viewing subsets of objects --------------------------- - -Now let's take a closer look at the ``model`` argument we've been -using all along. The ``model`` argument, which specifies the database -model that the view will operate upon, is available on all the -generic views that operate on a single object or a collection of -objects. However, the ``model`` argument is not the only way to -specify the objects that the view will operate upon -- you can also -specify the list of objects using the ``queryset`` argument:: - - from django.views.generic import DetailView - from books.models import Publisher, Book - - class PublisherDetailView(DetailView): - - context_object_name = "publisher" - queryset = Publisher.objects.all() - -Specifying ``model = Publisher`` is really just shorthand for saying -``queryset = Publisher.objects.all()``. However, by using ``queryset`` -to define a filtered list of objects you can be more specific about the -objects that will be visible in the view (see :doc:`/topics/db/queries` -for more information about :class:`QuerySet` objects, and see the -:doc:`class-based views reference ` for the complete -details). - -To pick a simple example, we might want to order a list of books by -publication date, with the most recent first:: - - urlpatterns = patterns('', - (r'^publishers/$', ListView.as_view( - queryset=Publisher.objects.all(), - context_object_name="publisher_list", - )), - (r'^books/$', ListView.as_view( - queryset=Book.objects.order_by("-publication_date"), - context_object_name="book_list", - )), - ) - - -That's a pretty simple example, but it illustrates the idea nicely. Of course, -you'll usually want to do more than just reorder objects. If you want to -present a list of books by a particular publisher, you can use the same -technique (here, illustrated using subclassing rather than by passing arguments -in the URLconf):: - - from django.views.generic import ListView - from books.models import Book - - class AcmeBookListView(ListView): - - context_object_name = "book_list" - queryset = Book.objects.filter(publisher__name="Acme Publishing") - template_name = "books/acme_list.html" - -Notice that along with a filtered ``queryset``, we're also using a custom -template name. If we didn't, the generic view would use the same template as the -"vanilla" object list, which might not be what we want. - -Also notice that this isn't a very elegant way of doing publisher-specific -books. If we want to add another publisher page, we'd need another handful of -lines in the URLconf, and more than a few publishers would get unreasonable. -We'll deal with this problem in the next section. - -.. note:: - - If you get a 404 when requesting ``/books/acme/``, check to ensure you - actually have a Publisher with the name 'ACME Publishing'. Generic - views have an ``allow_empty`` parameter for this case. See the - :doc:`class-based-views reference` for more details. - - -Dynamic filtering ------------------ - -Another common need is to filter down the objects given in a list page by some -key in the URL. Earlier we hard-coded the publisher's name in the URLconf, but -what if we wanted to write a view that displayed all the books by some arbitrary -publisher? - -Handily, the ``ListView`` has a -:meth:`~django.views.generic.detail.ListView.get_queryset` method we can -override. Previously, it has just been returning the value of the ``queryset`` -attribute, but now we can add more logic. - -The key part to making this work is that when class-based views are called, -various useful things are stored on ``self``; as well as the request -(``self.request``) this includes the positional (``self.args``) and name-based -(``self.kwargs``) arguments captured according to the URLconf. - -Here, we have a URLconf with a single captured group:: - - from books.views import PublisherBookListView - - urlpatterns = patterns('', - (r'^books/(\w+)/$', PublisherBookListView.as_view()), - ) - -Next, we'll write the ``PublisherBookListView`` view itself:: - - from django.shortcuts import get_object_or_404 - from django.views.generic import ListView - from books.models import Book, Publisher - - class PublisherBookListView(ListView): - - context_object_name = "book_list" - template_name = "books/books_by_publisher.html" - - def get_queryset(self): - publisher = get_object_or_404(Publisher, name__iexact=self.args[0]) - return Book.objects.filter(publisher=publisher) - -As you can see, it's quite easy to add more logic to the queryset selection; -if we wanted, we could use ``self.request.user`` to filter using the current -user, or other more complex logic. - -We can also add the publisher into the context at the same time, so we can -use it in the template:: - - class PublisherBookListView(ListView): - - context_object_name = "book_list" - template_name = "books/books_by_publisher.html" - - def get_queryset(self): - self.publisher = get_object_or_404(Publisher, name__iexact=self.args[0]) - return Book.objects.filter(publisher=self.publisher) - - def get_context_data(self, **kwargs): - # Call the base implementation first to get a context - context = super(PublisherBookListView, self).get_context_data(**kwargs) - # Add in the publisher - context['publisher'] = self.publisher - return context - -Performing extra work ---------------------- - -The last common pattern we'll look at involves doing some extra work before -or after calling the generic view. - -Imagine we had a ``last_accessed`` field on our ``Author`` object that we were -using to keep track of the last time anybody looked at that author:: - - # models.py - - class Author(models.Model): - salutation = models.CharField(max_length=10) - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=40) - email = models.EmailField() - headshot = models.ImageField(upload_to='/tmp') - last_accessed = models.DateTimeField() - -The generic ``DetailView`` class, of course, wouldn't know anything about this -field, but once again we could easily write a custom view to keep that field -updated. - -First, we'd need to add an author detail bit in the URLconf to point to a -custom view: - -.. parsed-literal:: - - from books.views import AuthorDetailView - - urlpatterns = patterns('', - #... - **(r'^authors/(?P\\d+)/$', AuthorDetailView.as_view()),** - ) - -Then we'd write our new view -- ``get_object`` is the method that retrieves the -object -- so we simply override it and wrap the call:: - - import datetime - from books.models import Author - from django.views.generic import DetailView - from django.shortcuts import get_object_or_404 - - class AuthorDetailView(DetailView): - - queryset = Author.objects.all() - - def get_object(self): - # Call the superclass - object = super(AuthorDetailView, self).get_object() - # Record the last accessed date - object.last_accessed = datetime.datetime.now() - object.save() - # Return the object - return object - -.. note:: - - This code won't actually work unless you create a - ``books/author_detail.html`` template. - -.. note:: - - The URLconf here uses the named group ``pk`` - this name is the default - name that ``DetailView`` uses to find the value of the primary key used to - filter the queryset. - - If you want to change it, you'll need to do your own ``get()`` call - on ``self.queryset`` using the new named parameter from ``self.kwargs``. - -More than just HTML -------------------- - -So far, we've been focusing on rendering templates to generate -responses. However, that's not all generic views can do. - -Each generic view is composed out of a series of mixins, and each -mixin contributes a little piece of the entire view. Some of these -mixins -- such as -:class:`~django.views.generic.base.TemplateResponseMixin` -- are -specifically designed for rendering content to an HTML response using a -template. However, you can write your own mixins that perform -different rendering behavior. - -For example, a simple JSON mixin might look something like this:: - - import json - from django import http - - class JSONResponseMixin(object): - def render_to_response(self, context): - "Returns a JSON response containing 'context' as payload" - return self.get_json_response(self.convert_context_to_json(context)) - - def get_json_response(self, content, **httpresponse_kwargs): - "Construct an `HttpResponse` object." - return http.HttpResponse(content, - content_type='application/json', - **httpresponse_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) - -Then, you could build a JSON-returning -:class:`~django.views.generic.detail.DetailView` by mixing your -: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`. - -Decorating class-based views -============================ - -.. highlightlang:: python - -The extension of class-based views isn't limited to using mixins. You -can use also use decorators. - -Decorating in URLconf ---------------------- - -The simplest way of decorating class-based views is to decorate the -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 -view:: - - from django.contrib.auth.decorators import login_required, permission_required - from django.views.generic import TemplateView - - from .views import VoteView - - urlpatterns = patterns('', - (r'^about/', login_required(TemplateView.as_view(template_name="secret.html"))), - (r'^vote/', permission_required('polls.can_vote')(VoteView.as_view())), - ) - -This approach applies the decorator on a per-instance basis. If you -want every instance of a view to be decorated, you need to take a -different approach. - -.. _decorating-class-based-views: - -Decorating the class --------------------- - -To decorate every instance of a class-based view, you need to decorate -the class definition itself. To do this you apply the decorator to the -:meth:`~django.views.generic.base.View.dispatch` method of the class. - -A method on a class isn't quite the same as a standalone function, so -you can't just apply a function decorator to the method -- you need to -transform it into a method decorator first. The ``method_decorator`` -decorator transforms a function decorator into a method decorator so -that it can be used on an instance method. For example:: - - from django.contrib.auth.decorators import login_required - from django.utils.decorators import method_decorator - from django.views.generic import TemplateView - - class ProtectedView(TemplateView): - template_name = 'secret.html' - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super(ProtectedView, self).dispatch(*args, **kwargs) - -In this example, every instance of ``ProtectedView`` will have -login protection. - -.. note:: - - ``method_decorator`` passes ``*args`` and ``**kwargs`` - as parameters to the decorated method on the class. If your method - does not accept a compatible set of parameters it will raise a - ``TypeError`` exception. diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt new file mode 100644 index 0000000000..39fb41df04 --- /dev/null +++ b/docs/topics/class-based-views/generic-display.txt @@ -0,0 +1,432 @@ +.. _Generic views: + +========================= +Class-based generic views +========================= + +.. note:: + Prior to Django 1.3, generic views were implemented as functions. The + function-based implementation has been removed in favor of the + class-based approach described here. + +Writing Web applications can be monotonous, because we repeat certain patterns +again and again. Django tries to take away some of that monotony at the model +and template layers, but Web developers also experience this boredom at the view +level. + +Django's *generic views* were developed to ease that pain. They take certain +common idioms and patterns found in view development and abstract them so that +you can quickly write common views of data without having to write too much +code. + +We can recognize certain common tasks, like displaying a list of objects, and +write code that displays a list of *any* object. Then the model in question can +be passed as an extra argument to the URLconf. + +Django ships with generic views to do the following: + +* Display list and detail pages for a single object. If we were creating an + application to manage conferences then a ``TalkListView`` and a + ``RegisteredUserListView`` would be examples of list views. A single + talk page is an example of what we call a "detail" view. + +* Present date-based objects in year/month/day archive pages, + associated detail, and "latest" pages. + +* Allow users to create, update, and delete objects -- with or + without authorization. + +Taken together, these views provide easy interfaces to perform the most common +tasks developers encounter. + + +Extending generic views +======================= + +There's no question that using generic views can speed up development +substantially. In most projects, however, there comes a moment when the +generic views no longer suffice. Indeed, the most common question asked by new +Django developers is how to make generic views handle a wider array of +situations. + +This is one of the reasons generic views were redesigned for the 1.3 release - +previously, they were just view functions with a bewildering array of options; +now, rather than passing in a large amount of configuration in the URLconf, +the recommended way to extend generic views is to subclass them, and override +their attributes or methods. + +That said, generic views will have a limit. If you find you're struggling to +implement your view as a subclass of a generic view, then you may find it more +effective to write just the code you need, using your own class-based or +functional views. + +More examples of generic views are available in some third party applications, +or you could write your own as needed. + + +Generic views of objects +======================== + +:class:`~django.views.generic.base.TemplateView` certainly is useful, but +Django's generic views really shine when it comes to presenting views of your +database content. Because it's such a common task, Django comes with a handful +of built-in generic views that make generating list and detail views of objects +incredibly easy. + +Let's start by looking at some examples of showing a list of objects or an +individual object. + +.. comment: link here to the other topic pages (form handling, date based, mixins) + +We'll be using these models:: + + # models.py + from django.db import models + + class Publisher(models.Model): + name = models.CharField(max_length=30) + address = models.CharField(max_length=50) + city = models.CharField(max_length=60) + state_province = models.CharField(max_length=30) + country = models.CharField(max_length=50) + website = models.URLField() + + class Meta: + ordering = ["-name"] + + def __unicode__(self): + return self.name + + class Book(models.Model): + title = models.CharField(max_length=100) + authors = models.ManyToManyField('Author') + publisher = models.ForeignKey(Publisher) + publication_date = models.DateField() + +Now we need to define a view:: + + # views.py + from django.views.generic import ListView + from books.models import Publisher + + class PublisherList(ListView): + model = Publisher + +Finally hook that view into your urls:: + + # urls.py + from django.conf.urls import patterns, url, include + from books.views import PublisherList + + urlpatterns = patterns('', + url(r'^publishers/$', PublisherList.as_view()), + ) + +That's all the Python code we need to write. We still need to write a template, +however. We could explicitly tell the view which template to use by adding a +``template_name`` attribute to the view, but in the absence of an explicit +template Django will infer one from the object's name. In this case, the +inferred template will be ``"books/publisher_list.html"`` -- the "books" part +comes from the name of the app that defines the model, while the "publisher" +bit is just the lowercased version of the model's name. + +.. note:: + + Thus, when (for example) the + :class:`django.template.loaders.app_directories.Loader` template loader is + enabled in :setting:`TEMPLATE_LOADERS`, a template location could be: + /path/to/project/books/templates/books/publisher_list.html + +.. highlightlang:: html+django + +This template will be rendered against a context containing a variable called +``object_list`` that contains all the publisher objects. A very simple template +might look like the following:: + + {% extends "base.html" %} + + {% block content %} +

Publishers

+
    + {% for publisher in object_list %} +
  • {{ publisher.name }}
  • + {% endfor %} +
+ {% endblock %} + +That's really all there is to it. All the cool features of generic views come +from changing the attributes set on the generic view. The +:doc:`generic views reference` documents all the +generic views and their options in detail; the rest of this document will +consider some of the common ways you might customize and extend generic views. + + +Making "friendly" template contexts +----------------------------------- + +.. highlightlang:: python + +You might have noticed that our sample publisher list template stores all the +publishers in a variable named ``object_list``. While this works just fine, it +isn't all that "friendly" to template authors: they have to "just know" that +they're dealing with publishers here. + +Well, if you're dealing with a model object, this is already done for you. When +you are dealing with an object or queryset, Django is able to populate the +context using the lower cased version of the model class' name. This is +provided in addition to the default ``object_list`` entry, but contains exactly +the same data, i.e. ``publisher_list``. + +If the this still isn't a good match, you can manually set the name of the +context variable. The ``context_object_name`` attribute on a generic view +specifies the context variable to use:: + + # views.py + from django.views.generic import ListView + from books.models import Publisher + + class PublisherList(ListView): + model = Publisher + context_object_name = 'my_favourite_publishers' + +Providing a useful ``context_object_name`` is always a good idea. Your +coworkers who design templates will thank you. + + +Adding extra context +-------------------- + +Often you simply need to present some extra information beyond that +provided by the generic view. For example, think of showing a list of +all the books on each publisher detail page. The +:class:`~django.views.generic.detail.DetailView` generic view provides +the publisher to the context, but how do we get additional information +in that template. + +However, there is; you can subclass +:class:`~django.views.generic.detail.DetailView` and provide your own +implementation of the ``get_context_data`` method. The default +implementation of this that comes with +:class:`~django.views.generic.detail.DetailView` simply adds in the +object being displayed to the template, but you can override it to send +more:: + + from django.views.generic import DetailView + from books.models import Publisher, Book + + class PublisherDetail(DetailView): + + model = Publisher + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(PublisherDetailView, self).get_context_data(**kwargs) + # Add in a QuerySet of all the books + context['book_list'] = Book.objects.all() + return context + +.. note:: + + Generally, get_context_data will merge the context data of all parent + classes with those of the current class. To preserve this behavior in your + own classes where you want to alter the context, you should be sure to call + get_context_data on the super class. When no two classes try to define the + same key, this will give the expected results. However if any class + attempts to override a key after parent classes have set it (after the call + to super), any children of that class will also need to explictly set it + after super if they want to be sure to override all parents. If you're + having trouble, review the method resolution order of your view. + +.. _generic-views-list-subsets: + +Viewing subsets of objects +-------------------------- + +Now let's take a closer look at the ``model`` argument we've been +using all along. The ``model`` argument, which specifies the database +model that the view will operate upon, is available on all the +generic views that operate on a single object or a collection of +objects. However, the ``model`` argument is not the only way to +specify the objects that the view will operate upon -- you can also +specify the list of objects using the ``queryset`` argument:: + + from django.views.generic import DetailView + from books.models import Publisher, Book + + class PublisherDetail(DetailView): + + context_object_name = 'publisher' + queryset = Publisher.objects.all() + +Specifying ``model = Publisher`` is really just shorthand for saying +``queryset = Publisher.objects.all()``. However, by using ``queryset`` +to define a filtered list of objects you can be more specific about the +objects that will be visible in the view (see :doc:`/topics/db/queries` +for more information about :class:`QuerySet` objects, and see the +:doc:`class-based views reference ` for the +complete details). + +To pick a simple example, we might want to order a list of books by +publication date, with the most recent first:: + + from django.views.generic import ListView + from books.models import Book + + class BookList(ListView): + queryset = Book.objects.order_by('-publication_date') + context_object_name = 'book_list' + +That's a pretty simple example, but it illustrates the idea nicely. Of course, +you'll usually want to do more than just reorder objects. If you want to +present a list of books by a particular publisher, you can use the same +technique:: + + from django.views.generic import ListView + from books.models import Book + + class AcmeBookListView(ListView): + + context_object_name = 'book_list' + queryset = Book.objects.filter(publisher__name='Acme Publishing') + template_name = 'books/acme_list.html' + +Notice that along with a filtered ``queryset``, we're also using a custom +template name. If we didn't, the generic view would use the same template as the +"vanilla" object list, which might not be what we want. + +Also notice that this isn't a very elegant way of doing publisher-specific +books. If we want to add another publisher page, we'd need another handful of +lines in the URLconf, and more than a few publishers would get unreasonable. +We'll deal with this problem in the next section. + +.. note:: + + If you get a 404 when requesting ``/books/acme/``, check to ensure you + actually have a Publisher with the name 'ACME Publishing'. Generic + views have an ``allow_empty`` parameter for this case. See the + :doc:`class-based-views reference` for more + details. + + +Dynamic filtering +----------------- + +Another common need is to filter down the objects given in a list page by some +key in the URL. Earlier we hard-coded the publisher's name in the URLconf, but +what if we wanted to write a view that displayed all the books by some arbitrary +publisher? + +Handily, the ``ListView`` has a +:meth:`~django.views.generic.detail.ListView.get_queryset` method we can +override. Previously, it has just been returning the value of the ``queryset`` +attribute, but now we can add more logic. + +The key part to making this work is that when class-based views are called, +various useful things are stored on ``self``; as well as the request +(``self.request``) this includes the positional (``self.args``) and name-based +(``self.kwargs``) arguments captured according to the URLconf. + +Here, we have a URLconf with a single captured group:: + + # urls.py + from books.views import PublisherBookList + + urlpatterns = patterns('', + (r'^books/([\w-]+)/$', PublisherBookList.as_view()), + ) + +Next, we'll write the ``PublisherBookList`` view itself:: + + # views.py + from django.shortcuts import get_object_or_404 + from django.views.generic import ListView + from books.models import Book, Publisher + + class PublisherBookList(ListView): + + template_name = 'books/books_by_publisher.html' + + def get_queryset(self): + self.publisher = get_object_or_404(Publisher, name=self.args[0]) + return Book.objects.filter(publisher=self.publisher) + +As you can see, it's quite easy to add more logic to the queryset selection; +if we wanted, we could use ``self.request.user`` to filter using the current +user, or other more complex logic. + +We can also add the publisher into the context at the same time, so we can +use it in the template:: + + # ... + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(PublisherBookListView, self).get_context_data(**kwargs) + # Add in the publisher + context['publisher'] = self.publisher + return context + +.. _generic-views-extra-work: + +Performing extra work +--------------------- + +The last common pattern we'll look at involves doing some extra work before +or after calling the generic view. + +Imagine we had a ``last_accessed`` field on our ``Author`` object that we were +using to keep track of the last time anybody looked at that author:: + + # models.py + + class Author(models.Model): + salutation = models.CharField(max_length=10) + name = models.CharField(max_length=200) + email = models.EmailField() + headshot = models.ImageField(upload_to='/tmp') + last_accessed = models.DateTimeField() + +The generic ``DetailView`` class, of course, wouldn't know anything about this +field, but once again we could easily write a custom view to keep that field +updated. + +First, we'd need to add an author detail bit in the URLconf to point to a +custom view:: + + from books.views import AuthorDetailView + + urlpatterns = patterns('', + #... + url(r'^authors/(?P\\d+)/$', AuthorDetailView.as_view(), name='author-detail'), + ) + +Then we'd write our new view -- ``get_object`` is the method that retrieves the +object -- so we simply override it and wrap the call:: + + from django.views.generic import DetailView + from django.shortcuts import get_object_or_404 + from django.utils import timezone + from books.models import Author + + class AuthorDetailView(DetailView): + + queryset = Author.objects.all() + + def get_object(self): + # Call the superclass + object = super(AuthorDetailView, self).get_object() + # Record the last accessed date + object.last_accessed = timezone.now() + object.save() + # Return the object + return object + +.. note:: + + The URLconf here uses the named group ``pk`` - this name is the default + name that ``DetailView`` uses to find the value of the primary key used to + filter the queryset. + + If you want to call the group something else, you can set ``pk_url_kwarg`` + on the view. More details can be found in the reference for + :class:`~django.views.generic.detail.DetailView` diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt new file mode 100644 index 0000000000..23d346a32a --- /dev/null +++ b/docs/topics/class-based-views/generic-editing.txt @@ -0,0 +1,205 @@ +Form handling with class-based views +==================================== + +Form processing generally has 3 paths: + +* Initial GET (blank or prepopulated form) +* POST with invalid data (typically redisplay form with errors) +* POST with valid data (process the data and typically redirect) + +Implementing this yourself often results in a lot of repeated +boilerplate code (see :ref:`Using a form in a +view`). To help avoid this, Django provides a +collection of generic class-based views for form processing. + +Basic Forms +----------- + +Given a simple contact form:: + + # forms.py + from django import forms + + class ContactForm(forms.Form): + name = forms.CharField() + message = forms.CharField(widget=forms.Textarea) + + def send_email(self): + # send email using the self.cleaned_data dictionary + pass + +The view can be constructed using a FormView:: + + # views.py + from myapp.forms import ContactForm + from django.views.generic.edit import FormView + + class ContactView(FormView): + template_name = 'contact.html' + form_class = ContactForm + success_url = '/thanks/' + + def form_valid(self, form): + # This method is called when valid form data has been POSTed. + # It should return an HttpResponse. + form.send_email() + return super(ContactView, self).form_valid(form) + +Notes: + +* FormView inherits + :class:`~django.views.generic.base.TemplateResponseMixin` so + :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` + can be used here +* The default implementation for + :meth:`~django.views.generic.edit.FormView.form_valid` simply + redirects to the :attr:`success_url` + +Model Forms +----------- + +Generic views really shine when working with models. These generic +views will automatically create a :class:`ModelForm`, so long as they +can work out which model class to use: + +* If the :attr:`model` attribute is given, that model class will be used +* If :meth:`get_object()` returns an object, the class of that object + will be used +* If a :attr:`queryset` is given, the model for that queryset will be used + +Model form views provide a :meth:`form_valid()` implementation that +saves the model automatically. You can override this if you have any +special requirements; see below for examples. + +You don't even need to provide a attr:`success_url` for +:class:`~django.views.generic.edit.CreateView` or +:class:`~django.views.generic.edit.UpdateView` - they will use +:meth:`get_absolute_url()` on the model object if available. + +If you want to use a custom :class:`ModelForm` (for instance to add +extra validation) simply set +:attr:`~django.views.generic.edit.FormMixin.form_class` on your view. + +.. note:: + When specifying a custom form class, you must still specify the model, + even though the :attr:`form_class` may be a :class:`ModelForm`. + +First we need to add :meth:`get_absolute_url()` to our :class:`Author` +class: + +.. code-block:: python + + # models.py + from django import models + from django.core.urlresolvers import reverse + + class Author(models.Model): + name = models.CharField(max_length=200) + + def get_absolute_url(self): + return reverse('author-detail', kwargs={'pk': self.pk}) + +Then we can use :class:`CreateView` and friends to do the actual +work. Notice how we're just configuring the generic class-based views +here; we don't have to write any logic ourselves:: + + # views.py + from django.views.generic.edit import CreateView, UpdateView, DeleteView + from django.core.urlresolvers import reverse_lazy + from myapp.models import Author + + class AuthorCreate(CreateView): + model = Author + + class AuthorUpdate(UpdateView): + model = Author + + class AuthorDelete(DeleteView): + model = Author + success_url = reverse_lazy('author-list') + +.. note:: + We have to use :func:`~django.core.urlresolvers.reverse_lazy` here, not + just ``reverse`` as the urls are not loaded when the file is imported. + +Finally, we hook these new views into the URLconf:: + + # urls.py + from django.conf.urls import patterns, url + from myapp.views import AuthorCreate, AuthorUpdate, AuthorDelete + + urlpatterns = patterns('', + # ... + url(r'author/add/$', AuthorCreate.as_view(), name='author_add'), + url(r'author/(?P\d+)/$', AuthorUpdate.as_view(), name='author_update'), + url(r'author/(?P\d+)/delete/$', AuthorDelete.as_view(), name='author_delete'), + ) + +.. note:: + + These views inherit :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` + which uses :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_prefix` + to construct the + :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` + based on the model. + + In this example: + + * :class:`CreateView` and :class:`UpdateView` use ``myapp/author_form.html`` + * :class:`DeleteView` uses ``myapp/author_confirm_delete.html`` + + If you wish to have separate templates for :class:`CreateView` and + :class:1UpdateView`, you can set either :attr:`template_name` or + :attr:`template_name_suffix` on your view class. + +Models and request.user +----------------------- + +To track the user that created an object using a :class:`CreateView`, +you can use a custom :class:`ModelForm` to do this. First, add the +foreign key relation to the model:: + + # models.py + from django import models + from django.contrib.auth import User + + class Author(models.Model): + name = models.CharField(max_length=200) + created_by = models.ForeignKey(User) + + # ... + +Create a custom :class:`ModelForm` in order to exclude the +``created_by`` field and prevent the user from editing it: + +.. code-block:: python + + # forms.py + from django import forms + from myapp.models import Author + + class AuthorForm(forms.ModelForm): + class Meta: + model = Author + exclude = ('created_by',) + +In the view, use the custom :attr:`form_class` and override +:meth:`form_valid()` to add the user:: + + # views.py + from django.views.generic.edit import CreateView + from myapp.models import Author + from myapp.forms import AuthorForm + + class AuthorCreate(CreateView): + form_class = AuthorForm + model = Author + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super(AuthorCreate, self).form_valid(form) + +Note that you'll need to :ref:`decorate this +view` using +:func:`~django.contrib.auth.decorators.login_required`, or +alternatively handle unauthorised users in the :meth:`form_valid()`. diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt new file mode 100644 index 0000000000..bdf649da48 --- /dev/null +++ b/docs/topics/class-based-views/index.txt @@ -0,0 +1,233 @@ +================= +Class-based views +================= + +.. versionadded:: 1.3 + +A view is a callable which takes a request and returns a +response. This can be more than just a function, and Django provides +an example of some classes which can be used as views. These allow you +to structure your views and reuse code by harnessing inheritance and +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 +reusable views which suits your use case. For full details, see the +:doc:`class-based views reference +documentation`. + +.. toctree:: + :maxdepth: 1 + + generic-display + generic-editing + mixins + +Basic examples +============== + +Django provides base view classes which will suit a wide range of applications. +All views inherit from the :class:`~django.views.generic.base.View` class, which +handles linking the view in to the URLs, HTTP method dispatching and other +simple features. :class:`~django.views.generic.base.RedirectView` is for a simple HTTP +redirect, and :class:`~django.views.generic.base.TemplateView` extends the base class +to make it also render a template. + + +Simple usage +============ + +Class-based generic views (and any class-based views that inherit from +the base classes Django provides) can be configured in two +ways: subclassing, or passing in arguments directly in the URLconf. + +When you subclass a class-based view, you can 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 + from django.views.generic import TemplateView + + class AboutView(TemplateView): + template_name = "about.html" + +Then, we just need to add this new view into our URLconf. As the class-based +views themselves are classes, we point the URL to the ``as_view`` class method +instead, which is the entry point for class-based views:: + + # urls.py + from django.conf.urls import patterns, url, include + from some_app.views import AboutView + + urlpatterns = patterns('', + (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 use 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 import http + + class JSONResponseMixin(object): + """ + A mixin that can be used to render a JSON response. + """ + reponse_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 view:: + + from django.views.generic import View + + class JSONView(JSONResponseMixin, View): + 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 +topic on :doc:`generic class based views`. + +Decorating class-based views +============================ + +.. highlightlang:: python + +The extension of class-based views isn't limited to using mixins. You +can use also use decorators. + +Decorating in URLconf +--------------------- + +The simplest way of decorating class-based views is to decorate the +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 +view:: + + from django.contrib.auth.decorators import login_required, permission_required + from django.views.generic import TemplateView + + from .views import VoteView + + urlpatterns = patterns('', + (r'^about/', login_required(TemplateView.as_view(template_name="secret.html"))), + (r'^vote/', permission_required('polls.can_vote')(VoteView.as_view())), + ) + +This approach applies the decorator on a per-instance basis. If you +want every instance of a view to be decorated, you need to take a +different approach. + +.. _decorating-class-based-views: + +Decorating the class +-------------------- + +To decorate every instance of a class-based view, you need to decorate +the class definition itself. To do this you apply the decorator to the +:meth:`~django.views.generic.base.View.dispatch` method of the class. + +A method on a class isn't quite the same as a standalone function, so +you can't just apply a function decorator to the method -- you need to +transform it into a method decorator first. The ``method_decorator`` +decorator transforms a function decorator into a method decorator so +that it can be used on an instance method. For example:: + + from django.contrib.auth.decorators import login_required + from django.utils.decorators import method_decorator + from django.views.generic import TemplateView + + class ProtectedView(TemplateView): + template_name = 'secret.html' + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(ProtectedView, self).dispatch(*args, **kwargs) + +In this example, every instance of ``ProtectedView`` will have +login protection. + +.. note:: + + ``method_decorator`` passes ``*args`` and ``**kwargs`` + as parameters to the decorated method on the class. If your method + does not accept a compatible set of parameters it will raise a + ``TypeError`` exception. diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt new file mode 100644 index 0000000000..0c19d60e35 --- /dev/null +++ b/docs/topics/class-based-views/mixins.txt @@ -0,0 +1,605 @@ +=================================== +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` 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`. + +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 + `. + + ``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`. + +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 +``/_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` 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 +``/_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` and :doc:`class-based view + 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`. + +.. 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\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 %} +

Publisher {{ publisher.name }}

+ +
    + {% for book in page_obj %} +
  1. {{ book.title }}
  2. + {% endfor %} +
+ + + {% 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`, :doc:`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`; 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. diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index ff0665b13a..b843107871 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -82,6 +82,8 @@ If your form is going to be used to directly add or edit a Django model, you can use a :doc:`ModelForm ` to avoid duplicating your model description. +.. _using-a-form-in-a-view: + Using a form in a view ---------------------- diff --git a/docs/topics/http/generic-views.txt b/docs/topics/http/generic-views.txt index fdaa27dcd0..afdd014411 100644 --- a/docs/topics/http/generic-views.txt +++ b/docs/topics/http/generic-views.txt @@ -2,4 +2,4 @@ Generic views ============= -See :doc:`/ref/class-based-views`. +See :doc:`/ref/class-based-views/index`. diff --git a/docs/topics/index.txt b/docs/topics/index.txt index 00647bc8b7..72f5090b15 100644 --- a/docs/topics/index.txt +++ b/docs/topics/index.txt @@ -11,7 +11,7 @@ Introductions to all the key parts of Django you'll need to know: http/index forms/index templates - class-based-views + class-based-views/index files testing auth -- cgit v1.3