内置基于类的通用视图

编写 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 PublisherListView(ListView):
    model = Publisher

最后将该视图挂接到您的 urls 中

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

urlpatterns = [
    path("publishers/", PublisherListView.as_view()),
]

这就是我们需要编写的全部 Python 代码。但是,我们仍然需要编写一个模板。我们可以通过向视图添加template_name属性来明确告诉视图使用哪个模板,但在没有显式模板的情况下,Django 会从对象名称推断出一个模板。在这种情况下,推断出的模板将为"books/publisher_list.html"——“books”部分来自定义模型的应用程序的名称,而“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 %}

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

创建“友好”模板上下文

您可能已经注意到,我们的示例发布者列表模板将所有发布者存储在一个名为object_list的变量中。虽然这可以正常工作,但它对模板作者来说并不那么“友好”:他们必须“仅仅知道”他们在这里处理的是发布者。

好吧,如果您正在处理模型对象,则此操作已为您完成。当您处理对象或查询集时,Django 能够使用模型类名称的小写版本填充上下文。这除了默认的object_list条目之外还提供,但包含完全相同的数据,即publisher_list

如果这仍然不是一个好的匹配,您可以手动设置上下文变量的名称。通用视图上的context_object_name属性指定要使用的上下文变量

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


class PublisherListView(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 PublisherDetailView(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 PublisherDetailView(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 BookListView(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 AcmeBookListView(ListView):
    context_object_name = "book_list"
    queryset = Book.objects.filter(publisher__name="ACME Publishing")
    template_name = "books/acme_list.html"

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

还要注意,这不是一种处理特定出版商书籍的优雅方法。如果我们想添加另一个出版商页面,我们需要在 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 PublisherBookListView

urlpatterns = [
    path("books/<publisher>/", PublisherBookListView.as_view()),
]

接下来,我们将编写PublisherBookListView视图本身。

# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher


class PublisherBookListView(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 中添加一个作者详细信息部分,以指向自定义视图。

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

返回顶部