practical django project 第九章 代码共享应用中的表单处理
第九章 代码共享应用中的表单处理
目前为止你所有的Django应用——除了weblog的评论系统——全部集中在站点的可信任用户通过Django的管理界面输入内容,而没有允许普通用户提交内容的机制。因此,在这个应用中你将需要一个方式允许用户提交他们自己的代码片段。你还需要确保他们提交的内容是符合你在模型中设定的数据格式的。
幸运的是,Django通过使用一种简单而强大的显示和处理基于web表单的系统使这项任务变得异常简单。在本章中,你将彻底的了解Django表单处理系统并使用它来创建一个可使用户提交和编辑它们代码的样例。
9.1一个简要的Django表单系统的介绍
Django表单处理代码,位于django.forms模块中,提供了三个关键的组件,涵盖了构建,显示,处理表单的各个方面:
- 一个字段类集合,类似于Django数据模型中的字段类型,表示了特定的数据类型而且知道该如何验证它们。
- 一个组件(widget)类集合,知道该如何生成不同类型的HTML控制表单(文本输入框,选择框等等),并从一个HTTP表单提交请求中读取相应的数据。
- 一个Form类将这些概念整合到一起并提供了统一接口来定义数据及校验数据的高级规则
9.1.1 一个简单的例子
为了来感受下它是如何工作的,我们来看一个简单但是常用的需求:用户注册。
#Admonition#这些代码到哪去了?
这段特定的代码逻辑上是不属于你正在开发的cab应用的,如果你已经开发了处理用户注册的代码,那么最好是将这些代码放置在独立的应用中。不过目前不用担心如何在python文件中保存这些代码的问题。这只是一个用于尽可能多的展示django表单处理系统的有用的例子。如果你确实有需要实现一个用户注册系统,当然,你可以自由的引用这段代码并调整到合适于你需要的样子。
基本的注册表单由三部分数据组成:
- 一个用户名
- 一个与新账户关联的e-mail地址
- 一个用于登陆的密码
另外,你还要做一点自定义的校验工作:
- 需要保证不存在两个相同的用户名
- 显示两个密码字段让用户输入同一个密码两次一直是个好主意。这会检查出输入时的错误并提供一点额外的安全措施,确保用户输入的的确是它们想要的密码。
从逻辑上说,这会产生一个有四个表单域的<element>元素:用户名和e-mail各一个,另外两个处理重复密码。下面就是构建这个表单开始时的样子:
1 from django import forms 2 3 class SignupForm(forms.Form): 4 username = forms.CharField(max_length = 30) 5 email = forms.EmailField() 6 password1 =forms.CharField(max_length = 30) 7 password2 = forms.CharField(max_length = 30)
除了使用django.froms代替django.db.models,以上代码看起来和定义数据模型时十分类似:简单的子类化适当的基类并加入合适的字段。
但是这些代码并不完善。HTML提供了一个特殊的表单输入类型来处理密码——<input type="password">——这是生成密码域的适当的方式。你可以轻微改写这两个字段来实现这个输入类型:
password1 = forms.CharField(max_length=30,widget=forms.PasswordInput())
password2 = forms.CharField(max_lengt=30,widget = forms.PasswordInput())
PasswordInput组件将会产生<input type="password">。这同样也显示出Django表单系统有分离的数据校验机制,一个是在字段中处理的,一个是在组件中处理的。最常见的情况是你有一个单独的基本校验规则需要使用与多个不同的字段,并且是来源于不同类型的HTML输入。这种分离校验机制就使事情变简单了:你可以重用一个字段类型而只需要修改包含的组件。
现在,再做一些修改:
password1 = forms.CharField(max_length=30,widget =forms.PasswordInput(render_value=False))
password2 = forms.CharField(max_lengt=30,widget = forms.PasswordInput(render_value=False))
参数render_value告诉PasswordInput尽管有一些数据,但是不会显示出来。当输入密码时产生了错误原先的数据会被完全清除使下次用户能正确输入。
9.1.2 校验用户名
到目前为止你所设置的字段都包含有隐式的校验规则。username字段及两个密码字段都有最大长度设置,Email字段会确认输入的是正常的e-mail格式的地址(通过正则表达式)。但还是需要保证用户名不是已经存在的,因此需要自定义校验规则。
你可以在form中定义一个名为你call_username()的方法。在校验过程中,Django表单系统会自动查找任何以call_开始,以字段名结束的方法,然后在字段内建的校验规则完成后调用。
下面是clean_username()方法看起来的样子(假设Django User模型已经被导入了):
User.objects.get(username=self.cleaned_date['username']) except User.DoesNotExist: return self.cleaned_data['username'] rasise forms.ValidationError("Thisusernameis already in use.Please choose another.")
这段代码短小精悍。首先,这个方法只会在username字段少于内建规则的30个字符时才会被调用。在这种情况下,username字段提交的值在self.cleaned_data['username']中。这个cleaned_data属性是一个所有被提交数据中那些已经通过校验后的数据值组成的字典。
查询是否存在一个用户与username字段提交的值精确匹配。如果没有这样一个用户则Django会抛出一个UserNotExist异常。这个异常告诉你这个用户不存在,因此这个username字段的值即通过了校验。在这种情况下,只简单返回这个值。
9.1.3 校验密码
校验密码是一个需要点技巧的,因为需要同时查看两个字段并确保两项匹配。你可以为一个字段定义一个方法,如下:
def clean_password2(self): if self.cleaned_data['password1'] != self.cleaned_data['password2']: raise forms.ValidationError("You must type the same password each time") return self.cleaned_data['password2']
但是还有一个更好的方式来实现这个校验。Django允许你定义一个校验方法——简单的名为clean()——可以应用于整个表单。下面是写法:
def clean(self): if 'password1' in self.clean_data and 'password2' in self.cleaned_data: if self.clean_data['password1'] != self.cleaned_data['password2']: raise forms.ValidationError("You must type the same password each time") return self.cleaned_data
注意在这种情况下,你手动的检查了两个password字段的值是否存在于clean_data中。如果有任何错误在单独的字段校验中被抛出,cleand_data就会变成空的。因此你需要在引用任何你需要的值之前检查它。
#Admonition# 默认的表单字段都是必须的
所有内建于Django表单系统的字段类型都是必须的,因此不能被留空。如果密码字段中的一个被留空,Django会在调用clean()方法前产生一个ValidationError的异常,因此你不需要为必需的值生成额外的错误。为了将一个字段标记为可选,可以向其传递一个关键字参数required=False。
9.1.4 创建新用户
现在,停下写form代码的手转到处理表单的视图上来。你可以创建视图使得可以创建和保存新的用户对象。但是如果一旦你需要在别的视图中重用这个form,就需要一遍遍的重写这样的代码。所以一个更好的办法是在form中编写一个方法如何处理校验后的数据。因为这个方法是保存一个新的User对象到数据库中,所以我们称其为save().
#Admonition# save()方法不只是针对数据库的
大多数时候,表单用于创建和更新模型对象,在这些情况下save()方法是最自然的选择。但是表单可能被用于其他目的(例如,一个联系表单可能会发送一封电子邮件而不是保存一个对象)。在Django社区中一般的约定是:任何时候表单类都有一个“知道”该如何处理处理校验后数据的方法,这个方法应该被叫做save(),即便它有时候可能不保存任何数据到你的数据库中去。给这类方法一个固定和易识别的名字的好处远大于可能导致的初始化混淆。
在save()中,你需要从form提交的用户名,e-mail和密码中生成一个User对象。假设你已经导入了User模型,下面可以这样编写:
def save(self): new_user = User.objects.create_user(username=slef.cleaned_data['username'], email=self.cleaned_data['email'],password=self.cleaned_data['password1']) return new_user
#Admoniton# 用户名及密码
储存在数据库中的用户名和密码有一个很大的问题,即任何能访问数据库的人都可能看到全部密码。由于很多人都喜欢在不同的站点上使用相同的密码,这就会导致恨严重的安全风险。为了帮助保护你的用户,Django阻止在数据库中储存用户实际用于登录的纯文本的密码。Django使用一套称为哈希函数(hash function)的数学技巧,将密码转换为一组随机的(实际上不是正真随机的)字符串和数字的组合。储存在数据库中的其实是这组随机结果。这样做的好处是,哈希函数只能单向工作:如果知道了密码可以使用哈希函数并总是得到同样的结果,但是如果只是知道结果,就不能反向的得到实际密码。这就提供了一种合理的储存密码的安全方法。当你尝试登录的时候,Django的认证系统对输入的密码运用哈希函数并比较其结果与数据库中储存的结果。这就表示纯文本的密码不必永久的保存在任何地方。但是由于这个系统的工作需要一定的技巧,Django的User模块有一个自定的管理器定义了你在这里使用的create_user()方法。这个方法负责处理对密码使用哈希函数并储存正确的结果。
下面是最终完成后的form:
1 from django.contrib.auth.models import User 2 from django import forms 3 4 class SignupForm(forms.Form): 5 username = forms.CharField(max_length=30) 6 email = forms.EmailField() 7 password1 = forms.CharField(max_length=30,widget = forms.PasswordInput(render_value=False)) 8 password2 = forms.CharField(max_length=30,widget = forms.PasswordInput(render_value=False)) 9 10 def clean_username(self): 11 try: 12 User.objects.get(username=self.cleaned_data['username']) 13 except User.DoesNotExist: 14 return self.cleaned__data['username'] 15 raise forms.ValidationError("This username is already in use. Please choose another.") 16 17 def clean(self): 18 if 'password1' in self.clean_data and 'password2' in self.cleaned_data: 19 if self.clean_data['password1'] != self.cleaned_data['password2']: 20 raise forms.ValidationError("You must type the same password each time") 21 return self.cleaned_data 22 23 def save(self): 24 new_user = User.objects.create_user(username =slef.cleaned_data['username'], 25 email=self.cleaned_data['email'], 26 password=self.cleaned_data['password1']) 27 return new_user
9.1.5 表单验证的工作方式
在视图中用来确定是否有数据通过校验的方法名为is_valid(),被定义与Form基类,所有Django表单都源于该基类。在Form类中,is_valid( ) touches off表单的验证路径,以一个特定顺序,通过一个叫做full_clean( )的方法(django.form中的Form基类中定义的其他方法见图9-1)。
校验 的顺序如下:
- 首先,full_clean()在每个表单域上循环。每一个字段类都有一个clean()方法,实现了字段的内建校验规则,每一个这样的方法会抛出ValidationError异常或返回一个值。如果一个ValidationErro方法被抛出,之后对这个字段的校验就不会继续(因为已经知道这个字段中的数据不可用)。如果返回的是一个值,将会被加入到表单的cleaned_data字典。
- 如果一个字段 的内建clean( )方法没有抛出ValidationError异常,那么任何自定义的校验方法就会被执行——这些自定义的校验方法以clean_开始以字段名结束。这些方法同样不是抛出异常就是返回一个值,如果返回了一个值,则被加入clean_data。
- 最后,表单的clean()方法被调用,它同样也会产生ValidationError异常,虽然它不与任何具体的字段相关联。如果clean()方法没有产生新的错误,它将返回一个完整的包含表单数据的字典,通常是这样:return self.cleaned_data。
- 如果没有校验错误产生,表单的cleaned_data字典将会被校验后的数据完全填充。如果存在校验错误,cleaned_data就不会存在,而一个含有错误的字典(self.errors)将会被校验错误填充。每个字段都知道该如何从字典中提取它自己的错误,这就是为什么你可以在模板中使用{{ form.username.errors }}的原因。
- 最后,is_valid()当存在校验错误的时候返回False,没有错误的时候返回True。
理解这个过程是充分发挥Django表单处理系统的作用的关键。可能初看起来有些复杂,但是在不同地方为表单附加校验规则的能力产生了巨大的灵活性而且能更容易的写出可重用的代码。举个例子,如果你需要不断的使用一个特定类型的校验多次,你会注意到给每一个表单都写自定义方法十分繁琐。最好是写一个自定义字段类,定义一个clean()方法,然后重用这个字段。
类似的,区分开特定字段校验方法和“表单级”clean( )方法显示出大量验证多个字段组合的技巧。当使用单独一个字段时不一定需要这些技巧。
9.1.6 处理表单
现在,来看看用于显示和处理表单的视图:
from django.http import HttpResponseRedirect from django.shortcuts import render_to_response def signup(request): if request.method =='POST': form = SignupForm(data = request.POST) if form.is_valid(): new_user = form.save() return HttpResponseRedirect("/account/login/") else: form = SignupForm() return render_to_response('signup.html',{'form':form})
我们一步步的分解来看:
- 首先看看这个方法的HTTP请求。通常是GET或者POST.(还有一些不常用的HTTP方法,典型的浏览器只支持GET和POST用于表单提交。)
- 当且仅当,request请求是POST 的时候,你实例化一个SignupForm并传递一个request.POST作为form数据。回顾第三章中,当写一个简单的search函数的时候,已经见到过随GET请求发送的request.GET是一个字典;类似的,request.POST也是一个数据字典(在本例中,是提交的表单数据)通过POST请求发送。
- 调用表单的is_valid()检查是否提交的数据通过了校验。在这个结果之下是匹配表单中字段和提交的数据并检查每个字段的校验规则。如果数据通过校验,is_valid()将会返回True并且表单的cleaned_data字典将被正确的值填充。否则,is_valid()将会返回False,并且cleaned_字典不会存在。
- 如果数据通过校验,则调用之前定意的save()方法。然后返回一个HTTP重定向——使用django.http.HttpResponseRedirect——到一个新页面,应该是一个新用户登录的视图。任何时候你通过HTTP POST接收了数据,在处理成功后应该总是重定向。通过将用户导入一个新页面,避免了一个常见的陷阱,即刷新页面或点击返回按钮时意外的重新提交表单。
- 如果request方法不是POST,那么实例化一个SignupForm不带有任何数据。技术上讲,这叫做非绑定的表单(没有任何可以使用的数据),和绑定表单相反,没有任何可提交的数据。
- 渲染一个模板,将表单作为一个变量传递给模板,然后返回一个响应。注意因为视图编写的方式,如果用户提交了验证后的数据你永远不会执行到这一步。在这种情况下,if语句会保证返回一个重定向。同样,注意这一步也会忽略掉是否存在未通过校验的数据或者根本没有数据——SignupForm对象不会根据情况的不同有不同的处理方式。
最后,来看看使用这个视图如何在signup.html模板中显示这个表单:
1 <html> 2 <head> 3 <title>Sign up for an account</title> 4 </head> 5 <body> 6 <h1>Sign up for an account</h1> 7 <p>Use the form below to register for your new account; all 8 fields are required.</p> 9 <form method="post" action=""> 10 {% if form.non_field_errors %} 11 <p><span class="error"> 12 {{ form.non_field_errors|join:", " }} 13 </span></p> 14 {% endif %} 15 <p>{% if form.username.errors %} 16 <span class="error">{{ form.username.errors|join:", " }}</span> 17 {% endif %}</p> 18 <p><label for="id_username">Username:</label> 19 {{ form.username }}</p> 20 <p>{% if form.email.errors %} 21 <span class="error"> 22 {{ form.email.errors|join:", " }} 23 </span> 24 {% endif %}</p> 25 <p><label for="id_name">Your e-mail address:</label> 26 {{ form.email }}</p> 27 <p>{% if form.password1.errors %} 28 <span class="error"> 29 {{ form.passsword1.errors|join:", " }} 30 </span> 31 {% endif %}</p> 32 <p><label for="id_password1">Password:</label> 33 {{ form.password1 }}</p> 34 <p>{% if form.password2.errors %} 35 <span class="error"> 36 {{ form.passsword2.errors|join:", " }} 37 </span> 38 {% endif %}</p> 39 <p><label for="id_password2">Password (again, to catch 40 typos): </label> 41 {{ form.password2 }}</p> 42 <p><input type="submit" value="Submit"></p> 43 </form> 44 </body> 45 </html>
这里的HTML语句大多都很简单:一个标准的<form>标签,每个字段带有一个<label>标签以及一个提交按钮。但是注意最终是如何实际显示字段的。每一个都作为{{ form }}变量的属性被访问。你可以检查每一个是否有错误,并且显示错误信息(将会是一种列表形式,即使只有一条信息,因此使用join过滤器,用一个特定字符将列表中的项组合起来。)
注意,虽然在表单的开始使用了{{ form.non_field_errors }}。这是因为clean()方法抛出的异常不属于任何一个字段(因为是来自于比较两个字段)。无论什么时候有一个潜在的校验错误来自clean( )方法,你需要检查non_field_errors并在存在的时候显示它。
9.2 编写表单来添加代码片段
现在,用户注册的例子已经给你关于如何编写一个接受提交数据信息的表单的很好的启发,你可以编写一个来添加Snippte模型实例。你可以简单的设置想要用户填写的字段信息,然后给它一个save()方法,用于创建和保存新的代码段。
但是现在这里有一个新的需要处理的问题。Snippet模型中的author字段必须填写,并且是被正确的填写,但是你不希望将其显示给用户并让他们来选择这个值。如果你这样做了,那么任何人都可以通过填写一个其他人的名字来假装成是别的用户。所以你需要某种方法来填写这个字段而又不使它成为表单中公开的部分。
幸运的是,这很容易办到:一个表单就是一个Python类。因此你可以添加你自己的__init__()方法并信任处理这个表单的视图函数将会传入确定的正确的、经过认证的用户,你可以储存并在需要保存snippet的时候引用。那么,我们来开始编写AddSnippetForm。
在cab目录下创建一个forms.py文件。在其中开始编写如下代码:
from django import forms from cab.models import Snippet class AddSnippetForm(forms.Form): def __init__(self,author,*args,**kwargs): super(AddSnippteForm,self).__init__(*args,**kwargs): self.author = author
除了接受一个额外的参数——author以便之后使用——在这里还做了两件重要的事:
- 对额外的author参数,你设定了一个接收*args和**kwargs的__init__()方法。这是Python当中用来接收任意组合的可选参数和关键字参数的一种快捷方法。
- 使用supre()调用父类的__init__()方法,传递了自定义__init__()方法接受到的其它参数。这保证了Form基类中的__init__()方法被调用,并将你表单中的其它内容设置妥当。
使用这一技术——接受*args和**kwargs并传递给父类方法——是使复写的方法接收大量参数,特别是大量的可选参数时的一种有用的快捷方法。Form基类的__init__()方法实际上接收了七个全部是可选的参数,这是一个方便的小技巧。
现在可以添加所需要的字段:
title = forms.CharField(max_length=255) description = forms.CharField(widget=forms.Textarea) code = forms.CharField(widget=forms.Textarea) tags = forms.CharField(max_length=255)
再次提醒,你可以借助修改字段所使用的组建来改变它的表现形式。Django的模型系统使用了两个不同的字段——CharField和TextField——来表现不同大小的基于文本的字段(必须如此,因为它们在底层数据库列上是工作于不同的数据类型的),表单系统则只有一个CharField。为了在最终的HTML文件中将其转换为<textarea>,你需要简单的将其组建修改为Textarea,以同样的方式,你在用户注册表单的例子中使用了PasswordInput组建。
这就解决了除了语言之外的其他字段,这个需要突然看起来需要那么一点点技巧。你像要做的是显示一个下拉列表(一个HTML元素<select>)其中含有可用语言及校验规则,使用户可以从中选择。但是到目前为止你见过的字段类型没有适合处理这个问题的,所以需要求助于一些新的东西。
一个可以处理这个问题的字段类型叫做ChoiceField。它需要一个选项列表(和模型字段中接受的选项是同样的格式——例如,你在weblog的Entry模型中的status字段已经见过)并确保提交的值是其中之一。但是将其设置妥当以使每次使用时表单查询语言集合(以防管理员像系统中添加了新的语言)这就需要在__init__()方法中做些hacking。像这样表现一个模型中的关联是一项极为常见的情况,因此你会希望有一种简单的方法来处理它。
最终证明,Django还是提供了一个简单的解决:一个叫做ModelChoiceField的特殊字段。一个普通的ChoiceField字段只需要一个简单的选项列表,一个ModelChoiceField字段需要一个Django QuerySet并从查询的结果(每次都执行最新的)中动态的生成它的选项。为了使用它,需要在文件头部导入模型的语句中加上Language 模型:
from cab.models import Snippet,Laabguage
然后简单的编写:
language = ModelChoiceField(queryset = Language.objetcs.all())
对这个表单来说,你不需要在字段本身提供的校验之外的任何特殊校验,所以你可以直接编写save()方法:
def save(self): snippet = Snippet(title = self.cleaned_data['title'], description = self.cleaned_data['description'], code = self.cleand_data['code'], tags = self.cleand_data['tags'], author = self.author, language = self.cleand_data['language']) snippet.save() return snippet
因为在Django中在一个步骤中创建并保存一个对象是一种常规做法,实际上你可以用这种更为简便的做法。Django提供的默认管理器类包含了一个叫做create()的方法,可以创建,保存并返回一个新的对象。使用它,你的save()方法就简化到了只需两行:
1 def save(self): 2 return Snippets.objects.create(title=self.cleaned_data['title'],description=self.cleaned_data['description'],code=self.cleaned_data['code'],tags=self.cleaned_data['tags'],author=self.author,language=self.cleaned_data['language']
现在,你的AddSnippetForm就完成了:
1 from django import forms 2 from cab.models import Snippet, Language 3 class AddSnippetForm(forms.Form): 4 def __init__(self, author, *args, **kwargs): 5 super(AddSnippetForm, self).__init__(*args, **kwargs): 6 self.author = author 7 title = forms.CharField(max_length=255) 8 description = forms.CharField(widget=forms.Textarea()) 9 code = forms.CharField(widget=forms.Textarea()) 10 tags = forms.CharField(max_length=255) 11 language = forms.ModelChoiceField(queryset=Language.objects.all()) 12 def save(self): 13 return Snippet.objects.create(title=self.cleaned_data['title'], 14 description=self.cleaned_data['description'], 15 code=self.cleaned_data['code'], 16 tags=self.cleaned_data['tags'], 17 author=self.author, 18 language=self.cleaned_data['language'])
9.2.1 编写视图来处理表单
现在你可以编写一个简短的名为add_snippet的视图来处理后续任务。在cab/views目录下,创建一个snippets.py文件,然后加入如下代码:
from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from cab.forms import AddSnippetForm def add_snippet(request): if request.method== 'POST' form = AddSnippetForm(author=request.user,data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = AddSnippetForm(author = request.user) return render_to_response('cab/add_snippet.html',{'form':form})
这段代码将实例化表单,校验数据,保存新的Snippet,然后返回一个重定向到该段snippet的详细视图。(再次强调,总是在成功的POST之后重定向。)
开始这个视图函数看起来还不错,但是这里有一个潜在的问题。你引用了request.user,即当前登录的用户(Django在当认证系统被正确的激活后会自动的设置这个值)。但是如果填写这个表单的人没有登录会发生什么呢?
答案是,你的数据不会被真正的校验。当当前用户没有登录,request.user是一个表示匿名用户的“假”对象,它无法被用做一个snippet的author字段的值。因此你需要以某种方式来确认只有登录的用户才能填写这个表单。
幸运的是,Django提供了一个简单的方式处理这个问题,使用一个认证系统中的称为login_required的装饰器。
你可以简单的导入并应用于你的视图函数,没有登录的用户将会被重定向到登录页面:
from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from django.contrib.auth.decorators import login_required from cab.forms import AddSnippetFrom def add_snippet(request): if request.method == 'POST': form = AddSnippetForm(author=request.user,data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = AddSnippetForm(author = request.user) return render_to_response('cab/add_snippet.html', {'form':form}) add_snippet = login_required(add_snippet)
#admonition:设置登录/登出视图
Django的认证系统,内置于django.contrib.auth,包括了认证并登入用户的视图函数及表单。只要是在你自己的机器上对一个应用进行测试,那么你可以通过django管理界面登录,然后访问任何你以login_required标记的视图。但是在公开部署时,你会需要为一般用户设置公开界面的登入/登出视图。
了解如何使用内置的认证视图,参考Django认证系统在线文档http://docs.djangoproject.com/en/dev/topics/auth/.
9.2.2 编写模版处理add_snippet视图
现在,你可以编写cab/add_snippet.html模版如下:
<html> <head> <title>Add a snippet</title> </head> <body> <h1>Add a snippet</h1> <p> Use the form below to submit your snippet;all fields are required.</p> <form method ="post" action=""> <p>{% if form.title.errors %} <span class="error"> {{form.title.errors|join:", "}} </span> {% endif %}</p>
<p><label for="id_title">Title:</label>
{{ form.title }}</p>
<p>{% if form.language.errors %}
<span class="error">
{{ form.language.errors|join:", " }}
</span>
{% endif %}</p>
9.3 从模型定义自动产生表单
尽管Django的表单系统使你能较为简洁的方式编写和使用这个表单,但是这还不是最理想的解决方案。设置一个表单来添加和编辑一个模型的实例是极为常见的事情,一遍又一遍的编写这些程式化的表单会是件及其恼火的事情(特别是你已经在定义模型类的时候将多数甚至全部相关信息都设置了一遍的情况下)。
幸运的是,有一个方法可以戏剧性的减少你编写的代码的数量。为提供不需要太多自定义行文的表单,Django提供了一个称为ModelForm的类,可以从模型定义自动的产生一个适度自定义的表单,包含了所有相关字段及必要的save()方法。它的最基础的工作方式如下:
from django.forms import ModelForm from cab.models import Snippet class SnippetForm(ModelForm): class Meta: model = Snippet
子类化ModelForm并提供一个内部Meta类指定一个模型将自动的从制定模型中提取字段来设置这个新的SnippetForm类。并且,ModelForm会自动的忽略在模型中定义为editable=False的字段,因此类似HTML版本的描述字段将不会在表单中显示。这里唯一欠缺的东西其实是author这个字段将会显示出来。幸运的是,ModelForm支持一些自定义,包括一个要排除在表单之外的字段列表,因此你可以简单的修改SnippetFrom的定义如下:
class SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author']
这会将author字段排除在外。现在,你可以简单的删除cab/forms.py并像这样重写cab/views/snippets.py:
from django.http import HttpResponseRedirect from django.forms import ModelForm from django.shortcuts import render_to_response from django.contrib.auth.decoartors import login_required from cab.models import Snippet class SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author'] def add_snippet(request): if request.method == 'POST': form = SnippetForm(data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = SnippetForm() return render_to_response('cab/add_snippet.html', {'form':form}) add_snippet = login_required(add_snippet)
当然,这不是十分正确。Snippets需要填充一个作者(author),但是你已经将这个字段排除出表单之外了。你可以回过头去定义一个自定义的__init__()方法,并传递一个request.user,但是ModelForm有一个更富技巧的方式。你可以用ModelForm来创建一个Snippet对象并返回它,但并不保存;你通过向save()方法传递一个额外的参数——commit=False——来做到这一点。当这样做的时候,save()还是会返回一个新的Snippet对象,但是它不会被保存到数据库中去。这就给了你自由的添加user,并手动的将这个新的Snippet对象插入到数据库的机会:
from django.http import HttpResponseRedirect from django.forms import ModelForm from django.shortcuts import render_to_response from django.contrib.auth.decorators import login_required from cab.models import Snippet class SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author'] def add_snippet(request): if request.method == 'POST': form = SnippetForm(data = request.POST) if form.is_valid(): new_snippet = form.save(commit=False) new_snippet.author = request.user new_snippet.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = SnippetForm() return render_to_response('cab/add_snippet.html',{'form':form}) add_snippet = login_required(add_snippet)
#admonition:commit=False 和多对多关系(many to many relationships)
如果使用的模型中有多对多字段(在表单中会以一个名为ModelMultipleChoiceField的字段来表示),你需要在在ModelForm中的save()方法里使用commit=False的时候再多一个步骤。多对多关系在主对象被保存之前是不能被设置的(因为需要知道它在数据库中的id)。因此在有多对多关系的表单中使用commit=False的时候,表单将会有一个名为save_m2m()的方法,为最终的多对多关系保存数据。你需要在保存了主对象之后手动的调用这个方法(不带参数)。
现在,你可以打开cab/urls/snippets.py,然后添加一个新的导入语句:
from cab.views.snippets import add_snippet
以及一个新的URL模式:
url(r'^add/$',add_snippet,name='cab_snippet_add'),
9.4 简化显示表单的模版
之前列出的模版还是会继续正常的工作,因为表单的字段没有改变。但是,如果django提供一种在模版中显示表单的简单方式可以使你免除编写全部重复的HTML和字段错误检查的话就太好了。你已经避免了自行定义表单类的冗繁工作,为什么不排除在模版上的冗余工作呢?
为此,每个Django表单都有一些附加的方法来获知如何将表单渲染为不同类型的HTML:
as_url:将表单渲染为一系列HTML列表项(<li>标签),一个字段一项。
as_p:将表单渲染为一系列的段落(<p>标签),一个段落一项。
as_table:将表单渲染为一个HTML表格,一个字段一个<tr>标签。
那么,举例来说,你可以将目前你所做的模版(以HTML段落元素分隔)替换为如下一句:
{{ form.as_p }}
但是在使用这些方法的时候需要注意:
- 它们都不输出闭合的<form>和</form>标签,因为表单“不知道”你计划在那里提交或者如何提交表单。你需要自行添加这些标签,并附带合适的action 及 method 属性。
- 它们都不输出用以提交表单的按钮。再说一次,表单不知道你像要如何提交,因此你需要自行提供一个或者多个<input type="submit">标签。
- as_url()方法不输出环绕它的<ul>和</ul>标签,as_table()方法也不输出环绕它的<table>和</table>标签。这是为了以防你想要添加更多自己的HTML内容(在表单显示中是十分普遍的需要),所以要记住自行填充这些标签。
- 最后,这些方法不能被简单的自定义。当你只需要基本的表单显示(特别是为了测试一个应用需要的快速原型)的时候,它们是十分简便的,但是如果你需要自定义表单表现形式的时候,可能需要返回头去手动的为表单建立模版。
9.5 编辑代码片段
现在你有了一个可以让用户提交他们的代码段的系统,但是如果有人想要返回去编辑他们提交的代码段该怎么做呢?不可避免的总是会有某人意外的提交了含有错误的代码段,或者找到了解决实际问题的更好方法。最好是允许用户在这些情况下能够编辑他们自己的代码段,因此我们通过一个名为edite_snippet的视图来设置代码段的编辑。
幸运的是,这十分之简单。ModelForm同样知道改如何编辑一个已经存在的对象,这就解决了大部分的苦活累活。你所要做的,就是处理两件事:
- 计算哪个Snippet对象要被编辑
- 保证要编辑这个Snippet的用户是代码段最初的作者
处理第一个问题十分简单:你可以在URL中设置你的edite_snippet视图函数接收Snippet的id,然后在数据库中查询它。然后你可以比较Snippet的author字段同当前登录用户的一致性来保证它们的匹配。所以,我么开始再添加两个导入语句在cab/views/snippets.py中:
from django.shortcuts import get_object_or_404 from django.http import HttpResponseForbidden
HttpResponseForbidden 类表示一个状态码为403的HTTP响应,它表示用户没有他们想要做的事情的权限。你需要在用户尝试编辑不是由他们提交的代码段的时候使用它。
下面是edite_snippet视图:
def edit_snippet(request,snippet_id): snippet = get_object_or_404(Snippet,pk=snippet_id) if request.user.id != snippet.author.id: return HttpResponseForbidden() if request.method == 'POST': form = SnippetForm(instance=snippet,data=request.POST) if form.is_valid(): snippet = form.save() return HttpResponseRedirect(snippet.get_absolute_url()) else: form = SnippetForm(instance=snippet) return render_to_response('cab/edit_snippet.html'.{'form':form}) edit_snippet = login_required(edit_snippet)
为了告诉ModelForm子类你要编辑一个已经存在的对象,你简单的将这个对象作为instance参数传递;表单会处理剩下的事情。注意因为Snippet已经有了一个作者,且这个值是不要改变的,你不需要使用commit=False然后手动的保存Snippet。表单不会修改这个值,所以你可以简单的让它来保存即可。
现在你可以为视图函数添加一个URL模式。首先修改cab/urls/snippets.py中的导入语句来导入这个视图函数:
from cab.views.snippet import add_snippet,edit_snippet
然后添加URL模式:
url(r'^edit/(?P<snippet_id>\d+)/$',edit_snippet,name='cab_snippet_edit'),
因为edit_snippet视图和add_snippet视图的表单具有同样的字段,你可以将模版化简成一个,然后通过传递一个指示添加还是编辑的的变量(这样诸如页面标题之类的元素可以根据它来改变)。因此我们来修改add_snippet视图的最后一行来传递一个叫做add的额外变量,将其值设置为True,然后将模版名改为cab/snippet_form.html:
return render_to_response('cab/snippet_form.html',{'form':form,'add':True})
然后修改edit_snippet视图中同样的一行来使用cab/snippet_form.html,并将add变量设置为False:
return render_to_response('cab/snippet_form.html'.{'form':form,'add':False})
现在可以简化为一个模版——cab/snippet_form.html——内容如下:
<html> <head> <title>{%if add%}Add a {% else %}Edit your{% endif %} snippet</title> </head> <body> <h1>{% if add %}Add a {% else %}Edit your{% endif %} snippet</h1> <p>Use the form below to {% if add %}add{% else %}edit {% endif %} your snippet;all fields are required</p> <form action="" method="post"> {{ form.as_p }} <p><input type="submit" value="Send" ></p> </form> </body> </html>
现在,使用户能够添加和编辑他们的代码段的表单、视图、模版都一应俱全了。下面就是完成后的cab/views/snippets.py 文件:
from django.http import HttpResponseForbidden,HttpResponseRedirect from django.forms import ModelForm from django.shortcuts import get_object_or_404,render_to_response from django.contrib.auth.decorators import login_required from cab.models import Snippet class SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author'] def add_snippet(request): if request.method == 'POST': form = SnippetForm(data=request.POST) if form.is_valid(): new_snippet = form.save(commit=False) new_snipet.author = request.user new_snippet.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form=SnippetForm() return render_to_response('cab/snippet_form.html',{'form':form,'add':True}) add_snippet=login_required(add_snippet) def edit_snippet(request,snippet_id): snippet = get_object_or_404(Snipet,pk=snippet_id) if request.user.id != snippet.author.id: return HttpResponseForbidden() if request.method == 'POST': form = SnippetForm(instance=snippet,data=request.POST) if form.is_valid(): snippet = form.save() return HttpResponseReidrect(snippet.get_absolute_url()) else: form = SnippetFrom(instance=snippet) return render_to_response('cab/snippet_form.html',{'form':form,'add':False}) edit_snippet = login_required(edit_snippet)
9.2 展望
在继续之前,我建议花点时间来熟悉一下Django的表单系统。尽管目前为止你对基本内容已经有了很好掌握,你可能还想多花点时间来看看django.forms包的完整文档(在线地址 http://docs/djangoproject.com/en/dev/topics/forms/)对全部特性有一个全面了解(包括全部字段类型和组件,及自定义表单外观的高级技巧)。
当你准备好回来之后,下一章将会对这个应用进行最后的扫尾工作,包括添加书签和评分系统,包括最受欢迎的代码段列表,以及为确定用户是否标记或评分而做的必要的模版扩展。