Django类视图笔记整理
Django基于类的视图
1.基于类的视图简介
基于类的视图使用Python 对象实现视图,它提供除函数视图之外的另外一种方式。它们不是替代基于函数的视图,但相对基于函数的视图,它们有以下不同和优点:
- HTTP 方法(GET、POST 等)可以有各自的方法,而不用通过条件分支来解决。
- 面向对象的技术例如Mixin(多继承)可以将代码分解成可重用的组件。
2.使用基于类的视图
2.1 基于类的视图的 GET 和 POST 方法
基于类的视图的核心是允许你用不同的实例方法来响应不同的HTTP请求方法,而不是在一个视图函数中使用条件分支代码来实现。
所以,视图函数中处理HTTP GET 的代码看上去将像:
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')
在基于类的视图中,它将变成:
from django.http import HttpResponse
from django.views.generic import View
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
2.2 URL解析
因为Django 的URL 解析器将请求和关联的参数发送给一个可调用的函数而不是一个类,所以基于类的视图有一个as_view() 类方法用来作为类的可调用入口。该as_view 入口点创建类的一个实例并调用dispatch() 方法。dispatch 查看请求是GET 还是POST 等等,并将请求转发给相应的方法,如果该方法没有定义则引发HttpResponseNotAllowed:
# urls.py
from django.conf.urls import url
from myapp.views import MyView
urlpatterns = [
url(r'^about/', MyView.as_view()),
]
值得注意的是,方法的返回值与基于函数的视图的返回值完全相同,即HttpResponse 的某种形式。这表示在基于类的视图中可以使用http快捷函数和TemplateResponse 对象。
2.3 设置类属性
然基于类的视图的最小实现不需要任何类属性来完成它的功能,但是在许多基于类的设计中类属性非常重要,有两种方式来设置类属性。
2.3.1 第一种方式是Python标准的方式,子类化并在子类中覆盖属性和方法。所以,如果父类有一个greeting 属性:
from django.http import HttpResponse
from django.views.generic import View
class GreetingView(View):
greeting = "Good Day"
def get(self, request):
return HttpResponse(self.greeting)
你可以在子类中覆盖它:
class MorningGreetingView(GreetingView):
greeting = "Morning to ya"
2.3.2 另外一种方式是在URLconf 中用as_view()调用的关键字参数配置类的属性:
urlpatterns = [
url(r'^about/', GreetingView.as_view(greeting="G'day")),
]
注:
对于每个请求都会实例化类的一个实例,但是as_view()入口点设置的类属性只在URL 第一次导入时配置。
3.使用 Mixin
Mixin 是多继承的一种形式,其来自多个父类的行为和属性可以组合在一起。
Mixin是重用多个类的代码的一种极好的方法,但是它们需要一些代价。代码在Mixin 中越分散,子类将越难阅读并知道它的行为;如果你的继承很深,将难以知道应该覆盖哪一个Mixin 的方法。
还要注意,只能继承一个通用视图 —— 也就是说,只能有一个父类继承View,其它的父类必须是Mixin。继承多个继承自View 的类 将不能像预期的那样工作——例如,试图在一个列表的顶部使用表单而组合ProcessFormView 和ListView。
3.1 封装as_view() 的Mixin
将共同的行为运用于多个类的一种方法是编写一个封装as_view() 方法的Mixin。
例如,如果你有许多通用视图,它们应该使用login_required() 装饰器,你可以这样实现一个Mixin:
from django.contrib.auth.decorators import login_required
class LoginRequiredMixin(object):
@classmethod
def as_view(cls, **initkwargs):
view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
return login_required(view)
class MyView(LoginRequiredMixin, ...):
# this is a generic view
...
4.基于类的视图处理表单
一个最基本的用于处理表单的视图函数可能是这样的:
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import MyForm
def myview(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
# <process form cleaned data>
return HttpResponseRedirect('/success/')
else:
form = MyForm(initial={'key': 'value'})
return render(request, 'form_template.html', {'form': form})
类似的一个基于类的视图看上去是这样:
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views.generic import View
from .forms import MyForm
class MyFormView(View):
form_class = MyForm
initial = {'key': 'value'}
template_name = 'form_template.html'
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
# <process form cleaned data>
return HttpResponseRedirect('/success/')
return render(request, self.template_name, {'form': form})
这是一个非常简单的情形,但你可以看到你将有机会自定义这个视图,例如通过URLconf 配置覆盖form_class 属性或者子类化并覆盖一个和多个方法。
5.装饰基于类的视图
基于类的视图的扩展不仅局限于使用Mixin。你还可以使用装饰器。由于基于类的视图不是函数,对它们的装饰取决于你使用as_view() 还是创建一个子类。
5.1 在URLconf 中装饰
装饰基于类的视图的最简单的方法是装饰as_view() 方法的结果。最方便的地方是URLconf 中部署视图的位置:
from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView
from .views import VoteView
urlpatterns = [
url(r'^about/', login_required(TemplateView.as_view(template_name="secret.html"))),
url(r'^vote/', permission_required('polls.can_vote')(VoteView.as_view())),
]
这个方法在每个实例的基础上运用装饰器。如果想让视图的每个实例都被装饰,你需要一种不同的方法。
5.2 装饰类
若要装饰基于类的视图的每个实例,你需要装饰类本身。可以将装饰器运用到类的dispatch() 方法上来实现这点。
类的方法和独立的函数不完全相同,所以你不可以直接将函数装饰器运用到方法上 —— 你首先需要将它转换成一个方法装饰器。method_decorator 装饰器将函数装饰器转换成方法装饰器,这样它就可以用于实例方法上。例如:
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
class ProtectedView(TemplateView):
template_name = 'secret.html'
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(ProtectedView, self).dispatch(*args, **kwargs)
在这个例子中,ProtectedView 的每个实例都将有登录保护。
注:
method_decorator 传递args 和*kwargs 参数给类上被装饰的方法。如果你的方法不接受与之兼容的参数集,它将引发一个TypeError 异常。
6.基于类的通用视图
编写Web应用可能是单调的,因为你需要不断的重复某一种模式。Django尝试从模型和模板层移除一些单调的情况,但是Web 开发者依然会在视图层经历这种厌烦。
Django的通用视图被开发用来消除这一痛苦。它们采用在视图开发过程中发现的某些共同的用法和模式然后把它们抽象出来,以便你能够写更少的代码快速的实现常见的视图。
Django使用通用视图完成下列功能:
- 为单一的对象展示列表和一个详细页面。如果我们创建一个应用来管理会议,那么一个TalkListView 和一个RegisteredUserListView 将是列表视图的例子。一个单独的讨论信息页面就是我们称之为 "详细" 视图的例子。
- 在年/月/日归档页面,以及详细页面和“最后发表”页面中,展示以日期为基础的对象。
- 允许用户创建,更新和删除对象 —— 以授权或者无需授权的方式。
总的来说,这些视图提供了一些简单的接口来完成开发者遇到的大多数的常见任务。
6.1 扩展通用视图
使用通用视图可以极大的提高开发速度,是毫无疑问的。 然而在大多数工程中, 总会遇到通用视图无法满足需求的时候。的确,大多数来自Django开发新手的问题是如何能使得通用视图的使用范围更广。
这就是说,通用视图有一些限制。如果你将你的视图实现为通用视图的子类,你就会发现这样能够更有效地编写你想要的代码,使用你自己的基于类或功能的视图。
在一些三方的应用中,有更多通用视图的示例,或者你可以自己按需编写。
6.2 对象的通用视图
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)
publication_date = models.DateField()
现在我们需要定义一个视图:
# views.py
from django.views.generic import ListView
from books.models import Publisher
class PublisherList(ListView):
model = Publisher
最后将视图解析到你的url上:
# urls.py
from django.conf.urls import url
from books.views import PublisherList
urlpatterns = [
url(r'^publishers/$', PublisherList.as_view()),
]
上面就是所有我们需要写的Python代码了。
注意:
所以,当(例如)DjangoTemplates后端的APP_DIRS选项在TEMPLATES中设置为True时,模板的位置应该为:/path/to/project/books/templates/books/publisher_list.html。
这个模板将会依据于一个上下文来渲染,这个上下文包含一个名为object_list包含所有publisher对象的变量。一个非常简单的模板可能看起来像下面这样:
{% extends "base.html" %}
{% block content %}
<h2>Publishers</h2>
<ul>
{% for publisher in object_list %}
<li>{{ publisher.name }}</li>
{% endfor %}
</ul>
{% endblock %}
这确实就是全部代码了。所有通用视图中有趣的特性来自于修改被传递到通用视图中的"信息" 字典。通用视图参考文档详细介绍了通用视图以及它的选项;本篇文档剩余的部分将会介绍自定义以及扩展通用 视图的常见方法。
6.3 使用“友好”的模板Context
你可能已经注意到,publisher_list 模板将所有Publisher 保存在object_list 变量中。虽然能正常工作,但这对模板作者并不"友好":他们“只知道”在这里要处理的是Publishers。
如果你在处理一个模型对象,Django 已经为你准备好。 当你处理一个普通对象或者Queryset 时,Django 能使用其模型类的小写名称来放入Context。实现方法是,除了默认的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总是个好主意。和你一起工作的设计模板的同事会感谢你的。
6.4 添加额外的上下文
你会经常需要展示一些通用视图不能提供的额外信息。 比如,考虑一下在每个Publisher 详细页面上显示一个包含所有图书的列表。DetailView 通用视图提供了Publisher 对象给上下文,但是我们如何在模板中获得附加信息呢?
答案是继承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。如果没有两个类尝试定义相同的键,它会返回预期的结果。然而,如果任何一个类尝试在父类持有一个键的情况下覆写它(在调用超类之后),这个类的任何子类都需要显式于父类之后设置它,如果你想要确保他们覆写了所有父类的话。如果你有这个麻烦,复查你视图中的方法调用顺序。
6.5 查看对象的子集
现在让我们来近距离查看下我们一直在用的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来定义一个过滤的对象列表,你可以更加详细 的了解哪些对象将会被显示在视图中(参见执行查询来获取更多关于查询集对象的更对信息,以及参见 基于类的视图参考来获取全部 细节)。
我们可能想要对图书列表按照出版日期进行排序来选择一个简单的例子,并且把 最近的放到前面:
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 的参数。详见基于类的视图参考。
6.6 动态过滤
另一个普遍的需求是在给定的列表页面中根据URL中的关键字来过滤对象。 前面我们把出版 商的名字硬编码到URLconf中,但是如果我们想要编写一个视图来展示任何publisher的所有 图书,应该如何处理?
相当方便的是, ListView 有一个get_queryset() 方法来供我们重写。在之前,它只是返回一个queryset属性值,但是现在我们可以添加更多的逻辑。
让这种方式能够工作的关键点,在于当类视图被调用时,各种有用的对象被存储在self上;同request(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)
如你所见,在queryset区域添加更多的逻辑非常容易;如果我们想的话,我们可以 使用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