一、简单投票系统
下面我们利用前面所学的知识,如下四个章节:
来完成一个简单投票系统,前面的学习基本都是在围绕这个简单投票系统而进行的,下面只需要结合及完善起来即可。在Django模板(Template)章节我们已经可以把问题显示出来了,如下图所示:
我们接着这个要完成整个简单投票系统,工作流程图如下:
下面我们要完善问题详情页、投票功能、以及投票结果显示页面。
首先写投票详细信息模板(“polls/detail.html”),该模板包含一个HTML的<form>元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# ./polls/templates/polls/detail.html {% if error_message %} <h1><p><strong>{{ error_message }}</strong></p><h1> {% endif %} <h3>{{ question.question_text }}</h3> <form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} {% for choice in question.choice_set.all %} <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" /> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br /> {% endfor %} <input type="submit" value="Vote" /> </form> |
这个“{% csrf_token %}”标签是Django默认就有的插件,用在表单中,防止跨站攻击的,一般表单提交必须写这个标签。然后你可以在POST请求中看到这个标识。
定义detail view函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# ./polls/views.py # -*- coding: utf-8 -*- from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.http import HttpResponseRedirect from django.urls import reverse from .models import Choice, Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:3] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) def detail(request, question_id): question = get_object_or_404(Question, id=int(question_id)) print(question) return render(request, 'polls/detail.html', {'question': question}) def vote(request, question_id): return HttpResponse('Your are voting on question {}'.format(question_id)) def results(request, question_id): return HttpResponse('You are looking at results of question {}'.format(question_id)) |
如果服务启动不报错的话,访问http://10.10.0.109:8000/polls/6/会得到如下页面:
Django get_object_or_404是django shortcuts模块里面一个比较简便的方法,特别是用django get来操作数据库的时候,可以帮我们少写一些代码,加快开发速度。
get_object_or_404的介绍: 我们原来调用django的get方法,如果查询的对象不存在的话,会抛出一个DoesNotExist的异常, 现在我们调用django get_object_or_404方法,它会默认的调用django的get方法, 如果查询的对象不存在的话,会抛出一个Http404的异常,存在则返回即可。这样对用户比较友好, 如果用户查询某个产品不存在的话,我们就显示404的页面给用户,比直接显示异常好。
get_object_or_404一般需要3个参数,modelname可以为一个mode,manage或query对象。而后面的*args,**kwargs则是查询的时候用到的参数。
下面用个例子看来下:
1 2 3 4 |
>>> from polls.models import * >>> from django.shortcuts import render, get_object_or_404 >>> get_object_or_404(Question, pk=6) <Question: What sport do you like?> |
Question是要查询的model,后面的pk=6是查询条件(pk表示主键,会自动查找主键字段),你可以根据你需要查询的情况来写条件。
如果通过get_object_or_404方法获取不到对应的值呢?get_object_or_404会返回内置的404页面,如下:
投票详情页定义完了之后,接下来就要提供一个投票功能了,定义vote view函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# ./polls/views.py # ... def vote(request, question_id): question = get_object_or_404(Question, id=question_id) if request.method == 'POST': # 打印POST请求; print(request.POST) # 执行try判断提交的选项是否正常; choice_id = request.POST.get('choice', 0) try: selected_choice = question.choice_set.get(pk=choice_id) # 当没有指定选项提交时返回如下异常; except Choice.DoesNotExist: return render(request, 'polls/detail.html', {'question': question, 'error_message': "You didn't select a choice."}) # 如果try执行成功则进行投票操作,数据库更改完成后跳转至投票结果展示页面(使用reverse函数反解析出URI); else: selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) else: return HttpResponse('Your request method: %s' % request.method) |
我们这里判断了请求类型,如果访问视图不是一个POST请求,它将返回我们定义的一些内容,这是我们在第一次访问该URL时可以预期发生的情况。当我们投票时,打印出了request.POST信息,你在终端可以看到如下信息:
1 |
<QueryDict: {'csrfmiddlewaretoken': ['FuxpdRmxcPyrTYyzE64d7XHKEvhaAcnC5ihBZ5ZqF0l2f1KLTrGCIiQ1vEBcii2e'], 'choice': ['2']}> |
POST是一个QueryDict,类字典,而值是一个列表,所以我们使用request.POST.get(‘choice’, 0)方法可以用来获取用户投票选项,然后使用question.choice_set.get(pk=choice_id)反查前面获取到的投票选项ID。如果查到证明用户投票确实存在就可以往下走,如果查不到就抛出异常,只要使用get方法就要考虑异常处理,但是如果使用filter获取不到则为空,不会抛异常。
这里投票完了之后,我们使用HttpResponseRedirect重定向到了http://10.10.0.109:8000/polls/6/results/页面,防止重复提交。当然你也可以重定向到其它URL。
定义显示投票结果页面:
1 2 3 4 5 6 7 8 9 10 |
# ./polls/templates/polls/results.html <h3>{{ question.question_text }}</h3> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}: {{ choice.votes }} vote {{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a> |
定义results view函数:
1 2 3 4 5 |
# ./polls/views.py # ... def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question}) |
如果没有问题,那么我们投完票后将会重定向到http://10.10.0.109:8000/polls/6/results/页面,就会执行results函数,返回results.html页面,得到投票结果,如下:
到此为止,整个简单投票系统基本完成了。
最后回到开始哪张流程图:
从点击问题–>问题详情–>进行投票–>投票详细的完整流程链,现在已经完成了问题详情–>进行投票–>投票详细的完整链,下面只需要完成,点击问题–>问题详情的跳转,修改index.html文件,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html> <head> <title>Python Django Template</title> </head> <body> <ul> {% for question in latest_question_list %} <li> <a href="{% url 'polls:detail' question.id %}" > {{ question.question_text }} </a></li> {% endfor %} </ul> </body> </html> |
主要就是完成每个问题的超链接地址通过反解析得到其问题详情页。
二、HttpRequest对象
当请求一个页面时,Django创建一个HttpRequest对象。该对象包含request的元数据,然后Django调用相应的view函数(HttpRequest对象自动传递给该view函数<作为第一个参数>),每一个view负责返回一个HttpResponse对象。就像下面这个hello()函数:
1 2 3 4 |
from django.http import HttpResponse def hello(request): return HttpResponse("Hello world") |
下面解释HttpRequest和HttpResponse对象的API(属性),就是包含当前请求URL的一些信息:
HttpRequest.scheme
一个字符串,表示请求的方案(通常是http或https)。
HttpRequest.path
请求页面的全路径,不包括域名。例如”/hello/world”。
HttpRequest.body
一个字符串,表示原始HTTP请求的正文。它对于处理非HTML形式的数据非常有用:二进制图像、XML等。 如果要处理常规的表单数据,应该使用HttpRequest.POST,因为Django已经将这些POST数据处理成QueryDict,而不是在body中的一节字符串。
你也可以使用”类文件“形式的接口从HttpRequest中读取数据。
HttpRequest.content_type
一个字符串,根据content_type参数获取MIME类型,比如浏览器访问是text/html,API访问时application/json。开发时可以根据这个来判断是什么设备访问,然后返回不同的数据。
HttpRequest.encoding
一个字符串,表示提交的数据的编码方式(如果为None则表示使用DEFAULT_CHARSET设置)。这个属性是可写的,你可以修改它来修改访问表单数据使用的编码。接下来对属性的任何访问(例如从GET或POST中读取数据)将使用新的encoding值。如果你知道表单数据的编码不在DEFAULT_CHARSET中,则使用它。
HttpRequest.method
请求中使用的HTTP方法的字符串表示。全大写表示。例如:
1 2 3 4 |
if request.method == 'GET': do_something() elif request.method == 'POST': do_something_else() |
HttpRequest.GET
包含所有HTTP GET参数的类字典对象,参见QueryDict文档。
HttpRequest.POST
包含所有HTTP POST参数的类字典对象。参见QueryDict文档。服务器收到空的POST请求的情况也是有可能发生的。也就是说,表单form通过HTTP POST方法提交请求,但是表单中可以没有数据。因此,不能使用语句if request.POST来判断是否使用HTTP POST方法;应该使用if request.method == “POST” (参见本表的method属性)。注意: POST不包括file-upload信息,参见FILES属性。
HttpRequest.COOKIES
包含所有cookies的标准Python字典对象,Keys和values都是字符串。
HttpRequest.session
一个既可读又可写的类似于字典的对象,表示当前的会话。只有当Django启用会话的支持时才可用。
HttpRequest.FILES
用来上传文件,包含所有上传文件的类字典对象,FILES中的每个Key都是<input type=”file” name=”” />标签中name属性的值。FILES中的每个value同时也是一个标准Python字典对象,包含下面三个Keys:
- filename:上传文件名,用Python字符串表示。
- content-type:上传文件的Content type。
- content:上传文件的原始内容。
注意:只有在请求方法是POST,并且请求页面中<form>有enctype=”multipart/form-data”属性时FILES才拥有数据。否则,FILES是一个空字典。
HttpRequest.META
元数据信息,一个标准的Python字典,包含所有的HTTP头部。具体的头部信息取决于客户端和服务器,下面是一些示例:
- CONTENT_LENGTH —— 请求的正文的长度(是一个字符串)。
- CONTENT_TYPE —— 请求的正文的MIME 类型。
- HTTP_ACCEPT —— 响应可接收的Content-Type。
- HTTP_ACCEPT_ENCODING —— 响应可接收的编码。
- HTTP_ACCEPT_LANGUAGE —— 响应可接收的语言。
- HTTP_HOST —— 客服端发送的HTTP Host头部。
- HTTP_REFERER —— Referring页面。
- HTTP_USER_AGENT —— 客户端的user-agent字符串。
- QUERY_STRING —— 单个字符串形式的查询字符串(未解析过的形式)。
- REMOTE_ADDR —— 客户端的IP 地址。
- REMOTE_HOST —— 客户端的主机名。
- REMOTE_USER —— 服务器认证后的用户。
- REQUEST_METHOD —— 一个字符串,例如"GET" 或"POST"。
- SERVER_NAME —— 服务器的主机名。
- SERVER_PORT —— 服务器的端口(是一个字符串)。
从上面可以看到,除CONTENT_LENGTH和CONTENT_TYPE之外,请求中的任何HTTP头部转换为META的键时,都会将所有字母大写并将连接符替换为下划线最后加上HTTP_ 前缀。所以,一个叫做X-Bender的头部将转换成META中的HTTP_X_BENDER键。
HttpRequest.user
是一个django.contrib.auth.models.User对象,代表当前登录的用户。如果访问用户当前没有登录,user将被初始化为django.contrib.auth.models.AnonymousUser的实例。你可以通过user的is_authenticated()方法来辨别用户是否登录:
1 2 3 4 |
if request.user.is_authenticated(): # Do something for logged-in users. else: # Do something for anonymous users. |
只有激活Django中的AuthenticationMiddleware时该属性才可用。
HttpRequest.session
唯一可读写的属性,代表当前会话的字典对象。只有激活Django中的session支持时该属性才可用。
三、QueryDict对象
在HttpRequest对象中,属性GET和POST得到的都是django.http.QueryDict所创建的实例。这是一个django自定义的类似字典的类,用来处理同一个键带多个值的情况。
在Python原始的字典中,当一个键出现多个值的时候会发生冲突,只保留最后一个值。而在HTML表单中,通常会发生一个键有多个值的情况,例如<select multiple>(多选框)就是一个很常见情况。
Django提供了QueryDict API,可以用来测试这些方法,进入django shell环境即可:
1 2 |
$ python manage.py shell >>> from django.http import QueryDict |
下面我们来看这个类中有什么方法:
1 |
QueryDict.__init__(query_string=None, mutable=False, encoding=None) |
这是一个构造函数,其中query_string需要一个字符串,例如:
1 2 |
>>> QueryDict('a=1&a=2&c=3') <QueryDict: {'a': ['1', '2'], 'c': ['3']}> |
如果query_string没有传入,则获得一个空的对象。
你所遇到的QueryDict对象,特别是request.POST和request.GET得到的。如果你想自己实例化一个对象,可以传递mutable=True使你所实例化的对象可变。当然request.POST和request.GET是django创建的,也就是说除非改django源码,否则它们是不可变的。
对于设置的键和值,会从encoding转码成Unicode。也就是说,如果传入的字符串query_string是GBK或者是utf-8的编码,将会自动转码成Unicode,然后用做字典的键和值。如果encoding = None,也就是没有设定的话,将使用DEFAULT_CHARSET的值,默认为:’utf-8’。
QueryDict实现所有标准的字典方法,还包括一些特有的方法:
QueryDict.__getitem__(key)
返回给出的key的值。如果key具有多个值,__getitem__()返回最后(最新)的值。如果key不存在,则引发django.utils.datastructures.MultiValueDictKeyError。(它是Python标准KeyError的一个子类,所以你仍然可以坚持捕获KeyError。)
1 2 3 |
>>> q = QueryDict('a=1&a=2&c=3') >>> q.__getitem__('a') '2' |
QueryDict.__setitem__(key, value)
设置给出的key的值为[value](一个Python列表,只有一个元素value)。注意:只有对象是可以改变的时候才能使用,例如通过.copy()方法创建的对象,或者设置为可变类型。
1 2 3 4 5 6 7 8 9 10 |
>>> q = QueryDict('a=1') >>> d = q.copy() >>> d.__setitem__('a',2) >>> d.get('a') 2 >>> q = QueryDict('a=1', mutable=True) >>> q.__setitem__('a',2) >>> q <QueryDict: {'a': [2]}> |
QueryDict.__contains__(key)
如果给出的key已经设置,则返回True。它让你可以做if “foo” in request.GET这样的操作。
1 2 3 4 |
>>> q.__contains__('a') True >>> q.__contains__('b') False |
QueryDict.get(key, default)
使用与上面__getitem__()相同的逻辑,但是当key不存在时返回一个默认值。
1 2 |
>>> q.get('key', 0) 0 |
QueryDict.setdefault(key, default)
类似标准字典的setdefault()方法,只是它在内部使用的是__setitem__()。也就是说,当key已经存在时,返回其值,key不存在时,返回default,同时添加key和default到对象中。
QueryDict.update(other_dict)
接收一个QueryDict或标准字典。类似标准字典的update()方法,但是它附加到当前字典项的后面,而不是替换掉它们。
1 2 3 4 5 6 |
>>> q = QueryDict('a=1', mutable=True) #当然要可变的才能使用; >>> q.update({'a': '2'}) >>> q.getlist('a') ['1', '2'] >>> q['a'] # returns the last ['2'] |
QueryDict.items()
类似标准字典的items()方法,返回一个迭代对象。但是它使用的是和__getitem__一样返回最新的值的逻辑。
1 2 3 4 5 6 7 8 9 |
>>> q = QueryDict('a=1&a=2&b=3') >>> m = q.items() >>> m.__name__ '_iteritems' >>> m.__next__() ('b', '3') >>> m.__next__() ('a', '2') >>> m.__next__() #会抛异常; |
QueryDict.lists()
类似QueryDict.iteritems(),返回一个包含键值对的元祖(key, value)迭代对象 ,value是一个包括所有key的值的列表。
1 2 3 4 5 6 |
>>> q = QueryDict('a=1&a=2&b=3') >>> m = q.lists() >>> m.__next__() ('a', ['1', '2']) >>> m.__next__() ('b', ['3']) |
QueryDict.values()
类似标准字典的values()方法,但是它使用的是和__getitem__一样返回最新的值的逻辑。也就是返回一个所有键对应的最新值的列表。
1 2 3 4 5 6 |
>>> q = QueryDict('a=1&a=2&b=3') >>> m = q.values() >>> m.__next__() '3' >>> m.__next__() '2' |
QueryDict.copy()
返回对象的副本,使用Python标准库中的copy.deepcopy()。此副本是可变的,即使原始对象是不可变的。
1 2 3 4 |
>>> q = QueryDict('a=1&a=2&c=3') >>> m = q.copy() >>> m <QueryDict: {'a': ['1', '2'], 'c': ['3']}> |
QueryDict.getlist(key, default)
以Python列表形式返回所请求的键的数据。如果键不存在并且没有提供默认值,则返回空列表。它保证返回的是某种类型的列表,除非默认值不是列表。
1 2 3 4 5 |
>>> q = QueryDict('a=1&a=2&b=3') >>> q.getlist('a') ['1', '2'] >>> q.getlist('c', 0) 0 |
QueryDict.setlist(key, list_)
为给定的键设置list_(与__setitem__()不同),可以设置一个多元素的列表。
QueryDict.appendlist(key, item)
将项追加到内部与键相关联的列表中。
QueryDict.setlistdefault(key, default_list)
类似setdefault,除了它接受一个列表而不是单个值。
QueryDict.pop(key)
返回给定键的值的列表,并从字典中移除它们。如果键不存在,将引发KeyError。
1 2 3 |
>>> q = QueryDict('a=1&a=2&a=3', mutable=True) >>> q.pop('a') ['1', '2', '3'] |
QueryDict.popitem()
删除字典任意一个成员(因为没有顺序的概念),并返回二值元组,包含键和键的所有值的列表。在一个空的字典上调用时将引发KeyError。
1 2 3 4 5 |
>>> q = QueryDict('a=1&a=2&b=3', mutable=True) >>> q.popitem() ('a', ['1', '2']) >>> q <QueryDict: {'b': ['3']}> |
QueryDict.dict()
返回QueryDict的dict表示形式。对于QueryDict中的每个(key, list)对 ,dict将有(key, item) 对,其中item是列表中的一个元素,使用与QueryDict.__getitem__()相同的逻辑,也就是最新的:
1 2 3 |
>>> q = QueryDict('a=1&a=3&a=5') >>> q.dict() {'a': '5'} |
QueryDict.urlencode([safe])
从数据中返回查询字符串格式。
1 2 3 |
>>> q = QueryDict('a=2&b=3&b=5') >>> q.urlencode() 'a=2&b=3&b=5' |
可选地,urlencode可以传递不需要编码的字符。(这意味着要进行url编码)
1 2 3 4 |
>>> q = QueryDict(mutable=True) >>> q['next'] = '/a&b/' >>> q.urlencode(safe='/') 'next=/a%26b/' |