表单
这一章节中,我们将学习如何通过web表单获取数据。django拥有一些巧妙的表单处理功能,通过一些相当直观的方法从用户中收集信息并通过模型保存到数据库中。根据django关于表单的官方文档,表单处理通过如下功能:
- 自动生成表单控件展现HTML表单(比如文本框或日期选择器);
- 检查提交的数据是否违反了一系列验证规则;
- 一旦出现错误重新展现一个表单;
- 将提交的表单数据转换成相关的python数据类型。
使用django表单功能的优点之一就是创建HTML表单时能为你节省很多时间和麻烦。
基本工作流
创建表单并处理用户输入的基本步骤如下所示:
- 如果你还没有在django应用目录里创建forms.py文件的话,先创建一个用来存储与表单相关的类。
- 为每个你希望要表示成表单的模型创建一个ModelForm类。
- 按照你的需求设计表单。
创建一个视图处理表单
- 包括显示表单
- 保存表单数据
- 当用户在表单中提交不正确的数据(或未提交数据)时,标记会发生的错误。
创建一个模板显示表单。
- 增加一个url模式映射新视图
这个工作流比之前的要复杂点,视图也比我们之前创建的视图要复杂一点,但是如果你多做几遍,你也能搞清楚每个模块是如何组合工作的。
Page和Category表单
这里我们将会执行一些必须的架构,让用户通过表单添加页面和分类到数据库中。
首先在rango应用的目录里创建一个叫forms.py的文件。尽管这一步不是必须的(你也可以把form类定义在models.py),但是这样做会让你的代码更加紧凑并容易运行。
创建ModelForm类
在forms.py里,我们将创建几个类并继承django的ModelForm类。实际上,ModelForm类就是一个帮助类,它允许你从一个已存在的模型创建一个django的表单。目前我们已经在rango中创建了两个模型(Category和Page),我们现在就创建两个ModelForms。
在rango/forms.py文件里,添加如下代码:
from django import forms
from rango.models import Page, Category
class CategoryForm(forms.ModelForm):
name = forms.CharField(max_length=128,
help_text="Please enter the category name.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
likes = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
slug = forms.CharField(widget=forms.HiddenInput(), required=False)
# An inline class to provide additional information on the form.
class Meta:
# Provide an association between the ModelForm and a model
model = Category
fields = ('name',)
class PageForm(forms.ModelForm):
title = forms.CharField(max_length=128,
help_text="Please enter the title of the page.")
url = forms.URLField(max_length=200,
help_text="Please enter the URL of the page.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
class Meta:
# Provide an association between the ModelForm and a model
model = Page
# What fields do we want to include in our form?
# This way we don't need every field in the model present.
# Some fields may allow NULL values, so we may not want to include them.
# Here, we are hiding the foreign key.
# we can either exclude the category field from the form,
exclude = ('category',)
# or specify the fields to include (i.e. not include the category field)
#fields = ('title', 'url', 'views')
在form类定义中,通过Meta中field
项指定表单可以包括的项,通过Meta中的exclude
指定哪些项排除在外。
django提供了许多的方法根据我们的行为创建个性化的表单。上面的代码例子中,我们指定了我们希望每一项所要展现的控件。举个例子,在PageForm类中,我们为title项定义了一个forms.CharField
,为url项定义了一个forms.URLField
。这两项都是为用户提供文本输入的。注意一下定义项中给出的max_length
参数,这里我们指定的长度必须要和在定义数据模型时指定的最大长度一致。回到模型章节查看一下,或者看一下models.py文件。
你还得注意的是在每个表单中我们views和likes项定义了IntegerField类型。注意我们通过配置参数widget=forms.HiddenInput()
设置了控件隐藏,并用initial=0
设置了值为0。这是设置默认值为0的一种方法。因为这一项被隐藏,用户就不能输入值了。
但是,在PageForm中,尽管我们隐藏了这些项,但是我们还是要在表单中包含这些项。如果在fields
我们没有包括views
,表单就不会包括这一项(尽管指定了),所以这个表单的隐藏项就不能返回一个0的值。根据模型的配置,这样就会产生一个错误。如果在模型定义时,我们用default=0
为这些项指定默认值,那我们就可以根据模型定义的默认值自动填充隐藏项的值。这样就避免了not null
错误。在这个例子中是不需要有隐藏项的。在CategoryForm类中我们也包括了slug项,并设置了widget=forms.HiddenInput()
,但是实际上表单不需要这一项,所以也不用指定一个初始值或默认值。因为我们的模型是通过调用save()来填充这一项。因此,当你定义模型和表单的时候,你得注意要保证表单正确的包含和传递填充模型所需要的数据。
除了CharField和IntegerField控件以外,还有很多其它的控件可用。比如说,django提供了EmailField
控件(提供邮件地址输入),ChoiceField
(单选输出按钮),DateField
(日期时间输入)。还有很多其它你可以用的控件,都提供了为你检查用户输入是否有错的功能(比如输入值是否提供了合法的整型数据)。
或许从类ModelForm中继承的类最重要的一方面就是要定义我们需要给表单提供什么模型。我们通过python中的元类来解决这个问题。在元类中的model
属性定义你想要使用的模型。举个例子,我们的CategoryForm类中引用了Category模型。这是用django使用指定模型的镜像创建一个表单的关键步骤。它还会帮助你统计在表单中保存和展现数据时产生的错误。
我们也使用了元类中的fields
元组去指定表单中包括的项。使用项名称元组指定你希望保存的项。
关于表单的更多信息
查阅django官方文档,获取更多的关于不同控件和如何个性化表单的信息。
创建一个增加分类的视图
在CategoryForm类的定义中,我们已经创建了一个新视图展现表单并处理提交上来的表单数据。添加如下代码到rango/views.py。
#Add this import at the top of the file
from rango.forms import CategoryForm
...
def add_category(request):
form = CategoryForm()
# A HTTP POST?
if request.method == 'POST':
form = CategoryForm(request.POST)
# Have we been provided with a valid form?
if form.is_valid():
# Save the new category to the database.
form.save(commit=True)
# Now that the category is saved
# We could give a confirmation message
# But since the most recent category added is on the index page
# Then we can direct the user back to the index page.
return index(request)
else:
# The supplied form contained errors -
# just print them to the terminal.
print form.errors
# Will handle the bad form, new form, or no form supplied cases.
# Render the form with error messages (if any).
return render(request, 'rango/add_category.html', {'form': form})
新添加的add_category()
视图函数添加了几个关键功能处理表单。首先我们创建了CategoryForm()
类,然后我们检查HTTP的请求是否是POST请求,也就是用户是否通过表单提交数据。然后我们用相同的URL来处理POST请求。add_category()
视图函数可以处理以下三种场景:
- 展示一个新的空的增加分类的表单;
- 将用户提交的表单数据保存到相应的模型中并渲染到rango的主页上;
- 如果出现错误,会重新展示带错误的表单。
GET和POST
GET和POST是什么意思?HTTP请求有两种不同的类型。
- HTTP的get请求类型用来请求指定的资源。换句话说,我们使用http的GET获取特殊的资源,它可以是个页面,图像或是其它文件。
- 与get不同,POST请求是提交从客户端浏览器所要处理的数据。这类请求一般是当提交HTML表单内容时使用的。
- 最后HTTP的POST请求是以通过程序在服务器上创建一个新资源而结束(比如一个新的数据项)。这个新资源之后可以通过GET请求访问。
- 查阅w3schools page on GET vs. POST获取更多信息。
django表单处理机制通过POST请求处理从用户浏览器返回的数据。django机制不仅会将表单数据保存到相应的模型,还会自动的为每一个表单项生成错误信息(如果需要)。如果提交的数据不全,导致产生潜在的数据库完整问题,django不会存储这样的数据。比如说,category项中未填写值将会发生错误,因为这项不能为空。
你还要注意下在我们调用render()的那一行,我们引用了一个新的叫add_category.html的模板。这个模板会包含一些与表单相关的模板代码和HTML。
创建Add Category 模板
创建文件templates/rango/add_category.html,并填写如下代码。
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Category</h1>
<div>
<form id="category_form" method="post" action="/rango/add_category/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Category" />
</form>
</div>
</body>
</html>
你可以看到在HTML页面的<body>
里我们加了<form>
元素。查看这个元素的属性,你会看到这个表单所有获取的数据都会作为HTTP的POST请求(method属性对大小写不敏感,你可以用post或POST,两个功能都一样)发给URL/rango/add_category/
。这个表单代码里,我们有两个for循环:
- 一个控制表单的隐藏项。
- 另一个控制表单的可见项。
这些可见项,就是用户在页面上能看见的项,由ModelForm里的Meta类中的fields属性控制的。这个循环生成HTML的表单元素标签。对于可见的表单项,我们也可以添加一些错误信息,可以在特殊的项中表现出来,也可以添加一些帮助信息,用来解释用户需要填写的内容。
隐藏项
有些表单项需要展现出来,而有些需要隐藏起来,因为HTTP是无状态协议。在不同的HTTP请求中不能维持一样的状态,这样就使web应用的某些部分难以实现。为了克服这个限制,我们可以创建隐藏的HTML表单项,让web应用传递重要信息到客户端的HTML表单中(页面上被隐藏),然后当用户提交表单的时候,再次发回到原始服务器。
跨站点请求伪造令牌
你应该注意到这个代码段{% csrf_token %}
。这是一个跨站请求伪造令牌,用来保护在随后提交表单时启动的HTTP的POST请求。django框架需要显式列出CSRF令牌。如果在表单中忘了加上一个CSRF令牌,提交表单时就有可能报错。查阅django官方文档获取更多信息。
映射到Add Category视图
现在我们需要将add_category()视图函数映射到URL上,。在模板中,我们在表单的action属性里使用了/rango/add_category
,现在我们需要创建一个从URL到视图的映射了。在rango/urls.py中,按照如下代码修改URL模式。
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'about/$', views.about, name='about'),
url(r'^add_category/$', views.add_category, name='add_category'),
url(r'^category/(?P<category_name_slug>[\w\-]+)/$',
views.show_category, name='show_category'),
]
在这个例子里,顺序不是很重要。但是你最好在django官方文档上看一下django是如何处理一个请求的。增加一个分类的URL是/rango/add_category/
。
修改index页面视图
最后一步让我们在index主页上添加一个链接,通过这个链接可以让我们直接的添加分类。编辑模板rango/index.html,如下所示,在about链接的div元素里添加一个超链接。
<a href="/rango/add_category/">Add a New Category</a><br />
示例
现在让我们尝试一下。重启你的django开发服务器,将你的服务器指向http://127.0.0.1:8000/rango/
。使用这个新链接跳转到添加分类的页面,然后试着添加一个分类。下图展示了添加分类和主页的截图。
分类丢失
如果你添加了几个分类,在index主页上可能不会出现。因为在主页上我们只显示5个分类。如果你登录到管理界面,你应该能看到你输入的所有分类。
另外一个确认分类是否添加成功的方法是修改rango/views.py的add_category()方法,将form.save(commit=True)
这一行修改为cat = form.save(commit=True)
。这样你就有了一个由表单创建的分类对象实例的引用,然后你就可以将这个分类打印到控制台上来(比如print cat.slug
)。
简洁的表单
回顾一下我们的Page模型里有一个url属性,被设置成URLField类型。在相对应的HTML表单里,django自然希望在文本框里填入的url字段的格式是正确的。但是用户会觉得输入像http://www.url.com
这样的文本很麻烦,确实,有些用户甚至都有可能不知道正确的URL格式是什么。
URL检查
现在的浏览器都会检查URL,确保其正确格式化。所以下面的例子仅在老的浏览器才有效。但是这个例子确实为你展示了在你把数据存入数据库之前如何整理这些数据。如果你没有旧的浏览器做测试,你可以将URLField修改为CharField。渲染后的HTML就不会在让浏览器根据你的行为进行检查了,你的代码就会被执行了。
在某些场景中,用户有可能输入了一些不完全正确的数据。我们可以重写ModelForm类中定义的clean()方法。这个方法在将表单数据保存到新的模型实例之前被调用,因此给我们提供了一个逻辑空间插入代码用来验证甚至是修复用户输入的表单数据,我们可以检查用户输入的url值是否以http://
开头,如果不是,我们可以为用户的输入预添加http://
。
class PageForm(forms.ModelForm):
...
def clean(self):
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
# If url is not empty and doesn't start with 'http://',
# then prepend 'http://'.
if url and not url.startswith('http://'):
url = 'http://' + url
cleaned_data['url'] = url
return cleaned_data
在clean()方法里,我们可以看到一个简单的处理示例,你可以把你自己的django表单处理代码复制到这里。
- 从ModelForm的字典属性clean_data中获取表单数据。
- 你希望检查的表单项数据可以从cleaned_data字典里获取,使用字典对象提供的
.get()
方法获取表单数据。如果用户并没有在表单项中输入一个值,那么在cleaned_data字典里这项就不存在。这个实例里,get()方法会返回一个None值,而不会触发一个KeyError异常。这会让你的代码看起来更简洁。 - 对于你要处理的每个表单项,你都需要检查一下值是否取到,如果输入值了,检查值是什么。如果值不是你所期望的,你也可以添加一些逻辑代码修复这个问题后再把这个值重新赋值给cleaned_data字典。
- 在clean()方法最后必须要返回一个clean_data字典的引用,否则修改将不起作用。
这个简单的例子向我们展示了如何将表单传来的数据进行加工后再存储起来。这是相当便利的,特别是当某些特殊的项需要有默认的值,或者表单里的数据缺失的时候,我们就需要处理这样的数据问题了。
clean方法重写
重写django框架中实现的方法为你提供了一个优雅的方式让你的应用添加额外的功能。有许多方法都可以根据你自己的需求进行安全的覆盖重写,就像上面ModelForm类里的clean()方法。查看django官方文档获取更多例子来了解如何重新默认功能,插入自己想实现的功能代码。
练习
既然这章你已经学完了,看一下下面的问题并考虑如何解决它们:
- 如果在添加分类表单中你并没有输入一个分类名称,将会发生什么?
- 当你试着添加一个已经存在的分类时,会发生什么?
- 当你访问一个不存在的分类时会发生什么?在下面的提示中可以找到解决这个问题的方法。
- 在上面我们实现ModelForm类的那一节,我们又一次的定义了max_length值,而这个值我们之前在models章节已经定义过了。这样就产生代码重复了,如何重构你的代码让max_length值不在重复?
- 学习django官方文档第四部分,巩固一下你所学习的内容。
- 最后让用户在每一个分类中添加页面,下面有一些样例代码和提示。
创建一个Add pages视图,模板和URL映射
下一个环节就是让用户在给出的分类里添加页面,如下所示,添加页面的工作流和添加分类的工作流类似。
- 创建一个新视图add_page(),
- 创建一个新模板rango/add_page.html,
- 添加一个URL映射,
- 修改分类页面,在分类的页面里添加一个新增page的链接。
下面是add_page()视图函数的代码:
from rango.forms import PageForm
def add_page(request, category_name_slug):
try:
category = Category.objects.get(slug=category_name_slug)
except Category.DoesNotExist:
category = None
form = PageForm()
if request.method == 'POST':
form = PageForm(request.POST)
if form.is_valid():
if category:
page = form.save(commit=False)
page.category = category
page.views = 0
page.save()
return show_category(request, category_name_slug)
else:
print form.errors
context_dict = {'form':form, 'category': category}
return render(request, 'rango/add_page.html', context_dict)
提示
为了帮助你做好上面的练习,下面的提示信息或许会对你有用。
- 在add_page.html模板中,你可以通过
{{ category.slug }}
访问slug,因为视图已经通过上下文字典将category对象传递给模板了。 - 确保只有当请求的分类存在才能显示链接,无论页面有没有,即在模板中通过以下代码进行检查
{% if cat %} .... {% else %} A category by this name does not exist {% endif %}
。 - 在category.html模板里,添加一个超链接
<a href="/rango/category/{{category.slug}}/add_page/">Add Page</a> <br/>
。 - 确保在add_page.html页面中,表单的post数据提交给
/rango/category/{{ category.slug }}/add_page/
。 - 更新rango/urls.py,创建一个URL映射(
/rango/category/<category_name_slug>/add_page/
)处理上面的链接。 - 避免max_length参数重复可以用Category类的另外一个属性,这个属性可以用来存储max_length值,然后在需要的时候引用。
如果你遇到问题,你也可以查阅下我们在GitHub上的代码。