内置的基于类的通用视图

编写 web 应用会很单调,因为我们一遍一遍地重复一些模式。Django 试图在模型和模板层面消除一些单调,但 web 开发者们仍然会在视图层感受到这种无聊。

Django 通用视图是为了缓解这种情况而被开发的。他们采用在视图开发时发现的某些通用的风格和模式,并把它们抽象化,因此你可能更快的编写公共的数据视图,而不是编写更多的代码。

我们可以识别出某些通用任务,比如显示对象列表,编写显示任何对象列表的代码。然后有问题的模型将被当做附加的参数传递给 URLconf。

Django 附带通用视图来执行以下操作:

  • 为单个对象显示列表和详情页。如果我们创建一个管理会议的应用,那么``TalkListView`` 和 RegisteredUserListView 就是列表视图的例子。单个"话题页"将作为例子中的"详情页"。
  • 在年/月/日的归档页面,相关的详情和最新页面将显示基于日期的对象。
  • 运行用户创建、更新和删除对象——无论是否授权。

总之,这些视图提供的接口来执行开发者们遇到的最常见的通用任务。

扩展通用视图

毫无疑问,使用通用视图可以大大加快开发速度。然而,在很多项目中,会出现通用视图不再适用。实际上,很多新手 Django 开发者问的最常见的问题是怎么让通用视图处理更大范围的情况。

这是通用视图在1.3版重新设计的原因之一——之前,它们只是具有各种选项的视图函数;现在,扩展通用视图的推荐方法是将它们子类化并且覆盖它们的属性或方法,而不是在 URLconf 中传递一个很庞杂的配置。

也就是说,通用视图有一个限制。如果你发现很难将实现的视图作为通用视图的子类,那么你可能会发现使用自己的基类或函数视图来编写你所需的代码会更有效率。

一些第三方应用里提供了很多通用视图案例,或者你可以编写你需要的通用视图。

对象的通用视图

TemplateView 当然也很常用,但 Django 通用视图在呈现数据库内容视图时确实很出色。因为这是一个很常见任务,Django 附带一些内置的通用视图来协助生成列表和对象的详情视图。

让我们首先看一些显示对象列表或单独对象的例子。

我们将使用这些模型:

# 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()

现在我们需要定义一个视图:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher

最后将这个视图挂钩到你的urls:

# urls.py
from django.urls import path
from books.views import PublisherList

urlpatterns = [
    path('publishers/', PublisherList.as_view()),
]

这就是我们需要编写的所有代码。尽管我们仍然需要编写一个模板。我们可以给视图添加 template_name 属性来告诉视图使用哪个模板,但如果没有明确的模板,Django 将从对象名称中推断一个。在这个例子中,推断模板将是 "books/publisher_list.html" —— "books" 部分来自定义模型所属 app 名称,而 "publisher" 必须是模型名称的小写。

注解

因此,假如一个 DjangoTemplates 后端的 APP_DIRS 选项在 TEMPLATES 中被设为 True 时,模板地址将会是 /path/to/project/books/templates/books/publisher_list.html 。

这个模板将针对变量名为 object_list 的上下文进行渲染,这个变量包含所有的出版者对象。模板可以是这个样子:

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

这就是全部内容。通用视图的所有炫酷功能来自改变通用视图上的属性设置。generic views reference 文档里有所有的通用视图和选项的详细说明;这篇文档剩下的部分将介绍一些你可能需要自定义和扩展通用视图的常用办法。

制作"友好"的模板上下文

你可能已经注意到了例子中的出版者列表模板在变量名为 object_list 里保存了所有的出版者。尽管它工作正常,但它对模板作者并不是特别友好:他们必须在这里处理出版者信息。

如果你正在处理模型对象,这已经完成了。当你正在处理对象或查询,Django 使用小写的模型类名来填充上下文。这是除了默认的 object_list 类目之外提供的,但包含完全相同的数据,即 publisher_list

如果仍然匹配的不好,你可以手工设置上下文变量的名称。通用视图上的``context_object_name`` 属性指定要使用的上下文变量:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher
    context_object_name = 'my_favorite_publishers'

提供有用的 context_object_name 总是一个好主意。设计模板的合作者将会感激你。

添加额外的上下文

通常,你只需要提供通用视图所提供的信息之外的一些附加的信息。比如,打算在每一个出版者详情页上显示所有的书籍列表。DetailView 通用视图提供出版者至上下文,但是怎么在模板里获取更多的信息呢?

