内置的基于类的通用视图

编写Web应用程序可能是单调的,因为我们一再重复某些模式。 Django试图在模型和模板层去掉一些单调,但是Web开发人员也在视图层次上体验了这种无聊。

Django的通用视图是为缓解这种痛苦而开发的。 他们采取一些在视图开发中常见的习惯用法和模式,并将它们抽象化,以便您可以快速编写数据的通用视图,而无需编写太多的代码。

我们可以识别某些常见任务,比如显示一个对象列表,并编写显示任何对象列表的代码。 那么这个模型可以作为一个额外的参数传递给URLconf。

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

  • 显示单个对象的列表和详细信息页面。 如果我们正在创建一个应用程序来管理会议,那么一个TalkListView和一个RegisteredUserListView就是列表视图的例子。 单个谈话页面就是我们称之为“细节”视图的一个例子。
  • 以年/月/日为基础的基于日期的对象归档页面,关联的详细信息和“最新”页面。
  • 允许用户创建,更新和删除对象 - 无论是否授权。

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

扩展通用视图

毫无疑问,使用通用视图可以大大加快开发速度。 然而,在大多数项目中,通用视图已经不能满足要求了。 事实上,新的Django开发人员提出的最常见的问题是如何使通用视图处理更广泛的情况。

这是为1.3版本重新设计通用视图的原因之一 - 以前,它们只是查看功能,有一系列令人困惑的选项;现在,不是在URLconf中传递大量配置,推荐扩展通用视图的方法是将它们继承,并覆盖它们的属性或方法。

也就是说,通用意见将会有一个限制。 如果您发现您正努力将视图实现为通用视图的子类,那么您可能会发现使用自己的基于类或功能的视图编写所需的代码更为有效。

通用视图的更多示例在某些第三方应用程序中可用,或者可以根据需要编写自己的视图。

对象的一般视图

TemplateView certainly is useful, but Django’s generic views really shine when it comes to presenting views of your database content. 由于这是一个常见的任务,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会从对象的名称中推断出一个模板。 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.

注意

Thus, when (for example) the APP_DIRS option of a DjangoTemplates backend is set to True in TEMPLATES, a template location could be: /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总是一个好主意。 设计模板的同事会感谢你。

添加额外的上下文

通常你只需要提供一些额外的信息,而不是通用视图提供的信息。 例如,考虑在每个发布者详细信息页面上显示所有图书的列表。 The DetailView generic view provides the publisher to the context, but how do we get additional information in that template?

答案是将DetailView继承下来,并提供您自己的get_context_data方法的实现。 默认的实现只是将正在显示的对象添加到模板,但是可以覆盖它以发送更多:

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().get_context_data(**kwargs)
        # Add in a QuerySet of all the books
        context['book_list'] = Book.objects.all()
        return context

注意

Generally, get_context_data will merge the context data of all parent classes with those of the current class. 为了在你想改变上下文的类中保持这种行为,你应该确保在超类上调用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()

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 Making queries for more information about QuerySet objects, and see the class-based views reference for the complete details).

举一个简单的例子,我们可能要按出版日期排列一个图书清单,最近的一个是:

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发布”的发布服务器。 通用视图在这种情况下有一个allow_empty参数。 有关更多详细信息,请参阅class-based-views reference

动态过滤

另一个常见的需求是通过URL中的某个键过滤列表页面中给定的对象。 之前我们在URLconf中对发布者的名字进行了硬编码,但是如果我们想写一个显示某个任意发布者的所有书籍的视图呢?

有用的是,ListView有一个我们可以覆盖的get_queryset()方法。 以前,它刚刚返回queryset属性的值,但是现在我们可以添加更多逻辑。

做这个工作的关键部分是,当调用基于类的视图时,各种有用的东西被存储在self;以及请求(self.request),这包括位置(self.args)和基于名称(self.kwargs)的参数根据URLconf捕获。

在这里,我们有一个带有一个捕获组的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()

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.

首先,我们需要在URLconf中添加一个作者详细信息位以指向一个自定义视图:

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 simply override it and wrap the call:

from django.views.generic import DetailView
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().get_object()
        # Record the last accessed date
        object.last_accessed = timezone.now()
        object.save()
        # Return the object
        return object

注意

此处的URLconf使用指定的组pk - 此名称是DetailView用于查找用于过滤查询集的主键值的默认名称。

如果您想要另外调用组,可以在视图上设置pk_url_kwarg 有关详细信息,请参见DetailView的参考资料