Django ------ 高级 view 和 URLconf 配置 额外URLconf参数技术应用到自己的工程
伪造捕捉到的URLconf的值:
要匹配某个模式的一堆视图,以及一个并不匹配这个模式但视图逻辑是一样的URL。这种情况下,可以通过向同一个视图传递额外URLconf参数来伪造URL值的捕捉。
例如,一个显示特定日子的应用。类似:
/mydata/jan/01/ /mydata/jan/02/ /mydata/jan/03/ # ... /mydata/dec/30/ /mydata/dec/31/
使用命名组方法在URLconf中捕捉:
urlpatterns = patterns('', (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view), )
视图函数原型类似:
def my_view(request, month, day): .........................
想添加另外一个使用my_view视图但不包含month和/或者day的URL时候,问题出现。
比如想添加一个,/mydata/birthday/ 这个url等价于/mydata/jan/06 这个时候可以这样利用额外URLconf参数:
urlpatterns = patterns('', (r'^mydata/birthday/$', view.my_view, {'month': 'jan', 'day': '06'}), <=== (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view), )
这个最牛逼的地方在于,根部不用改变自己的视图函数,因为视图函数只在乎它得到的参数,不会管到底这参数是捕捉回来的还是额外提供的。即month day。
创建一个通用视图:抽出我们代码中共性的东西是一个好习惯,通过使用额外的URLconf参数,可以把同样的思想用到django的视图中。
一个例子: #urls.py from django.conf.urls.default import * from mysite import views urlpatterns = patterns('', (r'^envents/$', views.event_list), (r'^blog/entries/$', views.entry_list), ) #vies.py from django.shortcuts import render_to_response from mysite.models import Event, BlogEntry def event_list(request): obj_list = Event.objects.all() return render_to_response('mysite/event_list.html', {'event_list':obj_list}) def entry_list(request): obj_list = BlogEntry.objects.all() return render_to_response('mysite/entry_list.html', {'entry_list':obj_list})
这连个视图做的事情实际上是一至的,显示一系列对象,让我们吧它们显示的对象的类型抽象出来:
from django.conf.urls.default import * from mysite import models, views urlpatterns = patterns('', (r'^events/$', views.object_list, {'model': models.Event}), <======== (r'^blog/entries/$', views.object_list, {'model': models.Entry}), ) vies.py from django.shortcuts import render_to_response def object_list(request, model): <=========================================== obj_list = model.object.all() template_name = 'mysite/%_list.html' % model.__name__.lower() return render_to_response(template_name, {'object_list': obj_list})
可重用的,与模型无关的视图,简单的重用这一个object_list视图。我们做了以下事情:
- 我们通过model参数直接传递了模型类。额外URLconf参数的字典是可以传递任何类型的对象,而不仅仅是字符串。
- 这一行:model.objects.all 是界定
- 我们使用:model.__name__.lower决定模板的名字。每个python的类都有一个__name__属性返回类名。
django有自己的内置通用视图。
提供视图配置选项:为自己认为用户可能希望改变的配置添加一个钩子到你的视图中会是一个好主意:一个应用中比较常见的可供配置代码是模板名字:
def my_view(request, template_name): var = do_something() return render_to_response(template_name, {'var': var})
了解捕捉值和额外参数之间的优先级别 额外的选项:
当冲突出现的时候,额外URLconf参数优先于参数值,也就是说,如果URLconf捕捉到一个命名组变量和一个额外URLconf参数包含的变量同名时候,额外参数的值会被调用。
例如,下边这个URLconf:
from django.conf.urls.default import * from mysite import views urlpatterns = patterns('', (r'^mydata/((?P<id>\d+)/$, views.my_view, {'id':3}), ) 在这里边,硬编码的(额外字典)id将优先使用。就是说任何请求都会做id设置3对待,不管URL里边的值。
使用缺省视图参数:注意字符串传递的方式
# urls.py from django.conf.urls.defaults import * from mysite import views urlpatterns = patterns('', (r'^blog/$', views.page), (r'^blog/page(?P<num>\d+)/$', views.page), ) # views.py def page(request, num='1'): <=========================================== # Output the appropriate page of blog entries, according to num. # ... 在这里,两个URL表达式都指向了同一个视图 views.page ,但是第一个表达式没有传递任何参数。 如果匹配到了第一个样式, page() 函数将会对参数 num 使用默认值 "1" ,如果第二个表达式匹配成功, page() 函数将使用正则表达式传递过来的num的值。
我们设置的是默认参数值是字符串'1',不是整数1,捕捉给num的总是字符串。
特殊情况下的视图:
例如说,用以下方式描述的来向django的管理站点添加一个目标页面
urlpatterns = patterns('', ............................. (r'^([^/]+)/([^/]+)/add/$', views.add_stage), ............................ ) 代码能够匹配/myblo/entries/add/ 和 /auth/groups/add/ 这样的URL
然而,对于用户添加页面/auth/user/add 是特殊情况,我们可以在视图中特别指出以解决这种情况:
def add_stage(request, app_label, model_name): if app_label =='auth' and model_name == 'user' do ...... else: do ,,,,,,
但是这样做不优雅,在view里面放了URL的逻辑,更优雅的方法是要利用URLconf从顶向下的解析顺序这个特点:
urlpatterns = patterns('', # ... ('^auth/user/add/$', views.user_add_stage), ('^([^/]+)/([^/]+)/add/$', views.add_stage), # ... )
优先匹配上面的模式,而不是第二个模式,虽然它匹配,短路逻辑。
从URL中捕获文本:正则表达式匹配的虽然是整数的字符串,但是参数是作为字符串传至view的。而不是整型。
(r'^articles/(?P<year>\d{4})/$', views.year_archive),
当你在写视图代码时记住这点很重要,许多Python内建的方法对于接受的对象的类型很讲究。 许多内置Python函数是挑剔的(这是理所当然的)只接受特定类型的对象。 一个典型的的错误就是用字符串值而不是整数值来创建 datetime.date 对象:
创建 datetime.date 对象: >>> import datetime >>> datetime.date('1993', '7', '9') Traceback (most recent call last): ... TypeError: an integer is required >>> datetime.date(1993, 7, 9) datetime.date(1993, 7, 9) 回到URLconf和视图处,错误看起来很可能是这样: # urls.py from django.conf.urls.defaults import * from mysite import views urlpatterns = patterns('', (r'^articles/(\d{4})/(\d{2})/(\d{2})/$', views.day_archive), ) # views.py import datetime def day_archive(request, year, month, day): # The following statement raises a TypeError! date = datetime.date(year, month, day)
因此, day_archive() 应该这样写才是正确的: def day_archive(request, year, month, day): date = datetime.date(int(year), int(month), int(day))
决定URLconf搜索的东西:
当一个请求过来的时候,Django试着将请求的URL作为一个普通Python字符串进行URLconf模式匹配,而不是Unicode字符串。当然这并不包括GET或POST参数或域名。也不包括第一个斜杠,因为 每个URL必定有一个斜杠。
在解析URLconf时,请求方法(例如, POST , GET , HEAD )并 不会 被考虑。 换而言之,对于相同的URL的所有请求方法将被导向到相同的函数中。 因此根据请求方法来处理分支是视图函数的责任。
urls.py from django.conf.urls.defaults import * from mysite import views urlpatterns = patterns('', ............................ (r'^somepager/$', view.some_page), ............................ ) views.py from django.http import Http404, HttpResponseRedirect from django.shortcuts import render_to_response def some_page(request): if request.method == ‘POST’: do_somethingforpost(0 return HttpResponseRedirect('/somurl') <================ elif request.method == 'GET': do_something_forget() return render_to_response('/page.html') <================ else: raise Http404()
在这个示例中,`` some_page()`` 视图函数对`` POST`` 和`` GET`` 这两种请求方法的处理完全不同。 它们唯一的共同点是共享一个URL地址: `` /somepage/.``正如大家所看到的,在同一个视图函数中对`` POST`` 和`` GET`` 进行处理是一种很初级也很粗糙的做法。 一个比较好的设计习惯应该是,用两个分开的视图函数——一个处理`` POST`` 请求,另一个处理`` GET`` 请求,然后在相应的地方分别进行调用。
# views.py from django.http import Http404, HttpResponseRedirect from django.shortcuts import render_to_response def method_splitter(request, GET=None, POST=None): <====== if request.method == GET and GET is not None: return GET(request) elif request.metod == POST and POST is not None: return POST(request) else; return Http404 def some_page_get(request): assert request.method == 'GET' do_something_for_get() return render_to_response('page.html') def some_page_post(request): assert request.method == 'POST' do_something_for_post() return HttpResponseRedirect('/someurl/') # urls.py from django.conf.urls.defaults import * from mysite import views urlpatterns = patterns('', # ... (r'^somepage/$', views.method_splitter, {'GET': views.some_page_get, 'POST': views.some_page_post}), # ... )
上面代码的分析很重要 要注意:在URLconf中,我们把/somepage/指到了method_splitter()函数,并把视图函数额外用到的POST和GET参数传递给它。最终把somepage视图分解了两个,这比把所有的逻辑放到一个视图简单优雅。
我们的代码中可以看到,假设GET和POST视图函数只有一个参数request,我们要使用method_splitter与那种会从URL里捕捉字符,或者会接收一些可选参数的视图一起工作的时候怎么办。为了实现这个,我们使用Python中一个优雅的特性,带*号的可变参数:
def method_splitter(request, *args, **kwargs): <================ get_view = kwargs.pop('GET', None) post_view = kwargs.pop('POST', None) if request.method == 'POST' and post_view is not None: return post_view(request, *args, **kwargs) elif request.method == 'GET' and get_view is not None: return get_view(request, *args, **kwargs) raise Http404
在这里边,我们重新构建method_splitter(),去掉了GET和POST两个关键字,改而支持使用*args和**kwargs,这是一个python 的特性,允许函数接收动态的可变数量的参数名只在运行时可知的参数。如果你在函数定义的时候,只在前面加入一个*号,所有传递给函数将会保存为一个元组,如果连个*号的话所有传递给函数的关键字参数,将会保存为一个字典。
回过头来看,你能发现我们用method_splitter()和*args接受**kwargs函数参数并把它们传递到正确的视图。但是在我们这样做之前,我们要调用两次获得参数kwargs.pop()GETPOST,如果它们合法的话。 (我们通过指定pop的缺省值为None,来避免由于一个或者多个关键字缺失带来的KeyError)
包装视图函数:例子如下,发现自己在各个不同视图重复了大量代码
def my_view1(request): if not request.user.is_authenticated(): return HttpResponseRedirect('/accounts/login/') # ... return render_to_response('template1.html') def my_view2(request): if not request.user.is_authenticated(): return HttpResponseRedirect('/accounts/login/') # ... return render_to_response('template2.html') def my_view3(request): if not request.user.is_authenticated(): return HttpResponseRedirect('/accounts/login/') # ... return render_to_response('template3.html')
这里每一个视图都检查request.user是否已经认证了的。不是的话重定向,是的话当前用户登录站点。如果我们能够从每个视图里边移除那些重复代码,并且只在需要认证的时候指明他们,那就完美了,通过一个视图包装函数来达到目的。
def requires_login(view): def new_view(request, *args, **kwargs): if not request.user.is_authenticated(): return HttpResponseRedirect('/accounts/login/') return view(request, *args, **kwargs) return new_view
函数requires_login 传日一个视图函数view,然后返回一个新的视图函数new_view,这个新的视图函数new_view在函数requires_login内定义,处理requires.use.is_authenticatde()这个验证,从而决定是否执行原来的view函数。
现在我们可以从视图函数中去if not的部分,验证,我们可以在URLconf中很容易的用requires_login来包装实现。
from django.conf.urls.default import * from mysite.views import * urlpatterns = patterns('', (r'^view1/$', requires_login(my_view1)), (r'^view2/$', requires_login(my_view2)), )
优化的代码和前面的功能一样,但是减少了代码冗余,我们建立了一个漂亮通用的函数requires_login帮助我们修饰所有需要用它来验证的视图。
包含其他URLconf :
如果你试图让你的代码用在多个基于Django的站点上,你应该考虑将你的URLconf以包含的方式来处理。 在任何时候,你的URLconf都可以包含其他URLconf模块。 对于根目录是基于一系列URL的站点来说,这是必要的。 例如下面的,URLconf包含了其他URLConf: from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^weblog/', include('mysite.blog.urls')), (r'^photos/', include('mysite.photos.urls')), (r'^about/$', 'mysite.views.about'), ) 在前面第6章介绍Django的admin模块时我们曾经见过include. admin模块有他自己的URLconf,你仅仅只需要在你自己的代码中加入include就可以了. 这里有个很重要的地方: 例子中的指向 include() 的正则表达式并 不 包含一个 $ (字符串结尾匹配符),但是包含了一个斜杆。 每当Django遇到 include() 时,它将截断匹配的URL,并把剩余的字符串发往包含的URLconf作进一步处理。 继续看这个例子,这里就是被包含的URLconf mysite.blog.urls : from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^(\d\d\d\d)/$', 'mysite.blog.views.year_detail'), (r'^(\d\d\d\d)/(\d\d)/$', 'mysite.blog.views.month_detail'), ) 通过这两个URLconf,下面是一些处理请求的例子: /weblog/2007/ :在第一个URLconf中,模式 r'^weblog/' 被匹配。 因为它是一个 include() ,Django将截掉所有匹配的文本,在这里是 'weblog/' 。URL剩余的部分是 2007/ , 将在 mysite.blog.urls 这个URLconf的第一行中被匹配到。 URL仍存在的部分为 2007/ ,与第一行的 mysite.blog.urlsURL设置相匹配。 /weblog//2007/(包含两个斜杠) 在第一个URLconf中,r’^weblog/’匹配 因为它有一个include(),django去掉了匹配的部,在这个例子中匹配的部分是’weblog/’ 剩下的部分是/2007/ (最前面有一个斜杠),不匹配mysite.blog.urls中的任何一行.1 /about/ : 这个匹配第一个URLconf中的 mysite.views.about 视图。
捕获的参数如何和include()协同工作:
一个被包含的URLconf接受任何来自 parent URLconfs 的被捕获的参数。比如:
# root urls.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^(?P<username>\w+)/blog/', include('foo.urls.blog')), ) # foo/urls/blog.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^$', 'foo.views.blog_index'), (r'^archive/$', 'foo.views.blog_archive'), )
在这个例子中,被捕获的 username 变量将传递给被包含的 URLconf,进而传递给那个URLconf中的 每一个 视图函数。
注意,这个被捕获的参数 总是 传递到被包含的URLconf中的 每一 行,不管那些行对应的视图是否需要这些参数。 因此,这个技术只有在你确实需要那个被传递的参数的时候才显得有用。
额外的URLconf如何和include()协同工作
相似的,你可以传递额外的URLconf选项到 include() , 就像你可以通过字典传递额外的URLconf选项到普通的视图。 当你这样做的时候,被包含URLconf的 每一 行都会收到那些额外的参数。
比如,下面的两个URLconf在功能上是相等的。
第一个:
# urls.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^blog/', include('inner'), {'blogid': 3}), ) # inner.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^archive/$', 'mysite.views.archive'), (r'^about/$', 'mysite.views.about'), (r'^rss/$', 'mysite.views.rss'), )
第二个:
# urls.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^blog/', include('inner')), ) # inner.py from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^archive/$', 'mysite.views.archive', {'blogid': 3}), (r'^about/$', 'mysite.views.about', {'blogid': 3}), (r'^rss/$', 'mysite.views.rss', {'blogid': 3}), )
这个例子和前面关于被捕获的参数一样(在上一节就解释过这一点),额外的选项将 总是 被传递到被包含的URLconf中的 每一 行,不管那一行对应的视图是否确实作为有效参数接收这些选项,因此,这个技术只有在你确实需要那个被传递的额外参数的时候才显得有用。 因为这个原因,这种技术仅当你确信在涉及到的接受到额外你给出的选项的每个URLconf时有用的才奏效。