答案是子类化 DetailView ,并提供你实现的 get_context_data 方法。默认的实现只是将正在显示的对象增加到模板,但你需要覆盖它来发送更多信息:

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

注解

通常,get_context_data 将合并当前类的所有父类的上下文数据。要在你想要改变上下文的类中保留此行为,你应该确保在超类上调用了 get_context_data 。当没有两个类尝试去定义相同的键是,会给出正确的结果。然而,如果任何类打算在父类已经设置键(调用super后)后覆盖键,如果任何子类想确保覆盖了所有父类,那么就需要在调用super后显式地设置它。如果有问题,请查看视图的方法解析顺序。

另一个考虑是来自基于类的通用视图的上下文数据将覆盖由上下文处理器提供的数据;可以查看 get_context_data() 的例子。

查看对象的子集

现在让我们仔细观察我们一直在使用的 model 参数。model 参数指定了视图将对其进行操作的数据模型,可用于对单个对象或对象集合进行操作的所有通用视图上。然而,model 参数不仅仅用来指定视图操作对象,还可以使用 queryset 参数指定对象列表。

from django.views.generic import DetailView
from books.models import Publisher

class PublisherDetail(DetailView):

    context_object_name = 'publisher'
    queryset = Publisher.objects.all()

指定 model = Publisher 只是 queryset = Publisher.objects.all() 的简写。然而,通过使用 queryset 定义过滤的对象列表,你可以更加具体的了解在视图中可见的对象。(查看 执行查询 来获取更多 QuerySet 对象的信息,查看 class-based views reference 来获取完整信息)

举一个例子,我们想通过出版日期排序一个书籍列表,最新的排第一:

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'

这是一个很简单的例子,但很好的说明了问题。当然,你通常会想做比重新排序对象更多的操作。如果你想显示特定出版者的书籍列表,你可以使用相同技术:

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'

注意,和过滤的查询结果一起,我们还要指定自定义的模板名称。如果我们不这么做,通用视图将使用与 "vanilla" 对象列表相同的模板,这可能不是我们想要的。

还需要注意,这不是一个特别优雅的获取指定出版者书籍的方法。如果你想添加其他出版者页面,我们需要在URLconf中再添加几行,但如果多个出版者,这就变得不合理了。我们将在下一个部分来处理这个问题。

注解

如果你在请求 /books/acme/ 时得到了404页面,请检查确保有叫 'ACME Publishing' 的出版者。通用视图有一个 allow_empty 参数来解决这个问题。查看 class-based-views reference 来获取更多细节。

动态过滤

其他常见需求是通过URL中的某个键来过滤列表页面里的对象。之前我们在URLconf中硬编码了出版者的名字,但如果我们想编写一个显示任意出版者书籍的视图呢?

我们可以方便地覆盖 ListViewget_queryset() 方法。默认情况下,它返回 queryset 属性值,但现在我们可以用它来添加更多逻辑。

这项工作的关键部分是当基于类的视图被调用的时候,各种常用的东西被存储在 self 上,而且请求 (self.request) 根据 URLconf 抓取位置(self.args) 和基于名称 (self.kwargs) 的参数。

现在,我们有个带有单个抓取组的URLconf。

# urls.py
from django.urls import path
from books.views import PublisherBookList

urlpatterns = [
    path('books/<publisher>/', PublisherBookList.as_view()),
]

下一步,我们将编写 PublisherBookList 视图:

# 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)

可以很方便地使用 get_queryset 来给查询集添加逻辑。比如,我们可以使用 self.request.user 来过滤当前用户或其他更复杂的逻辑。

我们也可以同时添加出版者到上下文中,因此我们能在模板中使用它:

# ...

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

执行额外的任务

最后一个常见模式里,我们将看到涉及在调用通用视图前后执行一些附加任务。

想象在 Author 模型上有一个 last_accessed 字段,用来查看谁是最新查看作者的人:

# 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()

当然,通用的 DetailView 类不晓得关于这个字段的任何信息,但我们可以再次非常坚定的编写自定义视图来保持字段更新。

首先,我们需要在URLconf中添加一个作者详情的url指向自定义视图:

from django.urls import path
from books.views import AuthorDetailView

urlpatterns = [
    #...
    path('authors/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
]

然后我们编写一个新的视图——get_object 来查找对象——因此我们可以覆盖它并包装调用:

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

注解

URLconf 在这里使用组 pk ,这个名字是 DetailView 用来查找过滤查询集的主键值的默认名称。

如果你想调用其他组,你可以在视图上设置 pk_url_kwarg 。可以在 DetailView 上找到更多细节。