之前有在用Django写一些小网站,现在暑假想说再来複习一下之前买的这本书
于是我就把它写成一系列的文章,也方便查语法
而且因为这本书大概是2014年出的,如今Django也已经出到2.多版
有些内容也变得不再支援或适用,而且语法或许也改变了
所以我会以最新版的Python和Django来修正这本书的内容跟程式码
目录:django系列文章-Django学习纪录
11. 表单的验证与模型化
11.1 表单验证
views.py
...def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") error = False if request.POST: visitor = request.POST['visitor'] content = request.POST['content'] email = request.POST['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 error = any(not request.POST[k] for k in request.POST) if '@' not in email: error = True if not error: Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) return render(request, 'comments.html', locals())...
comments.html
... {% else %} <p>无评价</p> {% endif %} <br/><br/> {% if error %} <p style="color: red;">* 表单输入不完整,请重新输入</p> {% endif %} <form action="" method="post"> {% csrf_token %} <table>...
一旦有栏位未填,error就会被设为True,并且不会新增资料
然后将错误讯息显示给使用者
接下来我们希望检查更精确
views.py
...def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") errors = [] if request.POST: visitor = request.POST['visitor'] content = request.POST['content'] email = request.POST['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 if any(not request.POST[k] for k in request.POST): errors.append('* 有空白栏位,请不要留空') if '@' not in email: errors.append('* email格式不正确,请重新输入') if not errors: Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) return render(request, 'comments.html', locals())...
comments.html
... {% else %} <p>无评价</p> {% endif %} <br/><br/> {% for e in errors %} <p style="color: red;">{{ e }}</p> {% endfor %} <form action="" method="post"> {% csrf_token %} <table>...
如果用户不小心忘记填某个资料,虽然我们给了用户错误讯息
但是用户已经填好的资料我们还是要让用户不需要再重新填
comments.html
... <table> <tr> <td> <label for="visitor">留言者:</label> </td> <td> <input id="visitor" type="text" name="visitor" value="{{visitor}}"> </td> </tr> <tr> <td> <label for="email">电子信箱:</label> </td> <td> <input id="email" type="text" name="email" value="{{email}}"> </td> </tr> <tr> <td> <label for="content">评价:</label> </td> <td> <textarea id="content" rows="10" cols="48" name="content" value="{{content}}"></textarea> </td> </tr> </table>...
如果表单顺利填完送出资料,那就必须将表单内容清空
views.py
def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") errors = [] if request.POST: visitor = request.POST['visitor'] content = request.POST['content'] email = request.POST['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 if any(not request.POST[k] for k in request.POST): errors.append('* 有空白栏位,请不要留空') if '@' not in email: errors.append('* email格式不正确,请重新输入') if not errors: Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) visitor, content, email = ('', '', '') return render(request, 'comments.html', locals())
我们先前在建立模型的时候,有些资料栏位会做blank=True的设定,那理所当然的,没做该项设定的资料栏位应该会被列为必填,难道我们提交表单的时候,资料库或Django不会抗议吗?答案是:目前还不会
这是因为blank=True的设定是针对资料验证而不是资料库的栏位的描述。大家可以利用
python manage.py sqlmigrate APP_NAME
去看一次语法,除了多设定为NOT NULL外,Django并没有对资料表做更多描述,那我们是白描述的吗?当然不,还记得admin吧,必填栏位一定要输入资料,不然会得到警告,admin怎么会知道该栏位必填呢?忘记我们有注册模型上去了吗!(blank=True的用意便是在此)
11.2 表单模型化
在django中除了资料库可以利用物件映射模型化之外,表单也可以
11.2.1 建立表单模型
restaurants/forms.py
from django import formsclass CommentForm(forms.Form): visitor = forms.CharField(max_length=20) email = forms.EmailField(max_length=20, required=False) content = forms.CharField(max_length=200, widget=forms.Textarea())
required参数代表是不是必填
在这边,变数设置的顺序也很重要,会影响到预设输出的顺序,尽量得依照最后输出到html上的顺序来设置
11.2.2 操作表单模型
进入django shell
python manage.py shell
>>> from restaurants.forms import CommentForm>>> f = CommentForm()>>> print(f)<tr><th><label for="id_visitor">Visitor:</label></th><td><input type="text" name="visitor" maxlength="20" required id="id_visitor"></td></tr><tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" maxlength="20" id="id_email"></td></tr><tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content"></textarea></td></tr>
我们建立了一个非绑定的表单f,所谓非绑定即一个未填资料的表单
如果用print将他列印,我们将会发现他以html表格(<table>
)的方式输出,我们稍微地来研究一下输出的表单元件标籤属性跟表单模型的变数有什么关係
一个表单模型栏位会产生一个<label>
元件和一个<input>
元件
每个<input>
元件会依据表单模型的Field类型来决定它的type属性如:EmailField对映到type="email"
每个<input>
会产生一个id属性,值为 id_表单模型变数名称
每个<input>
会产生一个name属性,值为表单模型变数名称
另外一些描述性的参数也会加入如:max_length
其他输出方法:
>>> f.as_p() # 段落输出'<p><label for="id_visitor">Visitor:</label> <input type="text" name="visitor" maxlength="20" required id="id_visitor"></p>\n<p><label for="id_email">Email:</label> <input type="email" name="email" maxlength="20" id="id_email"></p>\n<p><label for="id_content">Content:</label> <textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></p>'>>> f.as_ul() # 列表输出'<li><label for="id_visitor">Visitor:</label> <input type="text" name="visitor" maxlength="20" required id="id_visitor"></li>\n<li><label for="id_email">Email:</label> <input type="email" name="email" maxlength="20" id="id_email"></li>\n<li><label for="id_content">Content:</label> <textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></li>'>>> f.as_table() # 表格输出'<tr><th><label for="id_visitor">Visitor:</label></th><td><input type="text" name="visitor" maxlength="20" required id="id_visitor"></td></tr>\n<tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" maxlength="20" id="id_email"></td></tr>\n<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></td></tr>'
11.2.3 输出表单
views.py
...from restaurants.forms import CommentFormdef comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") errors = [] if request.POST: visitor = request.POST['visitor'] content = request.POST['content'] email = request.POST['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 if any(not request.POST[k] for k in request.POST): errors.append('* 有空白栏位,请不要留空') if '@' not in email: errors.append('* email格式不正确,请重新输入') if not errors: Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) visitor, content, email = ('', '', '') f = CommentForm() return render(request, 'comments.html', locals())...
comments.html
... <form action="" method="post"> {% csrf_token %} <table> {{ f.as_table }} </table> <input type="submit" value="给予评价"> </form>...
结果
如果留空或是格式错误便会出现警告了
这是html‵<input>
元件中email type的验证帮助
11.2.4 表单的绑定与验证
所谓绑定就是表单已输入资料,与资料绑定
例如
>>> from restaurants.forms import CommentForm>>> f = CommentForm()>>> f.is_boundFalse>>> f = CommentForm({'visitor':'dokelung','email':'dokelung@gmail.com','content':'Good!'})>>> f.is_boundTrue
只要提供表单类别一个字典参数(该字典的键值对刚好对应到表单元件(栏位)的名称与要填入的值),就可以将表单与资料绑定
透过表单物件的is_bound属性,我们便可以得知它是否被绑定(已填入资料)
一个已绑定的表单物件,便可以进行验证(未绑定的表单是不能进行验证的,这很直觉,没有填入资料的表单是不能判定对错的)
接下来我们利用表单物件的is_valid方法对下列状况进行验证:
>>> f = CommentForm({'visitor':'dokelung','email':'dokelung@gmail.com','content':'Good!'})>>> f.is_valid()True
电子邮箱未填>>> f = CommentForm({'visitor':'dokelung','content':'Good!'})>>> f.is_valid()True
因为required=False
3. 用户名字未填
>>> f = CommentForm({'email':'dokelung@gmail.com','content':'Good!'})>>> f.is_valid()False
如果没有放required这个参数,那么预设会是True
4. 电子邮箱错误格式
>>> f = CommentForm({'visitor':'dokelung','email':'dokelung','content':'Good!'})>>> f.is_valid()False
利用errors属性
>>> f = CommentForm({'user':'dokelung','email':'dokelung@gmail.com','content':'Good!'})>>> f['email'].errors[]>>> f = CommentForm({'email':'dokelung','content':'Good!'})>>> f['visitor'].errors['This field is required.']>>> f['email'].errors['Enter a valid email address.']>>> f.errors{'visitor': ['This field is required.'], 'email': ['Enter a valid email address.']}
最后,表单物件中的cleaned_data字典会提供给我们合法栏位(通过验证的栏位)的数据,不过该属性要在表单有进行过任何验证手段后才存在(包含呼叫is_valid或是存取errors字典等),我们来看看一个绑定的表单会怎么输出:
>>> f = CommentForm({'email':'dokelung','content':'Good!'})>>> f.cleaned_dataTraceback (most recent call last): File "<console>", line 1, in <module>AttributeError: 'CommentForm' object has no attribute 'cleaned_data'>>> f.is_valid()False>>> f.cleaned_data{'content': 'Good!'}
如果没有呼叫is_valid或是存取errors字典过,就存取cleaned_data,则会出现错误
>>> f.as_table()'<tr><th><label for="id_visitor">Visitor:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="visitor" maxlength="20"required id="id_visitor"></td></tr>\n<tr><th><label for="id_email">Email:</label></th><td><ul class="errorlist"><li>Enter a valid email address.</li></ul><input type="email" name="email" value="dokelung" maxlength="20" id="id_email"></td></tr>\n<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\nGood!</textarea></td></tr>'
合法的栏位会连带连绑定的资料一起输出(value属性)
而不合法的栏位会出现一个<ul>
列表,里面一项一项列的是该栏位的错误讯息与提示
而<ul>
标籤的class属性会设为errorlist,如果想要把错误讯息改为红色
在html的<head>
中加入CSS
<style> .errorlist{ color: red; }</style>
即可
我们放弃之前撰写的验证改用表单模型作验证
views.py
...def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") if request.POST: f = CommentForm(request.POST) if f.is_valid(): visitor = f.cleaned_data['visitor'] content = f.cleaned_data['content'] email = f.cleaned_data['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) visitor, content, email = ('', '', '') f = CommentForm() else: f = CommentForm() return render(request, 'comments.html', locals())...
comments.html
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .errorlist{ color: red; } </style> </head> <body> <h2>{{ r.name }}的评价</h2> {% if r.comment_set.all %} <p>目前共有{{ r.comment_set.all|length }}条评价</p> <table> <tr> <th>留言者</th> <th>时间</th> <th>评价</th> </tr> {% for c in r.comment_set.all %} <tr> <td> {{ c.visitor }} </td> <td> {{ c.date_time | date:"F j, Y" }} </td> <td> {{ c.content }} </td> </tr> {% endfor %} </table> {% else %} <p>无评价</p> {% endif %} <br/><br/> {% if f.errors %} <p style="color:red;"> 请依提示修复表单错误 </p> {% endif %} <form action="" method="post"> {% csrf_token %} <table> {{ f.as_table }} </table> <input type="submit" value="给予评价"> </form> </body></html>
当表单确定被提交后,我们会利用request.POST这个类字典当做CommentForm的字典参数产生一个表单物件,再透过is_valid方法检查表单的正确性,如果正确,我们产生一个评价并存入资料库,并且重设变量f为未绑定表单(空表单)
若不正确,变量f的各栏位依然会有原先填入的值
当然,如果表单未被提交,使用者将可以看到一个全新的空表单(未绑定表单)
11.2.5 客製化的表单输出
更换表单元件
表单文字类栏位预设是以<input>
作为html元件,如果想要更改
那就可以在表单模型中设定widget参数
content栏位使用<textarea>
而不是<input>
from django import formsclass CommentForm(forms.Form): visitor = forms.CharField(max_length=20) email = forms.EmailField(max_length=20, required=False) content = forms.CharField(max_length=200, widget=forms.Textarea())
设定表单元件属性
透过表单物件的参数来设定
例如max_length
或是min_length
设定表单栏位初始值
利用initial
参数
...def comment(request, id): if id: r = Restaurant.objects.get(id=id) else: return HttpResponseRedirect("/restaurants_list/") if request.POST: f = CommentForm(request.POST) if f.is_valid(): visitor = f.cleaned_data['visitor'] content = f.cleaned_data['content'] email = f.cleaned_data['email'] date_time = timezone.localtime(timezone.now()) # 撷取现在时间 Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r) visitor, content, email = ('', '', '') f = CommentForm(initial={'content': '我没意见'}) else: f = CommentForm(initial={'content': '我没意见'}) return render(request, 'comments.html', locals())...
要注意的是,这里产生的表单物件虽有预设值(初始值),但并未被绑定,他们是无法被验证的
自订验证规则
在表单模型中加入以clean_表单栏位
名称为名的验证方法
它会在该栏位通过预设检查后,透过此方法做自定义的检查:
forms.py
from django import formsclass CommentForm(forms.Form): visitor = forms.CharField(max_length=20) email = forms.EmailField(max_length=20, required=False) content = forms.CharField(max_length=200, widget=forms.Textarea()) def clean_content(self): content = self.cleaned_data['content'] if len(content) < 5: raise forms.ValidationError('字数不足') return content
首先我们从表单物件中拿出content栏位的cleaned_data,这点不用觉得奇怪,我们已经经过了基本的验证(CharField的验证,包含栏位不能为空等等),所以这边自然可以拿到cleaned_data,否则会在更早的地方便知道错误,便也不会进行本项检查了
接着我们将已经进行完基本检查的乾净content数据从cleaned_data中拿出来,并且用len计算字数,小于5字我们便引发一个ValidationError例外,而使用的字串参数将会成为表单栏位验证错误的提示
如果字数足够,我们会回传content作为验证后的表单值,这会形成最新的cleaned_data值,你也可以在这边对数据做一些修改
整理一下:
设定栏位标籤名称
每个表单元件都会附上一个<label>
标籤,他预设显示的字串是空格取代底线,第一个字母大写
就跟资料模型在admin中显示栏位名称的规则一样
若想要自定<label>
标籤的内容,可以透过在表单模型中各栏位的参数label来指定:
email = forms.EmailField(max_length=20, required=False, label='E-mail')
栏位个别输出与表单样式客製化
一次输出整个表单模型我们就无法做更动或调整
因此我们想要单独输出每一个元件
>>> from restaurants.forms import CommentForm>>> f = CommentForm({'visitor':'dokelung','email':'dokelung@gmail.com','content':'Good!'})>>> f['visitor']<django.forms.boundfield.BoundField object at 0x03491B30>>>> print(f['visitor'])<input type="text" name="visitor" value="dokelung" maxlength="20" required id="id_visitor">
把表单物件当作字典,便可以分别输出指定的栏位(不含<label>
标籤)
这个字典的键值都是一个BoundField物件,包含了errors这个纪录错误的属性
接下来我们来对表单作修改
comments.html
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .errorlist{ color: red; } </style> </head> <body> <h2>{{ r.name }}的评价</h2> {% if r.comment_set.all %} <p>目前共有{{ r.comment_set.all|length }}条评价</p> <table> <tr> <th>留言者</th> <th>时间</th> <th>评价</th> </tr> {% for c in r.comment_set.all %} <tr> <td> {{ c.visitor }} </td> <td> {{ c.date_time | date:"F j, Y" }} </td> <td> {{ c.content }} </td> </tr> {% endfor %} </table> {% else %} <p>无评价</p> {% endif %} <br/><br/> {% if f.errors %} <p style="color:red;"> 请依提示修复表单错误 </p> {% endif %} <form action="" method="post"> {% csrf_token %} <table> <tr> <th> <label for="id_visitor">留言者:</label> </th> <td> {{ f.visitor }} </td> <td> {{ f.visitor.errors }} </td> </tr> <tr> <th> <label for="id_email">电子信箱:</label> </th> <td> {{ f.email }} </td> <td> {{ f.email.errors }} </td> </tr> <tr> <th> <label for="id_content">评价:</label> </th> <td> {{ f.content }} </td> <td> {{ f.content.errors }} </td> </tr> </table> <input type="submit" value="给予评价"> </form> </body></html>
结果