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 有以下方法:

  1. dispatch()

  2. http_method_not_allowed()

  3. get_template_names()

  4. get_slug_field()

  5. get_queryset()

  6. get_object()

  7. get_context_object_name()

  8. get_context_data()

  9. get()

  10. render_to_response()

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 有以下方法:

  1. dispatch()

  2. http_method_not_allowed()

  3. get_template_names()

  4. get_queryset()

  5. get_context_object_name()

  6. get_context_data()

  7. get()

  8. render_to_response()

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/

效果图:

Django22-视图、上下文、中间件

最后,附上源代码下载:Django22-视图、上下文、中间件zqxt_context_processor.zip


我们从浏览器发出一个请求 Request,得到一个响应后的内容 HttpResponse ,这个请求传递到 Django的过程如下:

Django22-视图、上下文、中间件

也就是说,每一个请求都是先通过中间件中的 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,这样更安全,但是有时候发生错误我们不能看到错误详情,调试不方便,有没有办法处理好这两个事情呢?

  1. 普通访问者看到的是友好的报错信息

  2. 管理员看到的是错误详情,以便于修复 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(自己点开看看),而我可以看到:

Django22-视图、上下文、中间件


三,分享一个简单的识别手机的中间件,更详细的可以参考这个: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) 的话,和原来一样。