上一篇我们成功完成了模型(models),那么接下来我们来学习一下界面(views)的相关内容:
view是什么
在Django中,view通常是一个特殊的函数并且有特殊的模板。这个函数能够帮助我们更好的实现界面的功能。
更多的views
我们往polls/views.py中添加如下的代码:
1 | def detail(request, question_id): |
添加了上述代码后,为了能够让url能够正确的解析,我们在polls/urls.py中增加更多的额外代码
1 | from django.conf.urls import url |
这里的正则表达式用了【捕获】的概念,就是**匹配括号中的表达式,同时将内容写在名称为question_id的组里面。(在re模块中,可以通过groupdict查询dict和对应的名字)。
写一个能够处理事件的views
每一个view都能处理以下两件事中的一个:
- 回复一个HttpResponse
- 返回一个Http404
我们的view可以读取models中的数据,可以使用临时系统(比如Django,或者别的第三方的系统),产生一个PDF,输出一个XML,随手创建一个ZIP文件等等。
这里我们使用上一篇教程中创建的数据库:
1 | from django.http import HttpResponse |
这里的order_by是指"按照pub_date的顺序进行排序(虽然不知道是什么顺序就是了)"
为了让我们的页面更加美观,我们这里使用Django的模板来进行一些设计:
首先,在polls文件夹内创建一个叫做templates的文件夹,Django会在这个文件夹中搜索模板。
TEMPLATES描述了django将会如何独具和渲染模板。默认的DjangoTemplates后台文件中将APP_DIRS设置为真。依照惯例,DjangoTepmates将会寻找每一个INSTALLED_APPS中出现文件的tamplates。
在我们创建的templates目录下创建一个叫做polls的文件夹,里面创建一个index.html.我们要让我们的templats的目录变为polls/tempaltes/polls/index.html,因为这个app_directories是如上面提到的那样来读取的,我们可以简单的将目录命名为polls/index.html
然后我们修改polls.views.py中的代码,让template能够被读取并且渲染:
1 | from django.http import HttpResponse |
这里将我们写好的index.html读取进来,并且设置了context的上下文,然后将这个渲染的结果作为HttpRespose的结果。
捷径:render函数:
非常常用的函数,会返回一个渲染过的httpresponse的对象。所以我们可以修改我们的views中的代码:
1 | from django.shortcuts import render |
render将会将请求数据,渲染的template位置和相应的context作为参数传入,然后对数据进行渲染。
提出404错误
我们接下来完善一下details页面:
1 | from django.http import Http404 |
捷径: get_object_or_404()
这个函数能够让我们快速的提取models中的对象内容,若不存在的话就会立刻报错。
1 | from django.shortcuts import get_object_or_404, render |
这个get_object_or_404函数接受需要提取内容的对象以及对应所需要的参数。
为了解耦处理,这些异常一定要及时处理。
使用template系统:
此时我们需要完善我们的detail页面,所以这里我们将要将每一个Question的数据进行展示:
这里给出要求:
主标题
列出每一个投票的内容。
这里留意,每一个投票的内容都是可以使用question.关联数据项_all来获取一整个可迭代对象。
将写死的url移除
注意,当我们在index.html页面中写一个链接的时候,我们最好不要写死一个url,不然的话耦合程度太高就不能替换成其他的templates。由于我们在polls.urls里面已经定义过,我们就能可以更改当前的url
<a href="\{\% url 'detail' question.id \%\}">
通过这个写法,回去polls.urls模块中的URL定义中查找对应模块的定义:
1 | url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), |
这里name='detail’将url模块命名,同时在配置的时候会叫question_id提取出来,然后加载url的后面。如果我们想要改变模板中的地址,我们还可以这么改:
1 | # added the word 'specifics' |
命名空间
Django可以兼容多个apps。比如,poslls app有一个details 模块,可能在同一个项目中的另一个app也有。那么Django是如何分辨究竟是那个app将要使用url template 标签呢?
为了能够使用,我们需要在urls.py的开头加上一个URLConf的标签,加上的标签能够设置这个程序的命名空间:
app_name = ‘polls’
写一个简单的表单
首先修改一下details.html的内容,增加一个表单。
- 每一个radio 按钮都和choice’ID 关联。每一个radio 按钮的名字都是choice。也就是说,post请求中会包含一个choice=ID的请求。
- 这个action的值我们设置为url ‘polls:vote’ question.id,方法设置为post,就是说会将数据传输给服务器端。当需要将一个表单的数据传输给服务器端的时候,就需要使用post方法。
- forloop.counter是一个变量,表示当前的循环的下标是啥。
- 小心csrf(跨域请求攻击),不过Django自己实现了一个防御机制。只需要加上csrf_token即可。
我们在之前已经实现了urls的配置:
1 | url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote') |
接下来让我们修改一下vote函数,让其能够实现后台操作:
1 | from django.shortcuts import get_object_or_404, render |
- requst.POST 是一个类似字典的对象,可以让我们获取我们之前请求的数据,request.POST[‘choice’]将会返回我们选中的id的字符串类型。
注意到这个方法还会提供 request.GET 方法,从而获得相应数据。但是为了防止恶意请求(?),我们还是只使用post方法接受以保证我们只会接受到post请求 - 如果 choice 没有在post请求中提供,request.POST[‘choice’]将会抛出KeyError的错误,所以我们要提前考虑这个情况,遇到这种情况应该即使的反馈重新绘制的画面。
- 注意到我们在最后使用的是HttpResponseRedirecth对象,而不是直接HttpResponse,为的是防止用户直接使用后退键将页面返回造成的反复提交的问题。注意所有的post返回都应该真么处理。
- 我们在最后使用了reverse函数(不是python自带的那个),这个函数将会帮助我们避免写死url,这里给出了我们想要传输的view的名称和指向当前view的URL 模式。
这个reverse的意思是指【将命名域和子域名颠倒】,然后将polls放在域名匹配的前面,然后子域名放在后面(好像没什么问题。。)在这个情况下,我们使用之前提出的
1 | url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='result') |
经过解析之后就会变成/polls/3/results,注意到这个3的位置正好是我们解析得到的位置。
最后这个重定向后的url就会让results views展示最后的页面
reverse 的迷惑
reverse其实是一个【动态加载url】的的函数,还记得我们在url中对url进行了命名:
url("^index/$", name = “index”)
这个index就是这个url的名字(name),然后reverse 的含义其实是
reverse(“作用域:url名字”)
reverse会尝试从我们的apps中查找所有有可能的url,以上述为例子,假如我们的apps域为:
app_name = “test”
那么为了匹配上述的url,我们的reverse可以这么写:
reverse(“test:index”)
此时就会将匹配完成的url,也就是url所对应的localhost/index/所获取。
所以接下来让我们来完善一下这个results.html页面;
提示:
页面上得有每一个choice的【内容】和【票数】
使用更少的代码
由于我们上述提到的功能都很简单,我们此时应该使用一些"generic views"系统来快速的配置这些页面。
首先是urls.py的配置
1 | from django.conf.urls import url |
然后是相关的view.py中的代码:
1 | from django.shortcuts import get_object_or_404, render |
这里使用了ListView和DetailView去展示我们的内容,ListView展示的是一个对象列表,而DetailView展示的是一个销毁展示特定对象的详细页面
- 每一个地向都需要至少知道一个model属性的对象来展示
- DetailView产生的界面需要一个从URL中获取的主键,叫做pk所以我们需要将question_id作为主键交过去。
默认情况下,DetailView兼顾和i使用一个叫做<app name?/_detail.html的url,如这里我们不更换名字,那么名字将会变成polls/question_detail.html",这个template_name属性告诉了Django使用一个特殊的模板名字而不是自动生成一个对应的名字。
我们之前在手动写render的时候,会给页面中的变量赋值。比如question等等。由于DetailView的model传入参数的时候,我们传入的Question会正确的设置变量。但是List View中却会给传入的Question对象命名为question_list。所以这里正确的做法就是覆盖掉对象的上下文名字【context_object_name】,从而能修改默认的名字。
自动测试
在模型设计上,我们有一个Question.was_published_reently()的方法,功能是判断当前的Question是否已经显示。然而,当时的判断方法似乎会导致我们一些将来提出的Quetion也被视为published,这里我们进行测试:
- 使用Admin创建一个问题,时间确定在将来的一个时间
- 在shell中调用对应函数进行测试。
发现的确是存在这个问题,于是我们可以写一个自动化测试,Django已经提供了一个tests.py给我们,这个测试系统将会自动的寻找每一个文件中以test开头的测试。
在tests.py文件中添加如下代码:
1 | import datetime |
然后我们使用
1 | python manage.py test polls |
就可以对polls进行测试。
当我们执行上述代码的时候,一下的事情发生了:
- 寻找polls app 中的相关测试
- 找打了一个django.test.TestCase类
- 为测试创建了一个特定的测试数据库
- 在test开头的函数 test_was_published_recently_with_future_question中穿件了一个Question对象,其中pub_date的数据为30天后
- 然后使用了assertIs()方法,发现了返回值的数据不一致。
修复bug
由于这个函数的意思是【一天内推送的question】,所以这里我们增加判断语句即可。
可以通过别的测试项目来增加测试的内容。
测试界面
注意到,我们除了要求我们的代码内部的行为,还需要在意其在view层面的变化。
测试用户
Django提供了一个Client来模拟一个用户来与界面交互。
首先我们现在命令行中体验一下这个感觉:
1 | from django.test.utils import setup_test_environment |
然后我们依次测试部分数据:
1 | # get a response from '/' |
由于我们之前运行了添加未来的question,所以我们在展示的时候是不能够将所有的数据展示,只能够展示已经出现过的数据。于是这里我们修改views.py中的index:
1 | def get_queryset(self): |
过滤器中的参数__lte表示【比传入参数litte or equal】的数据,所以此时只会返回所有的<=当前时间的数据。
测试一下新的view
由于我们刚刚只是测试了现有的数据,但是问题是,如果我们每次新建数据的时候会触发漏洞怎么办?所以这里我们就尝试按照上面shell中操作的那样,模拟一个创建的过程。
我们首先写一个create_question来模拟新建这个question的过程,注意这个过程我们不会将数据引入到我们的数据库中(也就不去save,而是直接创建一个对象,这样的话在结束的时候,数据就会丢失)。
1 | def create_question(question_text, days): |
然后接下来就是各类测试:不创建问题的,创建未来的问题的,过去的问题的,同时创建两种问题,还有连续创建两个过去的问题。