Skip to main content

内置基于类的通用视图

编写Web应用程序可能是单调的,因为我们一遍又一遍地重复某些模式。 Django试图在模型层和模板层去除一些单调性,但是Web开发人员在视图级别也经历了这种无聊。

Django的 通用视图 是为了减轻疼痛而开发的。它们采用在视图开发中找到的某些常见习语和模式,并对它们进行抽象,以便您可以快速写入数据的常见视图,而无需编写太多代码。

我们可以识别某些常见任务,如显示对象列表,以及编写显示 any 对象列表的代码。然后,该模型可以作为一个额外的参数传递给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):              # __unicode__ on Python 2
        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):              # __unicode__ on Python 2
        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.conf.urls import url
from books.views import PublisherList

urlpatterns = [
    url(r'^publishers/$', PublisherList.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 %}

这真的是所有的。通用视图的所有酷功能来自于更改通用视图上设置的属性。 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 Publisher, Book

class PublisherDetail(DetailView):

    model = Publisher

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super(PublisherDetail, self).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 参数。有关详细信息,请参阅 class-based-views reference

动态过滤

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

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

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

这里,我们有一个URLconf与一个捕获的组:

# urls.py
from django.conf.urls import url
from books.views import PublisherBookList

urlpatterns = [
    url(r'^books/([\w-]+)/$', 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.args[0])
        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(PublisherBookList, self).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.conf.urls import url
from books.views import AuthorDetailView

urlpatterns = [
    #...
    url(r'^authors/(?P<pk>[0-9]+)/$', AuthorDetailView.as_view(), name='author-detail'),
]

然后我们写我们的新视图 - get_object 是检索对象的方法 - 所以我们简单地覆盖它并包装调用:

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(AuthorDetailView, self).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 的参考文献中找到