之前有在用Django写一些小网站,现在暑假想说再来複习一下之前买的这本书
于是我就把它写成一系列的文章,也方便查语法
而且因为这本书大概是2014年出的,如今Django也已经出到2.多版
有些内容也变得不再支援或适用,而且语法或许也改变了
所以我会以最新版的Python和Django来修正这本书的内容跟程式码
目录:django系列文章-Django学习纪录
17. 视图类别
17.1 什么是视图类别(Class-based View)?
将原本的here视图函式
mysite/mysite/views.py
def here(request): return HttpResponse('妈,我在这!')
改为HereView视图类别
from django.views.generic.base import Viewclass HereView(View): def get(self, request): return HttpResponse('妈,我在这!')
首先django.views.generic.base.View
这是所有视图类别的最基础类别,任何视图类别都要继承它
接着撰写(其实是覆写)get
方法,此方法有两个参数,第一个放self,第二个放HttpRequest
物件
此方法的内容跟原来的视图函式一模一样,也可以用render_to_response
和render
urls.py设置
原本的
import mysite.viewsurlpatterns = [ ... path('here/', mysite.views.here), ...]
改为
import mysite.viewsurlpatterns = [ ... path('here/', mysite.views.HereView.as_view()), ...]
原本是要呼叫视图函式的名称,现在改为要呼叫HereView视图类别的as_view
方法
而as_view
是View类别以及所有继承此类别的衍生类别都会有的类别方法
我们可以想像它是视图类别的进入点
在URL配置档中,一律要呼叫对应的视图类别的as_view
方法
类别方法是属于类别的方法,不需要实体化的物件实例就可以呼叫和使用
但物件方法只能由实例来呼叫
而as_view
方法会利用View类别(以及所有继承此类别的衍生类别)中的物件方法dispatch
方法来选择适当的处理函式,例如当我们使用Http协定去GET某个URL页面时,dispatch
会帮我们选择使用get
方法来回应
整理一下View类别(以及所有继承此类别的衍生类别)中重要的方法及属性:
名称|型态|说明
------------- | -------------http_method_names
|属性|定义了这个视图类别可以接收、处理的Http协定,预设值为:['get', 'post', 'put', 'delete', 'head', 'options', 'trace']as_view()
|类别方法|可视为view的进入点,在urls.py中视图类别需呼叫这个方法,会将进来的请求转至dispatch
处理dispatch
|物件方法|这个方法会将核可的http请求(协定有列在http_method_names
中)转发至相对应的函式,如使用者发了一个GET,如果GET请求在http_method_names
中,那么便会将request转发至这个视图类别的get(self, request, **kwargs)
方法处理
所以我们能够透过设定http_method_names
来指定允许接收和处理的Http协定
17.2 视图类别的种类与使用
python的继承
class BaseClass: def test(self): print("BaseClass")class MyClass(BaseClass): # MyClass继承了BaseClass passmc = MyClass()mc.test()
结果:
BaseClass
python也支援多重继承
class BaseClass: def test(self): print("BaseClass")class Mixin1: def ability1(self): print("Get Ability1!") def test(self): print("Mixin1")class Mixin2: def ability2(self): print("Get Ability2!") def test(self): print("Mixin2")class MyClass(Mixin2, Mixin1, BaseClass): passmc = MyClass()mc.ability1()mc.ability2()mc.test()
结果:
Get Ability1!Get Ability2!Mixin2
在python中,多重继承由右边继承至左边
所以这就是为什么mc.test()
的结果是印出Mixin2
这个可能跟直觉相反,当你看到class MyClass(Mixin2, Mixin1, BaseClass)
的时候
可以想像它的演变顺序从左到右依序是由后代到祖先:
MyClass < Mixin2 < Mixin1 < BaseClass
像Mixin1与Mixin2这种用以提供衍生类别藉由多重继承来汇聚多个类别能力的基础类别,叫做Mixin,因为它会Mix in to它的衍生类别
透过django.views.generic
模组下的各种Mixin
能够让View类别变为拥有更多功能的新视图类别
像是内建的TemplateView
,ListView
,FormView
都是继承了不同Mixin实作的功能,而发展成不同的视图类别,这些视图类别皆放置在django.views.generic
下,如果想找内建的视图类别和Mixin只要到这里找即可
接下来我们要利用这些视图类别来改写之前的视图函式
17.2.1 Generic View
比较重要的Generic View有两个:View和TemplateView
View刚才我们看过了
TemplateView
TemplateView可以根据参数给定的模板以及Context参数来填写并回应该模板
TemplateViewt除了继承View这个最基础的类别之外
也继承了两个Mixin:ContextMixin和TemplateResponseMixin
其实几乎其他所有的内建视图类别都有继承这两个Mixin,因为这两个Mixin实作了跟模板使用有关的功能,而模板的使用几乎是必备的
ContextMixin
这个类别只有一个方法get_context_data
,它会回传一个字典,之后要呈现在网页上的变量都可以藉由呼叫或覆写这个函式来定义TemplateResponseMixin
这个类别会依据被覆写的template_name
属性来取得模板,并处理、回传一个HttpResponse
所以我们将使用ContextMixin来设置变量,用TemplateResponseMixin来指定模板而TemplateView原本的get方法已经将填写与回应的动作建置好了,如果没有特殊需求可以不用去覆写它将原本的index视图函式mysite/mysite/views.pydef index(request): return render(request, 'index.html')
改为
from django.views.generic.base import View, TemplateViewclass IndexView(TemplateView): template_name = 'index.html'
在template_name
填入要使用的模板名称
设定了这个变数后,TemplateResponseMixin会自动帮我们取得指定的模板
接着要设定模板中的request变量
利用TemplateView中的ContextMixin来设置,但我们就必须要覆写get方法了
class IndexView(TemplateView): template_name = 'index.html' def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) context['request'] = request return self.render_to_response(context)
让我们的get方法多出*args, **kwargs
两个参数,这是一个比较安全的写法,这样做可以保持这个函式的弹性
接着利用ContextMixin提供的get_context_data
方法来取得context
并且帮这个context多设置request这个变量
最后利用TemplateResponseMixin提供的render_to_response
来填写模板并回应
但这个方法必须要重新覆写get方法,有点麻烦,我们可以这样做:
settings.py中
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', # <-加入这行 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, },]
在处理器中加入request处理器的模组,如此一来request变量将会自动加入context中
这样我们就不需要覆写get方法了,因此可以把get方法整个拿掉也不会受影响
接下来设置urls.py
path('index/', mysite.views.IndexView.as_view())
由于这个页面只有载入模板,所以可以使用更简洁的方式
from django.views.generic.base import TemplateViewurlpatterns = [ ... path('index/', TemplateView.as_view(template_name='index.html')), ...]
除了request这种特殊的变量需要藉由覆写get方法才能拿到之外
如果要设置其他变量其实只要覆写get_context_data
就好了
假设我们要加入time这个变量,而且request已经利用处理器设置好了
from django.utils import timezoneclass IndexView(TemplateView): template_name = 'index.html' def get_context_data(self, **kwargs): # 取得字典型态的Context context = super(IndexView, self).get_context_data(**kwargs) # 加入我们额外想要的时间参数 context["time"] = timezone.now() return context
藉由super
呼叫IndexView基础类别TemplateView的get_context_data
方法来取得原始的context,接着为context设置time变量,最后回传即可
如此一来,IndexView会使用预设的get方法完成所有的动作
注意一下,如果用的是python3,则super(IndexView, self)
只需要打成super()
就可以了
17.2.2 Display View
视图类别DisplayView皆有一个必须赋值的属性model
这个属性用来定义所要展示的模型(资料库表单)
因此,DisplayView常用来与模型资料库一起工作
接下来要介绍其中两个视图类别:ListView和DetailView
使用ListView
ListView可以展示某个资料库模型里的资料,有点像内建查询功能一样
将原本的
mysite/restaurants/views.py
from django.contrib.auth.decorators import login_requiredfrom restaurants.models import Restaurant, Food@login_requireddef list_restaurants(request): restaurants = Restaurant.objects.all() return render(request, 'restaurants_list.html', locals())
改为
from django.views.generic.list import ListViewfrom restaurants.models import Restaurant, Foodclass RestaurantsView(ListView): model = Restaurant template_name = 'restaurants_list.html' context_object_name = 'restaurants'
首先要指定一个要展示的资料库模型给model
我们想展示Restaurant的资料,所以填入Restaurant
接着跟之前一样使用template_name
来指定使用的模板
如果只写出模板名称,那此类别所使用的TemplateResponseMixin会到每个应用下的template目录去寻找该模板,所以这边填入restaurants_list.html即可
如果不设定template_name
,那Django会去专案目录下的templates中寻找名为"应用名称/模型名称_list.html"
的模板,比如说这里我们不设置的话,就必须在mysite/templates中放置一个叫做restaurants/restaurant_list.html的模板context_object_name
用来为我们取出来的变量(一个模型资料的清单)取名
如果不设置此变数,则预设取出的资料清单会以object_list
为名
比如说模板中的{% for r in restaurants %}
就须改为{% for r in object_list %}
除此之外ListView还可以设置queryset
,此变数会决定查询的方式和结果
预设的queryset
为model.objects.all()
,我们可以使用任何合法的模型查询手段来改变它,比如说我们想要依名称排序
queryset = Restaurant.objects.order_by("-name")
还记得原本这个页面需要登入才能浏览
我们可以直接在urls.py中使用装饰器
path('restaurants_list/', login_required(restaurants.views.RestaurantsView.as_view()))
或是另一种方式,装饰dispatch
方法
由于dispatch
会将请求转发到各对应的函式
所以dispatch
被修饰过的视图类别,它所有支援的Http协定方法,都需要登入才能操作了
from django.contrib.auth.decorators import login_requiredfrom django.utils.decorators import method_decoratorfrom django.views.generic.list import ListViewfrom restaurants.models import Restaurant, Foodclass RestaurantsView(ListView): model = Restaurant template_name = 'restaurants_list.html' context_object_name = 'restaurants' @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(RestaurantsView, self).dispatch(request, *args, **kwargs)
而method_decorator
这个装饰器负责引入原本修饰函式的装饰器给类别中的方法使用
使用DetailView
DetailView则是用在单一笔资料的检视页面,如我们之前做的菜单,因为这个页面呈现了其中一间餐厅的详细资料,所以适合用DetailView来写
之前使用的方法一配置
path('menu/', menu)
from django.shortcuts import render_to_responsefrom restaurants.models import Restaurantfrom django.http import HttpResponseRedirectdef menu(request): if 'id' in request.GET and request.GET['id'] != '': restaurant = Restaurant.objects.get(id=request.GET['id']) return render_to_response('menu.html', locals()) else: return HttpResponseRedirect("/restaurants_list/")
又或者採用方法三
re_path(r'menu/(\d{1,5})', menu),
def menu(request, id): if id: restaurant = Restaurant.objects.get(id=id) return render_to_response('menu.html', locals()) else: return HttpResponseRedirect("/restaurants_list/")
不论使用哪一种配置都可以改写成这样
from django.views.generic.detail import DetailViewclass MenuView(DetailView): model = Restaurant template_name = 'menu.html' context_object_name = 'restaurant' @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(MenuView, self).dispatch(request, *args, **kwargs)
DetailView中的model
代表的是我们要检视的某一笔资料位于哪一个模型
其他设置和ListView都是一样的
事实上我们可以用ListView来完成这件事,只要调整我们的queryset
,不过DetailView提供了更特定的功能可以使用
使用DetailView就不必处理GET方法的判定,也不用处理id的取得,更不用亲自去模型中查询出要显示的那笔资料
不过我们却完全没有告诉视图我们要的是哪一笔资料,没关係
因为DetailView预设会向URL讨一个主键参数(也就是我们的餐厅id),所以它内部预设会使用取得的主键来查询出该笔资料,只是这个主键会以关键字参数的方式取得(而且预设名字为pk)
所以我们必须修改URL配置
re_path(r'menu/(?P<pk>\d+)', restaurants.views.MenuView.as_view())
如果不想用pk这个名字,只要覆写pk_url_kwarg
属性即可
class MenuView(DetailView): model = Restaurant template_name = 'menu.html' context_object_name = 'restaurant' pk_url_kwarg = 'id' @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(MenuView, self).dispatch(request, *args, **kwargs)
顺便一提,原本的函式中有重导的动作
在DetailView也可以藉由覆写get方法来完成
from django.http import HttpResponseRedirect, Http404class MenuView(DetailView): model = Restaurant template_name = 'menu.html' context_object_name = 'restaurant' @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(MenuView, self).dispatch(request, *args, **kwargs) def get(self, request, pk, *args, **kwargs): try: return super(MenuView, self).get(self, request, pk=pk, *args, **kwargs) except Http404: return HttpResponseRedirect('/restaurants_list/')
一旦餐厅的id不是合法的,将会取不到资料,此时会导致Http404
的错误,所以利用try/catch来接这个错误
17.3 EditView
这类的视图是要用来与使用者互动的,它能透过发送表单,新增、修改、删除资料库的内容,我们来介绍FormView
17.3.1 使用FormView
将原本的
def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") if request.POST: f = CommentForm(request.POST) if f.is_valid(): visitor = f.cleaned_data['visitor'] content = f.cleaned_data['content'] email = f.cleaned_data['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) visitor, content, email = ('', '', '') f = CommentForm(initial={'content': '我没意见'}) else: f = CommentForm(initial={'content': '我没意见'}) return render(request, 'comments.html', locals())
修改为
from restaurants.forms import CommentFormfrom django.views.generic.detail import SingleObjectMixinfrom django.views.generic.edit import FormViewclass CommentView(FormView, SingleObjectMixin): form_class = CommentForm template_name = 'comments.html' success_url = '/comment/' initial = {'content': '我没意见'} def form_valid(self, form): Comment.objects.create( visitor=form.cleaned_data['visitor'], email=form.cleaned_data['email'], content=form.cleaned_data['content'], date_time=timezone.localtime(timezone.now()), restaurant=self.get_object(), ) return self.render_to_response(self.get_context_data( form=self.form_class(initial=self.initial) ))
首先form_class
设定要使用的表单类别,template_name
跟之前一样
接着success_url
指定一个合法的URL路径,预设如果表单验证正确,就会在表单处理完后跳转至此页面
而initial
可以依表单栏位名称提供初始值
最后form_valid
方法会在使用者提交表单且表单栏位皆验证通过时被呼叫(如果使用者是第一次进入该页面,则会由其他方法处理),不过此方法有预设的处理手段,如果我们不覆写它的话,它就只会让页面跳转至success_url
,因为我们要依据表单来建立Comment资料,所以我们覆写此方法
而form_valid
吃两个参数,第一个是self,第二个是已经验证通过的表单模型物件form
我们从form中取出各栏位的cleaned_data
并呼叫Comment物件管理器的create
方法来新增一笔评论
如果下一步是要跳转至成功页面的话,可以简单呼叫CommentView的父类别(FormView)的form_valid
函式,毕竟它预设会处理跳转的动作,如下:
... return super(CommentView, self).form_valid(form)...
但在这里我们想要回到原本的页面,并重新生成一个未使用的表单,所以我们不回传父类别的方法,而自行回传一个有新的表单的页面,所以在函式结尾的地方使用self.render_to_response
来回应一个新页面,我们不需提供模板(因为已经设定过了),只需要提供表单模型的变量
context = self.get_context_data()context['form'] = self.form_class(initial=self.initial)return self.render_to_response(context)
为了简化可以这样做:
return self.render_to_response(self.get_context_data(form=self.form_class(initial=self.initial)))
所以这样就可以提供form变量给模板,记得要将comments.html中的f变量改为form
而且在comments.html中,我们还有r变量未提供
from restaurants.forms import CommentFormfrom django.views.generic.detail import SingleObjectMixinfrom django.views.generic.edit import FormViewclass CommentView(FormView, SingleObjectMixin): form_class = CommentForm template_name = 'comments.html' success_url = '/comment/' initial = {'content': '我没意见'} # 加入以下两行 model = Restaurant context_object_name = 'r' def form_valid(self, form): Comment.objects.create( visitor=form.cleaned_data['visitor'], email=form.cleaned_data['email'], content=form.cleaned_data['content'], date_time=timezone.localtime(timezone.now()), restaurant=self.get_object(), ) return self.render_to_response(self.get_context_data( form=self.form_class(initial=self.initial) )) def get_context_data(self, **kwargs): self.object = self.get_object() return super(CommentView, self).get_context_data(object=self.object, **kwargs)
增加两个变数model
和context_object_name
,它们都是SingleObjectMixin提供的变数,SingleObjectMixin可以帮助我们从某资料库模型中取出一笔资料,并以变量的方式提供给模板,透过设定model
就可指定资料库模型,透过context_object_name
就可指定变量名称
接着要覆写get_context_data
方法,利用SingleObjectMixin的get_object()
方法来取得资料物件(此方法会自动撷取URL参数),然后将此变量加入context
SingleObjectMixin跟DetailView的功能很像就是因为DetailView就是靠SingleObjectMixin组装起来的
然后要记得改成使用关键字参数
re_path(r'comment/(?P<pk>\d+)', restaurants.views.CommentView.as_view())
别忘了之前我们也把评论页面设为有权限设定,同样也是透过装饰dispatch
达成
from django.utils.decorators import method_decoratorfrom restaurants.forms import CommentFormfrom django.views.generic.detail import SingleObjectMixinfrom django.views.generic.edit import FormViewdef user_can_comment(user): return user.is_authenticated and user.has_perm('restaurants.can_comment')class CommentView(FormView, SingleObjectMixin): form_class = CommentForm template_name = 'comments.html' success_url = '/comment/' initial = {'content': '我没意见'} model = Restaurant context_object_name = 'r' def form_valid(self, form): Comment.objects.create( visitor=form.cleaned_data['visitor'], email=form.cleaned_data['email'], content=form.cleaned_data['content'], date_time=timezone.localtime(timezone.now()), restaurant=self.get_object(), ) return self.render_to_response(self.get_context_data( form=self.form_class(initial=self.initial) )) def get_context_data(self, **kwargs): self.object = self.get_object() return super(CommentView, self).get_context_data(object=self.object, **kwargs) @method_decorator(user_passes_test(user_can_comment, login_url='/accounts/login/')) def dispatch(self, request, *args, **kwargs): return super(CommentView, self).dispatch(request, *args, **kwargs)
17.4 视图类别的优势
举例,TemplateView无法轻易设置context,不过我们可以这样做:
class AdvTemplateView(TemplateView): def get(self, request, *args, **kwargs): return self.render_to_response(self.context)
透过继承以及覆写方法来产生一个更符合我们要求的新类别
之后如果遇到需要提供变量的情况,我们可以继承这个类别:
class MyView(AdvTemplateView): template_name = 'index.html' # 利用一个tricky的技巧取得context context = AdvTemplateView().get_context_data() context['hello'] = '123' # 提供了hello变量
还有像是我们常常需要判断是GET还是POST方法,不过只要透过dispatch
方法就能够达到简化代码的目的