Django-begin-02

上一篇我们成功完成了模型(models),那么接下来我们来学习一下界面(views)的相关内容:

view是什么

在Django中,view通常是一个特殊的函数并且有特殊的模板。这个函数能够帮助我们更好的实现界面的功能。


更多的views
我们往polls/views.py中添加如下的代码:

1
2
3
4
5
6
7
8
9
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)

添加了上述代码后,为了能够让url能够正确的解析,我们在polls/urls.py中增加更多的额外代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.conf.urls import url
from . import views
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

这里的正则表达式用了【捕获】的概念,就是**匹配括号中的表达式,同时将内容写在名称为question_id的组里面。(在re模块中,可以通过groupdict查询dict和对应的名字)。


写一个能够处理事件的views
每一个view都能处理以下两件事中的一个:

  • 回复一个HttpResponse
  • 返回一个Http404
    我们的view可以读取models中的数据,可以使用临时系统(比如Django,或者别的第三方的系统),产生一个PDF,输出一个XML,随手创建一个ZIP文件等等。

这里我们使用上一篇教程中创建的数据库:

1
2
3
4
5
6
7
8
9
10
11
from django.http import HttpResponse
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([q.question_text for q in latest_question_list])
return HttpResponse(output)
# Leave the rest of the views (detail, results, vote) unchanged

这里的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
2
3
4
5
6
7
8
9
10
11
12
13
from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))

这里将我们写好的index.html读取进来,并且设置了context的上下文,然后将这个渲染的结果作为HttpRespose的结果。

捷径:render函数:
非常常用的函数,会返回一个渲染过的httpresponse的对象。所以我们可以修改我们的views中的代码:

1
2
3
4
5
6
7
8
9
from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)

render将会将请求数据,渲染的template位置和相应的context作为参数传入,然后对数据进行渲染。

提出404错误
我们接下来完善一下details页面:

1
2
3
4
5
6
7
8
9
10
11
from django.http import Http404
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})

捷径: get_object_or_404()
这个函数能够让我们快速的提取models中的对象内容,若不存在的话就会立刻报错。

1
2
3
4
5
6
7
from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})

这个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
2
# added the word 'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),

命名空间

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from .models import Choice, Question
# ...
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

  • 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
2
3
4
5
6
7
8
9
10
11
from django.conf.urls import url
from . import views
app_name = 'polls'
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

然后是相关的view.py中的代码:

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
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
... # same as above, no changes needed.

这里使用了ListView和DetailView去展示我们的内容,ListView展示的是一个对象列表,而DetailView展示的是一个销毁展示特定对象的详细页面

  • 每一个地向都需要至少知道一个model属性的对象来展示
  • DetailView产生的界面需要一个从URL中获取的主键,叫做pk所以我们需要将question_id作为主键交过去。
    默认情况下,DetailView兼顾和i使用一个叫做_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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import datetime
    from django.utils import timezone
    from django.test import TestCase
    from .models import Question
    class QuestionMethodTests(TestCase):
    def test_was_published_recently_with_future_question(self):
    """
    was_published_recently() should return False for questions whose
    pub_date is in the future.
    """
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)

然后我们使用

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
2
3
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
`

setup_test_environment()提供了一个临时的渲染器,这个允许我们能够在response中增加形如response.context这类内容的属性。注意到,这个方法并不会新建立一个测试数据库。

接下来我们需要导入测试用户类。

1
2
3
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()

然后我们依次测试部分数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> # get a response from '/'
>>> response = client.get('/')
>>> # we should expect a 404 from that address
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What&#39;s up?</a></li>\n \n </ul>\n\n'
>>> # If the following doesn't work, you probably omitted the call to
>>> # setup_test_environment() described above
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>

由于我们之前运行了添加未来的question,所以我们在展示的时候是不能够将所有的数据展示,只能够展示已经出现过的数据。于是这里我们修改views.py中的index:

1
2
3
4
5
6
7
8
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]

过滤器中的参数__lte表示【比传入参数litte or equal】的数据,所以此时只会返回所有的<=当前时间的数据。

测试一下新的view

由于我们刚刚只是测试了现有的数据,但是问题是,如果我们每次新建数据的时候会触发漏洞怎么办?所以这里我们就尝试按照上面shell中操作的那样,模拟一个创建的过程。
我们首先写一个create_question来模拟新建这个question的过程,注意这个过程我们不会将数据引入到我们的数据库中(也就不去save,而是直接创建一个对象,这样的话在结束的时候,数据就会丢失)。

1
2
3
4
5
6
7
8
def create_question(question_text, days):
"""
Creates a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)

然后接下来就是各类测试:不创建问题的,创建未来的问题的,过去的问题的,同时创建两种问题,还有连续创建两个过去的问题。