内置基于类的通用视图 — Django 文档

来自菜鸟教程
Django/docs/2.2.x/topics/class-based-views/generic-display
跳转至:导航、​搜索

内置基于类的通用视图

编写 Web 应用程序可能很单调,因为我们一次又一次地重复某些模式。 Django 试图消除模型层和模板层的一些单调性,但 Web 开发人员也在视图层体验到这种无聊。

Django 的 通用视图 是为了减轻这种痛苦而开发的。 它们采用视图开发中发现的某些常见习语和模式并将它们抽象出来,以便您可以快速编写常见的数据视图,而无需编写太多代码。

我们可以识别某些常见任务,例如显示对象列表,并编写显示 任何 对象列表的代码。 然后有问题的模型可以作为额外的参数传递给 URLconf。

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

  • 显示单个对象的列表和详细信息页面。 如果我们正在创建一个应用程序来管理会议,那么 TalkListViewRegisteredUserListView 将是列表视图的示例。 单个讨论页就是我们所说的“详细信息”视图的一个示例。
  • 在年/月/日存档页面、相关详细信息和“最新”页面中显示基于日期的对象。
  • 允许用户创建、更新和删除对象——无论是否获得授权。

总之,这些视图提供了简单的界面来执行开发人员遇到的最常见的任务。

扩展通用视图

毫无疑问,使用通用视图可以大大加快开发速度。 然而,在大多数项目中,通用视图有时会不再适用。 事实上,新的 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.py
from django.urls import path
from books.views import PublisherList

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

这就是我们需要编写的所有 Python 代码。 然而,我们仍然需要编写一个模板。 我们可以通过向视图添加 template_name 属性来明确告诉视图使用哪个模板,但是在没有显式模板的情况下,Django 将从对象的名称中推断出一个。 在这种情况下,推断的模板将是 "books/publisher_list.html"——“books”部分来自定义模型的应用程序的名称,而“publisher”位只是模型名称的小写版本。

笔记

因此,当(例如)DjangoTemplates 后端的 APP_DIRS 选项在 :setting:`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 %}

这就是它的全部内容。 通用视图的所有酷特性都来自更改通用视图上设置的属性。 通用视图参考 详细记录了所有通用视图及其选项; 本文档的其余部分将考虑一些您可以自定义和扩展通用视图的常用方法。

制作“友好”的模板上下文

您可能已经注意到,我们的示例发布商列表模板将所有发布商存储在名为 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 的更多信息,请参阅 进行查询 对象,有关完整详细信息,请参阅 基于类的视图参考 )。

举一个简单的例子,我们可能想按出版日期排序书籍列表,最新的在前:

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'

请注意,除了过滤后的 queryset,我们还使用了自定义模板名称。 如果我们不这样做,通用视图将使用与“vanilla”对象列表相同的模板,这可能不是我们想要的。

另请注意,这不是制作特定于出版商的书籍的一种非常优雅的方式。 如果我们想添加另一个发布者页面,我们需要在 URLconf 中再添加几行,而且不止几个发布者会变得不合理。 我们将在下一节中处理这个问题。

笔记

如果您在请求 /books/acme/ 时收到 404,请检查以确保您确实拥有名为“ACME Publishing”的发布者。 对于这种情况,通用视图具有 allow_empty 参数。 有关更多详细信息,请参阅 基于类的视图参考


动态过滤

另一个常见的需求是通过 URL 中的某个键过滤列表页面中给出的对象。 之前我们在 URLconf 中硬编码了出版商的名称,但是如果我们想编写一个视图来显示某个任意出版商的所有书籍怎么办?

方便的是,ListView 有一个 get_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)

如您所见,向查询集选择添加更多逻辑非常容易; 如果需要,我们可以使用 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 中添加一个作者详细信息位以指向自定义视图:

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 的参考中找到