435 lines
16 KiB
Plaintext
435 lines
16 KiB
Plaintext
==================================
|
|
Built-in class-based generic views
|
|
==================================
|
|
|
|
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 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 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 to help generate list and detail views of objects.
|
|
|
|
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 __str__(self):
|
|
return self.name
|
|
|
|
class Author(models.Model):
|
|
salutation = models.CharField(max_length=10)
|
|
name = models.CharField(max_length=200)
|
|
email = models.EmailField()
|
|
headshot = models.ImageField(upload_to='author_headshots')
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Book(models.Model):
|
|
title = models.CharField(max_length=100)
|
|
authors = models.ManyToManyField('Author')
|
|
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
|
|
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.urls import path
|
|
from books.views import PublisherList
|
|
|
|
urlpatterns = [
|
|
path('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 the lowercased version of the model's name.
|
|
|
|
.. note::
|
|
|
|
Thus, when (for example) the ``APP_DIRS`` option of a ``DjangoTemplates``
|
|
backend is set to True in :setting:`TEMPLATES`, a template location could
|
|
be: /path/to/project/books/templates/books/publisher_list.html
|
|
|
|
This template will be rendered against a context containing a variable called
|
|
``object_list`` that contains all the publisher objects. A template might look
|
|
like this:
|
|
|
|
.. code-block:: html+django
|
|
|
|
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<h2>Publishers</h2>
|
|
<ul>
|
|
{% for publisher in object_list %}
|
|
<li>{{ publisher.name }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% 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</ref/class-based-views/index>` 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
|
|
-----------------------------------
|
|
|
|
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 lowercased 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 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_favorite_publishers'
|
|
|
|
Providing a useful ``context_object_name`` is always a good idea. Your
|
|
coworkers who design templates will thank you.
|
|
|
|
|
|
.. _adding-extra-context:
|
|
|
|
Adding extra context
|
|
--------------------
|
|
|
|
Often you 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?
|
|
|
|
The answer is to subclass :class:`~django.views.generic.detail.DetailView`
|
|
and provide your own implementation of the ``get_context_data`` method.
|
|
The default implementation adds 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 Book, Publisher
|
|
|
|
class PublisherDetail(DetailView):
|
|
|
|
model = Publisher
|
|
|
|
def get_context_data(self, **kwargs):
|
|
# Call the base implementation first to get a context
|
|
context = super().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 explicitly 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.
|
|
|
|
Another consideration is that the context data from class-based generic
|
|
views will override data provided by context processors; see
|
|
:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` for
|
|
an example.
|
|
|
|
.. _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
|
|
|
|
class PublisherDetail(DetailView):
|
|
|
|
context_object_name = 'publisher'
|
|
queryset = Publisher.objects.all()
|
|
|
|
Specifying ``model = Publisher`` is 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:`~django.db.models.query.QuerySet` objects, and see the
|
|
:doc:`class-based views reference </ref/class-based-views/index>` for the
|
|
complete details).
|
|
|
|
To pick an 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 minimal 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 AcmeBookList(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</ref/class-based-views/index>` 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.list.MultipleObjectMixin.get_queryset` method we
|
|
can override. By default, it returns the value of the ``queryset`` attribute,
|
|
but we can use it to 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 django.urls import path
|
|
from books.views import PublisherBookList
|
|
|
|
urlpatterns = [
|
|
path('books/<publisher>/', 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.kwargs['publisher'])
|
|
return Book.objects.filter(publisher=self.publisher)
|
|
|
|
Using ``get_queryset`` to add logic to the queryset selection is as convenient
|
|
as it is powerful. For instance, 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().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`` model that we were
|
|
using to keep track of the last time anybody looked at that author::
|
|
|
|
# models.py
|
|
from django.db import models
|
|
|
|
class Author(models.Model):
|
|
salutation = models.CharField(max_length=10)
|
|
name = models.CharField(max_length=200)
|
|
email = models.EmailField()
|
|
headshot = models.ImageField(upload_to='author_headshots')
|
|
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 django.urls import path
|
|
from books.views import AuthorDetailView
|
|
|
|
urlpatterns = [
|
|
#...
|
|
path('authors/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
|
|
]
|
|
|
|
Then we'd write our new view -- ``get_object`` is the method that retrieves the
|
|
object -- so we override it and wrap the call::
|
|
|
|
from django.utils import timezone
|
|
from django.views.generic import DetailView
|
|
from books.models import Author
|
|
|
|
class AuthorDetailView(DetailView):
|
|
|
|
queryset = Author.objects.all()
|
|
|
|
def get_object(self):
|
|
obj = super().get_object()
|
|
# Record the last accessed date
|
|
obj.last_accessed = timezone.now()
|
|
obj.save()
|
|
return obj
|
|
|
|
.. 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`
|