Django22-视图、上下文、中间件
我们用Django开发,比如做一个博客,我们需要做一个文章列表,文章详情页,这种需求是比较普遍的,所以Django中提供了Class-Based Views。
有时候我们想直接渲染一个模板,不得不写一个视图函数
1
2
|
def render_template_view(request):
return render(request, '/path/to/template.html' )
|
其实可以用 TemplateView 可以直接写在 urls.py 中,不需要定义一个这样的函数。
这样的例子还有很多,下面一一介绍:
在urls.py中使用类视图的时候都是调用它的 .as_view() 函数
一,Base Views
1. django.views.generic.base.View
这个类是通用类的基类,其它类都是继承自这个类,一般不会用到这个类,个人感觉用函数更简单些。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# views.py
from django.http import HttpResponse
from django.views.generic import View
class MyView(View):
def get( self , request, * args, * * kwargs):
return HttpResponse( 'Hello, World!' )
# urls.py
from django.conf.urls import patterns, url
from myapp.views import MyView
urlpatterns = patterns('',
url(r '^mine/$' , MyView.as_view(), name = 'my-view' ),
)
|
2. django.views.generic.base.TemplateView
在 get_context_data() 函数中,可以传一些 额外内容 到 模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# views.py
from django.views.generic.base import TemplateView
from articles.models import Article
class HomePageView(TemplateView):
template_name = "home.html"
def get_context_data( self , * * kwargs):
context = super (HomePageView, self ).get_context_data( * * kwargs)
context[ 'latest_articles' ] = Article.objects. all ()[: 5 ]
return context
# urls.py
from django.conf.urls import patterns, url
from myapp.views import HomePageView
urlpatterns = patterns('',
url(r '^$' , HomePageView.as_view(), name = 'home' ),
)
|
3. django.views.generic.base.RedirectView
用来进行跳转, 默认是永久重定向(301),可以直接在urls.py中使用,非常方便:
1
2
3
4
5
6
7
|
from django.conf.urls import patterns, url
from django.views.generic.base import RedirectView
urlpatterns = patterns('',
url(r '^go-to-django/$' , RedirectView.as_view(url = 'http://djangoproject.com' ), name = 'go-to-django' ),
url(r '^go-to-ziqiangxuetang/$' , RedirectView.as_view(url = 'http://www.ziqiangxuetang.com' ,permant = False ), name = 'go-to-zqxt' ),
)
|
其它的使用方式:(new in Django1.6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
# views.py
from django.shortcuts import get_object_or_404
from django.views.generic.base import RedirectView
from articles.models import Article
class ArticleCounterRedirectView(RedirectView):
url = ' # 要跳转的网址,
# url 可以不给,用 pattern_name 和 get_redirect_url() 函数 来解析到要跳转的网址
permanent = False #是否为永久重定向, 默认为 True
query_string = True # 是否传递GET的参数到跳转网址,True时会传递,默认为 False
pattern_name = 'article-detail' # 用来跳转的 URL, 看下面的 get_redirect_url() 函数
# 如果url没有设定,此函数就会尝试用pattern_name和从网址中捕捉的参数来获取对应网址
# 即 reverse(pattern_name, args) 得到相应的网址,
# 在这个例子中是一个文章的点击数链接,点击后文章浏览次数加1,再跳转到真正的文章页面
def get_redirect_url( self , * args, * * kwargs):
If url is not set , get_redirect_url() tries to reverse the pattern_name using what was captured in the URL (both named and unnamed groups are used).
article = get_object_or_404(Article, pk = kwargs[ 'pk' ])
article.update_counter() # 更新文章点击数,在models.py中实现
return super (ArticleCounterRedirectView, self ).get_redirect_url( * args, * * kwargs)
# urls.py
from django.conf.urls import patterns, url
from django.views.generic.base import RedirectView
from article.views import ArticleCounterRedirectView, ArticleDetail
urlpatterns = patterns('',
url(r '^counter/(?P<pk>\d+)/$' , ArticleCounterRedirectView.as_view(), name = 'article-counter' ),
url(r '^details/(?P<pk>\d+)/$' , ArticleDetail.as_view(), name = 'article-detail' ),
)
|
二,Generic Display View (通用显示视图)
1. django.views.generic.detail.DetailView
DetailView 有以下方法:
-
get()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# views.py
from django.views.generic.detail import DetailView
from django.utils import timezone
from articles.models import Article
class ArticleDetailView(DetailView):
model = Article # 要显示详情内容的类
template_name = 'article_detail.html' # 模板名称,默认为 应用名/类名_detail.html(即 app/modelname_detail.html)
# 在 get_context_data() 函数中可以用于传递一些额外的内容到网页
def get_context_data( self , * * kwargs):
context = super (ArticleDetailView, self ).get_context_data( * * kwargs)
context[ 'now' ] = timezone.now()
return context
# urls.py
from django.conf.urls import url
from article.views import ArticleDetailView
urlpatterns = [
url(r '^(?P<slug>[-_\w]+)/$' , ArticleDetailView.as_view(), name = 'article-detail' ),
]
|
article_detail.html
1
2
3
4
5
6
7
|
< h1 >标题:{{ object.title }}</ h1 >
< p >内容:{{ object.content }}</ p >
< p >发表人: {{ object.reporter }}</ p >
< p >发表于: {{ object.pub_date|date }}</ p >
< p >日期: {{ now|date }}</ p >
|
2. django.views.generic.list.ListView
ListView 有以下方法:
-
get()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# views.py
from django.views.generic. list import ListView
from django.utils import timezone
from articles.models import Article
class ArticleListView(ListView):
model = Article
def get_context_data( self , * * kwargs):
context = super (ArticleListView, self ).get_context_data( * * kwargs)
context[ 'now' ] = timezone.now()
return context
# urls.py:
from django.conf.urls import url
from article.views import ArticleListView
urlpatterns = [
url(r '^$' , ArticleListView.as_view(), name = 'article-list' ),
]
|
article_list.html
1
2
3
4
5
6
7
8
|
< h1 >文章列表</ h1 >
< ul >
{% for article in object_list %}
< li >{{ article.pub_date|date }} - {{ article.headline }}</ li >
{% empty %}
< li >抱歉,目前还没有文章。</ li >
{% endfor %}
</ ul >
|
未完待续
Class-based views 官方文档:
https://docs.djangoproject.com/en/dev/ref/class-based-views/#built-in-class-based-views-api
有时候我们想让一些内容在多个模板中都要有,比如导航内容,我们又不想每个视图函数都写一次这些变量内容,怎么办呢?
这时候就可以用 Django 上下文渲染器来解决。
一,初识上下文渲染器
我们从视图函数说起,在 views.py 中返回字典在模板中使用:
1
2
3
4
|
from django.shortcuts import render
def home(request):
return render(request, 'home.html' , { 'info' : 'Welcome to ziqiangxuetang.com !' })
|
这样我们就可以在模板中使用 info 这个变量了。
1
|
{{ info }}
|
模板对应的地方就会显示:Welcome to ziqiangxuetang.com !
但是如果我们有一个变量,比如用户的IP,想显示在网站的每个网页上。再比如显示一些导航信息在每个网页上,该怎么做呢?
一种方法是用死代码,直接把栏目固定写在 模块中,这个对于不经常变动的来说也是一个办法,简单高效。
但是像用户IP这样的因人而异的,或者经常变动的,就不得不想一个更好的解决办法了。
由于上下文渲染器API在Django 1.8 时发生了变化,被移动到了 tempate 文件夹下,所以讲解的时候分两种,一种是 Django 1.8 及以后的,和Django 1.7及以前的。
我们来看Django官方自带的小例子:
Django 1.8 版本:
django.template.context_processors 中有这样一个函数
1
2
|
def request(request):
return { 'request' : request}
|
Django 1.7 及以前的代码在这里:django.core.context_processors.request 函数是一样的。
在settings.py 中:
Django 1.8 版本 settings.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
TEMPLATES = [
{
'BACKEND' : 'django.template.backends.django.DjangoTemplates' ,
'DIRS' : [],
'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' ,
],
},
},
]
|
Django 1.7 版本 settings.py 默认是这样的:
1
2
3
4
5
6
7
8
9
|
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth" ,
"django.core.context_processors.debug" ,
"django.core.context_processors.i18n" ,
"django.core.context_processors.media" ,
"django.core.context_processors.static" ,
"django.core.context_processors.tz" ,
"django.contrib.messages.context_processors.messages"
)
|
我们可以手动添加 request 的渲染器
1
2
3
4
5
|
TEMPLATE_CONTEXT_PROCESSORS = (
...
"django.core.context_processors.request" ,
...
)
|
这里的 context_processors 中放了一系列的 渲染器,上下文渲染器 其实就是函数返回字典,字典的 keys 可以用在模板中。
request 函数就是在返回一个字典,每一个模板中都可以使用这个字典中提供的 request 变量。
比如 在template 中 获取当前访问的用户的用户名:
1
|
User Name: {{ request.user.username }}
|
二,动手写个上下文渲染器
2.1 新建一个项目,基于 Django 1.8,低版本的请自行修改对应地方:
1
2
3
|
django-admin.py startproject zqxt
cd zqxt
python manage.py startapp blog
|
我们新建了 zqxt 项目和 blog 这个应用。
把 blog 这个app 加入到 settings.py 中
1
2
3
4
5
6
|
INSTALLED_APPS = (
'django.contrib.admin' ,
...
'blog' ,
)
|
整个项目当前目录结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
zqxt
├── blog
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── zqxt
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
|
2.2 我们在 zqxt/zqxt/ 这个目录下(与settings.py 在一起)新建一个 context_processor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2015-10-31 14:26:26
# @Author : Weizhong Tu ([email protected])
from django.conf import settings as original_settings
def settings(request):
return { 'settings' : original_settings}
def ip_address(request):
return { 'ip_address' : request.META[ 'REMOTE_ADDR' ]}
|
2.3 我们把新建的两个 上下文渲染器 加入到 settings.py 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
TEMPLATES = [
{
'BACKEND' : 'django.template.backends.django.DjangoTemplates' ,
'DIRS' : [],
'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' ,
'zqxt.context_processor.settings' ,
'zqxt.context_processor.ip_address' ,
],
},
},
]
|
最后面两个是我们新加入的,我们稍后在模板中测试它。
2.4 修改 blog/views.py
1
2
3
4
5
6
7
8
9
|
from django.shortcuts import render
def index(reuqest):
return render(reuqest, 'blog/index.html' )
def columns(request):
return render(request, 'blog/columns.html' )
|
2.5 新建两个模板文件,放在 zqxt/blog/templates/blog/ 中
index.html
1
2
3
4
5
|
< h1 >Blog Home Page</ h1 >
DEBUG: {{ settings.DEBUG }}
ip: {{ ip_address }}
|
columns.html
1
2
3
4
5
|
< h1 >Blog Columns</ h1 >
DEBUG: {{ settings.DEBUG }}
ip: {{ ip_address }}
|
2.6 修改 zqxt/urls.py
1
2
3
4
5
6
7
8
9
|
from django.conf.urls import include, url
from django.contrib import admin
from blog import views as blog_views
urlpatterns = [
url(r '^blog_home/$' , blog_views.index),
url(r '^blog_columns/$' , blog_views.columns),
url(r '^admin/' , include(admin.site.urls)),
]
|
2.7 打开开发服务器并访问,进行测试吧:
1
|
python manage.py runserver
|
就会看到所有的模板都可以使用 settings 和 ip_address 变量:
http://127.0.0.1:8000/blog_home/
http://127.0.0.1:8000/blog_columns/
效果图:
最后,附上源代码下载:zqxt_context_processor.zip
我们从浏览器发出一个请求 Request,得到一个响应后的内容 HttpResponse ,这个请求传递到 Django的过程如下:
也就是说,每一个请求都是先通过中间件中的 process_request 函数,这个函数返回 None 或者 HttpResponse 对象,如果返回前者,继续处理其它中间件,如果返回一个 HttpResponse,就处理中止,返回到网页上。
中间件不用继承自任何类(可以继承 object ),下面一个中间件大概的样子:
1
2
3
4
5
6
|
class CommonMiddleware( object ):
def process_request( self , request):
return None
def process_response( self , request, response):
return response
|
还有 process_view, process_exception 和 process_template_response 函数。
一,比如我们要做一个 拦截器,发现有恶意访问网站的人,就拦截他!
假如我们通过一种技术,比如统计一分钟访问页面数,太多就把他的 IP 加入到黑名单 BLOCKED_IPS(这部分没有提供代码,主要讲中间件部分)
1
2
3
4
5
6
|
#项目 zqxt 文件名 zqxt/middleware.py
class BlockedIpMiddleware( object ):
def process_request( self , request):
if request.META[ 'REMOTE_ADDR' ] in getattr (settings, "BLOCKED_IPS" , []):
return http.HttpResponseForbidden( '<h1>Forbidden</h1>' )
|
这里的代码的功能就是 获取当前访问者的 IP (request.META['REMOTE_ADDR']),如果这个 IP 在黑名单中就拦截,如果不在就返回 None (函数中没有返回值其实就是默认为 None),把这个中间件的 Python 路径写到settings.py中
1.1 Django 1.9 和以前的版本:
1
2
3
4
|
MIDDLEWARE_CLASSES = (
'zqxt.middleware.BlockedIpMiddleware' ,
...其它的中间件
)
|
1.2 Django 1.10 版本 更名为 MIDDLEWARE(单复同形),写法也有变化,详见 第四部分。
如果用 Django 1.10版本开发,部署时用 Django 1.9版本或更低版本,要特别小心此处。
1
2
3
4
|
MIDDLEWARE = (
'zqxt.middleware.BlockedIpMiddleware' ,
...其它的中间件
)
|
Django 会从 MIDDLEWARE_CLASSES 或 MIDDLEWARE 中按照从上到下的顺序一个个执行中间件中的 process_request 函数,而其中 process_response 函数则是最前面的最后执行。
二,再比如,我们在网站放到服务器上正式运行后,DEBUG改为了 False,这样更安全,但是有时候发生错误我们不能看到错误详情,调试不方便,有没有办法处理好这两个事情呢?
-
普通访问者看到的是友好的报错信息
-
管理员看到的是错误详情,以便于修复 BUG
当然可以有,利用中间件就可以做到!代码如下:
1
2
3
4
5
6
7
8
|
import sys
from django.views.debug import technical_500_response
from django.conf import settings
class UserBasedExceptionMiddleware( object ):
def process_exception( self , request, exception):
if request.user.is_superuser or request.META.get( 'REMOTE_ADDR' ) in settings.INTERNAL_IPS:
return technical_500_response(request, * sys.exc_info())
|
把这个中间件像上面一样,加到你的 settings.py 中的 MIDDLEWARE_CLASSES 中,可以放到最后,这样可以看到其它中间件的 process_request的错误。
当访问者为管理员时,就给出错误详情,比如访问本站的不存在的页面:http://www.ziqiangxuetang.com/admin/
普通人看到的是普通的 404(自己点开看看),而我可以看到:
三,分享一个简单的识别手机的中间件,更详细的可以参考这个:django-mobi 或 django-mobile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
MOBILE_USERAGENTS = ( "2.0 MMP" , "240x320" , "400X240" , "AvantGo" , "BlackBerry" ,
"Blazer" , "Cellphone" , "Danger" , "DoCoMo" , "Elaine/3.0" , "EudoraWeb" ,
"Googlebot-Mobile" , "hiptop" , "IEMobile" , "KYOCERA/WX310K" , "LG/U990" ,
"MIDP-2." , "MMEF20" , "MOT-V" , "NetFront" , "Newt" , "Nintendo Wii" , "Nitro" ,
"Nokia" , "Opera Mini" , "Palm" , "PlayStation Portable" , "portalmmm" , "Proxinet" ,
"ProxiNet" , "SHARP-TQ-GX10" , "SHG-i900" , "Small" , "SonyEricsson" , "Symbian OS" ,
"SymbianOS" , "TS21i-10" , "UP.Browser" , "UP.Link" , "webOS" , "Windows CE" ,
"WinWAP" , "YahooSeeker/M1A1-R2D2" , "iPhone" , "iPod" , "Android" ,
"BlackBerry9530" , "LG-TU915 Obigo" , "LGE VX" , "webOS" , "Nokia5800" )
class MobileTemplate( object ):
"""
If a mobile user agent is detected, inspect the default args for the view
func, and if a template name is found assume it is the template arg and
attempt to load a mobile template based on the original template name.
"""
def process_view( self , request, view_func, view_args, view_kwargs):
if any (ua for ua in MOBILE_USERAGENTS if ua in request.META[ "HTTP_USER_AGENT" ]):
template = view_kwargs.get( "template" )
if template is None :
for default in view_func.func_defaults:
if str (default).endswith( ".html" ):
template = default
if template is not None :
template = template.rsplit( ".html" , 1 )[ 0 ] + ".mobile.html"
try :
get_template(template)
except TemplateDoesNotExist:
pass
else :
view_kwargs[ "template" ] = template
return view_func(request, * view_args, * * view_kwargs)
return None
|
参考文档:https://docs.djangoproject.com/en/1.8/topics/http/middleware/
四,补充:Django 1.10 接口发生变化,变得更加简洁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class SimpleMiddleware( object ):
def __init__( self , get_response):
self .get_response = get_response
# One-time configuration and initialization.
def __call__( self , request):
# Code to be executed for each request before
# the view (and later middleware) are called.
# 调用 view 之前的代码
response = self .get_response(request)
# Code to be executed for each request/response after
# the view is called.
# 调用 view 之后的代码
return response
|
Django 1.10.x 也可以用函数来实现中间件,详见官方文档。
五,让 你写的中间件 兼容 Django新版本和旧版本
1
2
3
4
5
6
7
8
9
10
11
12
|
try :
from django.utils.deprecation import MiddlewareMixin # Django 1.10.x
except ImportError:
MiddlewareMixin = object # Django 1.4.x - Django 1.9.x
class SimpleMiddleware(MiddlewareMixin):
def process_request( self , request):
pass
def process_response(request, response):
pass
|
新版本中 django.utils.deprecation.MiddlewareMixin 的 源代码 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class MiddlewareMixin( object ):
def __init__( self , get_response = None ):
self .get_response = get_response
super (MiddlewareMixin, self ).__init__()
def __call__( self , request):
response = None
if hasattr ( self , 'process_request' ):
response = self .process_request(request)
if not response:
response = self .get_response(request)
if hasattr ( self , 'process_response' ):
response = self .process_response(request, response)
return response
|
__call__ 方法会先调用 self.process_request(request),接着执行 self.get_response(request) 然后调用 self.process_response(request, response)
旧版本(Django 1.4.x-Django 1.9.x) 的话,和原来一样。