HTML表单是网站交互性的经典方式,本章将简单介绍Web表单的基本概念和如何用Django对用户提交的表单数据进行处理。在Web开发中除非你计划构建的网站和应用只是发布内容而不接受访问者的输入,否则你将需要理解并使用表单。Django提供广泛的工具和库来帮助你构建表单来接收网站访问者的输入,然后处理以及响应输入。
一、HTML表单
在HTML中,表单的作用是收集元素中的内容。多数情况下被用到的表单元素是输入元素(<input>),输入类型是由类型属性(type)定义的,表单使用表单标签<form>...</form>来设置。中间可以由访问者添加类似于文本,选择,或者一些控制模块等等,然后这些内容将会被送到服务端。
HTML表单元素
表单标签<input>允许用户在表单中输入内容,比如:文本域(textarea)、下拉列表、单选框(radio-buttons)、复选框(checkboxes)等等,非常简单而且内建于HTML本身,其它的表单会复杂些;例如弹出一个日期选择对话框的界面、允许你移动滚动条的界面、使用JavaScript和CSS以及HTML表单<input>标签来实现操作控制的界面。
常用<input type=””>元素中type属性有:
属性 | 描述 |
---|---|
text | 定义常规文本输入。 |
radio | 定义单选按钮输入(多选一)。 |
password | 密码字段字符不会明文显示,而是以星号或圆点替代。 |
submit | 定义提交按钮(提交表单) |
Checkboxes | 定义复选框,用户需要从若干给定的选择中选取一个或若干选项。 |
….. |
与<input>元素一样,一个表单必须指定两样东西:
- 目的地:响应用户输入数据的URL。
- 方式:发送数据的HTTP方法。
下面是<form>属性的列表:
属性 | 描述 |
---|---|
accept-charset | 规定在被提交表单中使用的字符集(默认:页面字符集)。 |
action | 规定向何处提交表单的地址(URL)(提交页面)。 |
autocomplete | 规定浏览器应该自动完成表单(默认:开启)。 |
enctype | 规定被提交数据的编码(默认:url-encoded)。 |
method | 规定在提交表单时所用的HTTP方法(默认:GET)。 |
name | 规定识别表单的名称(对于DOM使用:document.forms.name)。 |
novalidate | 规定浏览器不验证表单。 |
target | 规定action属性中地址的目标(默认:_self)。 |
PS:关于HTML和表单部分直接去w3cschool网站学习。
二、GET和POST
处理表单时候只会用到GET和POST方法。
Django的登录表单使用POST方法,在这个方法中浏览器组合表单数据、对它们进行编码以用于传输、将它们发送到服务器然后接收它的响应。相反,GET组合提交的数据为一个字符串,然后使用它来生成一个URL。这个URL将包含数据发送的地址以及数据的键和值。如果你在Django文档中做一次搜索,你会立即看到这点,此时将生成一个https://docs.djangoproject.com/search/?q=forms&release=1形式的URL。
GET和POST用于不同的目的。用于改变系统状态的请求,例如,给数据库带来变化的请求应该使用POST;GET只应该用于不会影响系统状态的请求。
GET还不适合密码表单,因为密码将出现在URL中,以及浏览器的历史和服务器的日志中,而且都是以普通的文本格式。它还不适合数据量大的表单和二进制数据。例如一张图片,使用GET请求作为管理站点的表单具有安全隐患:攻击者很容易模拟表单请求来取得系统的敏感数据。POST,如果与其它的保护措施结合将对访问提供更多的控制,例如Django的CSRF保护。
另一个方面,GET适合网页搜索这样的表单,因为这种表示一个GET请求的URL可以很容易地作为书签、分享和重新提交。
三、Django中的表单
处理表单是一件很复杂的事情,考虑一下Django的Admin站点,不同类型的大量数据项需要在一个表单中准备好、渲染成HTML、使用一个方便的界面编辑、返回给服务器、验证并清除,然后保存或者向后继续处理。
Django的表单功能可以简化并自动化大部分这些工作,而且还可以比大部分程序员自己所编写的代码更安全。
Django会处理表单工作中的三个显著不同的部分:
- 准备数据、重构数据,以便下一步提交。
- 为数据创建HTML表单。
- 接收并处理客户端提交的表单和数据。
可以手工编写代码来实现,但是Django可以帮你完成所有这些工作。
我们已经简短讲述HTML表单,但是HTML的<form>只是其机制的一部分。在一个Web应用中,”表单”可能指HTML的<form>、或者生成它的Django的Form、或者提交时发送的结构化数据、或者这些部分的总和。
四、Django的Form类
表单系统的核心部分是Django的Form类,Django的模型描述一个对象的逻辑结构、行为以及展现给我们的方式,与此类似,Form类描述一个表单并决定它如何工作和展现。
就像模型类的属性映射到数据库的字段一样,表单类的字段会映射到HTML的<input>表单的元素。(ModelForm通过一个Form映射模型类的字段到HTML表单的<input>元素;Django的Admin站点就是基于这个)。
表单的字段本身也是类,它们管理表单的数据并在表单提交时进行验证。DateField和FileField处理的数据类型差别很大,必须完成不同的事情。
表单字段在浏览器中呈现给用户的是一个HTM 的“widget” —— 用户界面的一个片段。每个字段类型都有一个合适的默认Widget类,需要时可以覆盖。
五、构建一个搜索功能
5.1 使用GET方法
利用HTML表单,使用GET方法做一个简单的搜索跳转功能。访问http://10.10.0.109:8000/polls/search-form/得到如下页面:
在输入框中输入一些内容,然后点击搜索按钮跳转到如下页面:
就是返回搜索内容,会发现URL地址会改变,利用form action属性简单实现。
下面就是实现这个功能的代码了,很简单,首先增加一个支持表单的search.html文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# ./polls/templates/polls/search.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Python Django</title> </head> <body> <form action="/polls/search" method="get"> 输入你要搜索的内容: <input type="text" name="q"> <input type="submit" value="搜索"> </form> </body> </html> |
给views.py文件增加两个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# ./polls/views.py # -*- coding: utf-8 -*- from django.shortcuts import render from django.http import HttpResponse from django.http import HttpResponseRedirect def search_form(request): return render(request, 'polls/search.html') def search(request): request.encoding='utf-8' if 'q' in request.GET: message = '你搜索的内容为: ' + request.GET['q'] else: message = '你提交了空表单' return HttpResponse(message) |
urls.py规则修改为如下形式:
1 2 3 4 5 6 7 8 9 |
# ./polls/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^search-form/', views.search_form, name='search_form'), url(r'^search/', views.search, name='search'), ] |
注意,入口urls.py文件如下配置:
1 2 3 4 5 6 7 8 9 |
# ./project_name/urls.py from django.conf.urls import url from django.contrib import admin from django.conf.urls import include urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^polls/', include('polls.urls')) ] |
执行过程口述一下,如下步骤:
1. 访问/polls/search-form/首先会经过入口urls.py文件匹配中url(r’^polls/’, include(‘polls.urls’))匹配到,转给App下的urls文件处理。
2. 应用polls下的urls.py文件接收到之后经过url(r’^search-form/’, views.search_form, name=’search_form’)匹配到,然后执行views.search_form函数。
3. 执行views.search_form函数时就会返回search.html文件,至此上面第一张图出来了。
4. 输入搜索内容,点击搜索按钮提交时,会交由form action属性处理跳转到/polls/search路径,并且传一个GET方法。
5. 然后又会经过入口urls.py文件匹配中url(r’^polls/’, include(‘polls.urls’))匹配到,转给App下的urls文件处理。应用polls下的urls.py文件接收到之后经过url(r’^search/’, views.search, name=’search’)匹配到,然后执行views.search函数。然后search函数匹配GET方法,获取到参数值,最后通过HttpResponse方法把信息返回到页面,至此上面第二张图出来了,整个处理过程也完事了。
5.2 使用POST方法
上面我们使用了GET方法,视图显示和请求处理分成两个函数处理。但提交数据时更常用POST方法,我们下面使用该方法,并用一个URL和处理函数,同时显示视图和处理请求。
效果如下,我们访问http://10.10.0.109:8000/polls/search-post/得到如下页面:
输入你要搜索的内容,然后点击搜索按钮,得到如下页面:
我们可以看到URL地址没有改变。
下面就是实现这个功能的代码了,很简单,首先增加一个支持表单的post.html文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# ./polls/templates/polls/post.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Python Django</title> </head> <body> <form action="/polls/search-post/" method="post"> {% csrf_token %} 输入你要搜索的内容: <input type="text" name="q"> <input type="submit" value="搜索"> </form> <p>{{ rlt }}</p> </body> </html> |
给views.py文件增加一个函数:
1 2 3 4 5 6 7 |
# ./polls/views.py def search_post(request): ctx = {} if request.POST: ctx['rlt'] = request.POST['q'] print(ctx) #看一下ctx的值是什么样子的; return render(request, "polls/post.html", ctx) |
urls.py增加一条规则,如下形式:
1 2 3 4 5 6 7 8 9 10 |
# ./polls/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^search-form/', views.search_form, name='search_form'), url(r'^search/', views.search, name='search'), url(r'^search-post/', views.search_post), ] |
其实这种方式只是测试玩玩,两次执行动作都是同一套流程,一模一样。只是由于第一次执行在post.html模板中{{ rlt }}变量没有取到值,所以我们没看见。第二遍执行时由于我们搜索框有值了,所以模板里面的{{ rlt }}变量取到了值就展示出来了,给我们展示出来的就是URL是同一个。
六、构建一个表单
假设你想在你的网站上创建一个简单的表单,以获得用户的名字。你需要类似这样的模板:
1 2 3 4 5 |
<form action="/your-name/" method="post"> <label for="your_name">Your name: </label> <input id="your_name" type="text" name="your_name" value="{{ current_name }}"> <input type="submit" value="OK"> </form> |
这告诉浏览器发送表单的数据到/your-name/这个URL,并使用POST方法。 它将显示一个标签为”Your name:”的文本字段,和一个”OK”按钮。如果模板上下文包含一个your_name变量,它将用于预填充current_name字段。
你将需要一个视图来渲染这个包含HTML表单的模板,并提供合适的current_name字段。当表单提交时,发往服务器的POST请求将包含表单数据。
现在你还需要一个对应/your-name/的视图,然后处理请求中的键/值对。
这是一个非常简单的表单,实际应用中,一个表单可能包含几十上百个字段,其中大部分需要预填充,而且我们预料到用户将来回编辑-提交几次才能完成操作。
即使在提交表单之前,我们也可能需要在浏览器中进行一些验证。我们可能想要使用更复杂的字段,这样可以让用户做一些事情,例如从日历中选择日期等等。这个时候,让Django来为我们完成大部分工作是很容易的。
6.1 创建一个表单
当我们已经想好了HTML表单应该呈现的样子后,在Django中就可以创建表单,如下:
1 2 3 4 5 |
# ./polls/forms.py from django import forms class NameForm(forms.Form): your_name = forms.CharField(label='Your name', max_length=3) |
定义一个Form类,只带有一个字段(your_name)。我们已经对这个字段使用一个友好的标签,当渲染时它将出现在<label>中(在这个例子中,即使我们省略它,我们指定的label还是会自动生成)。
字段允许的最大长度通过max_length定义。它完成两件事情。首先,它在HTML的<input>上放置一个maxlength=”3″ (这样浏览器将在第一时间阻止用户输入多于这个数目的字符)。它还意味着当Django收到浏览器发送过来的表单时,它将验证数据的长度。
Form的实例具有一个is_valid()方法,它为所有的字段运行验证的程序。当调用这个方法时,如果所有的字段都包含合法的数据,它将:
- 返回True。
- 将表单的数据放到cleaned_data属性中。
这个表单被模板渲染后,看上去应该像下面这样:
1 2 |
<label for="your_name">Your name: </label> <input id="your_name" type="text" name="your_name" maxlength="3"> |
6.2 创建视图
发送给Django网站的表单数据通过一个视图处理,一般和发布这个表单的是同一个视图,这允许我们重用一些相同的逻辑。
要操作一个通过URL发布的表单,我们要在视图中实例表单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from django.http import HttpResponse from django.http import HttpResponseRedirect from django.urls import reverse from .models import Choice, Question from .forms import NameForm def get_name(request): if request.method == 'POST': form = NameForm(request.POST) if form.is_valid(): dt = form.cleaned_data return HttpResponse(dt['your_name']) # return HttpResponseRedirect('/thanks/') else: form = NameForm() #第一次GET请求生成的form里面内容的格式; return render(request, 'polls/name.html', {'form': form}) |
如果访问视图的是一个GET请求,它将创建一个空的表单实例并将它放置到要渲染的模板的上下文中。这是我们在第一次访问该URL时预期发生的情况。
如果表单的提交使用POST请求,那么视图将再次创建一个表单实例并使用请求中的数据填充它:form = NameForm(request.POST)。这叫做”绑定数据至表单“(现在是绑定的形式)。
然后调用表单的is_valid()方法;如果它不为True,我们将带着这个表单返回到模板。这时表单不再为空(未绑定),所以HTML表单将用之前提交的数据填充,然后可以根据要求编辑并改正它。
如果is_valid()为True,接下来我们将能够在cleaned_data属性中找到所有合法的表单数据(字典)。接收到数据之后,就可以存入到数据库中,然后可以选择再次发送HTTP重定向给浏览器告诉它下一步的去向(我们这里测试就直接返回value)。我们应每次都给成功的POST请求做重定向,这就是web开发的最佳实践。因为POST成功后,直接重定向,不会造成重复向同一个页面POST数据(返回同一个页面时,但参数要求不同)。
6.3 创建模板
我们不需要在name.html模板中做很多工作,最简单的例子是:
1 2 3 4 5 6 |
# ./polls/templates/polls/name.html <form action="" method="post"> {% csrf_token %} {{ form }} <input type="submit" value="Submit" /> </form> |
模板中action=””意味着表单将提交给与当前页面相同的URL。根据{{ form }},所有的表单字段和它们的属性将通过Django的模板语言拆分成HTML标记 。
最终效果前端效果图如下:
标记部分就是这个form表单帮我们生成的前端代码,其中还有一些我们定义的约束,如最大字符数及不能为空等。
表单和跨站请求伪造的防护
Django原生支持一个简单易用的跨站请求伪造的防护,当提交一个启用CSRF防护的POST表单时,你必须使用上面例子中的csrf_token模板标签。然而,因为CSRF防护在模板中不是与表单直接捆绑在一起的,这个标签在这篇文档的以下示例中将省略。
HTML5输入类型和浏览器验证
如果你的表单包含URLField、EmailField或其它整数字段类型,Django将使用url、email和number这样的HTML5输入类型。默认情况下,浏览器可能会对这些字段进行它们自身的验证,这些验证可能比Django的验证更严格。如果你想禁用这个行为,请设置form标签的novalidate属性,或者指定一个不同的字段,如TextInput。
现在我们有了一个可以工作的网页表单,它通过Django Form描述、通过视图处理并渲染成一个HTML <form>。这是你入门所需要知道的所有内容,但是表单框架为了便利提供了更多的内容。一旦你理解了上面描述的基本处理过程,你应该可以理解表单系统的其它功能并准备好学习更多的底层机制。
七、关于Django的Form类的更多内容
所有的表单类都作为django.forms.Form的子类创建,包括你在Django管理站点中遇到的ModelForm。实际上,如果你的表单打算直接用来添加和编辑Django的模型,ModelForm可以节省你的许多时间、精力和代码,因为它将根据Model类构建一个表单以及适当的字段和属性。
- 绑定和未绑定的表单实例
Bound and unbound forms之间的区别非常重要:
未绑定的表单没有关联的数据,当渲染给用户时,它将为空或包含默认的值。
绑定的表单具有提交的数据,因此可以用来检验数据是否合法。如果渲染一个不合法的绑定的表单,它将包含内联的错误信息,告诉用户如何纠正数据。
表单的is_bound属性将告诉你一个表单是否具有绑定的数据。
- 关于更多字段
考虑一个比我们上面的最小例子更有用的形式,我们可以用它来在个人网站上实现“联系我”功能:
1 2 3 4 5 6 7 |
from django import forms class ContactForm(forms.Form): subject = forms.CharField(max_length=10,label='主题') message = forms.CharField(widget=forms.Textarea,label='消息') email = forms.EmailField(required=False,label='Email') cc_myself = forms.BooleanField(required=False,label='Myself') |
我们前面的表单只使用一个字段your_name,它是一个CharField。 在这个例子中,我们的表单具有四个字段:message、subject、email和cc_myself。 CharField,EmailField和BooleanField只是三种可用的字段类型;在Form fields中可以找到完整列表。
- 自定义约束
另外你可以在form中自定义校验规则,该方法在校验时被系统自动调用,次序在“字段约束”之后。
比如下面约束message中最多能有多少个单词:
1 2 3 4 5 6 7 8 9 10 |
class ContactForm(forms.Form): .... def clean_message(self): message = self.cleaned_data['message'] #能到此处说明数据符合字段约束要求; num_words = len(message.split()) if num_words < 4: raise forms.ValidationError("单词个数低于4个!") if num_words > 9: raise forms.ValidationError("单词个数大于9个!") return message |
- 窗口小部件
每个表单字段都有一个对应的Widget class,它对应一个HTML表单Widget,例如<input type=”text”>。
在大部分情况下,字段都具有一个合理的默认Widget。 例如,默认情况下,CharField具有一个TextInput Widget,它在HTML中生成一个<input type=”text”>。 如果你需要message,在定义表单字段时你应该指定一个合适的Widget,例如我们定义的<textarea>字段。
- 字段数据
不管表单提交的是什么数据,一旦通过调用is_valid()成功验证(is_valid()返回True),验证后的表单数据将位于form.cleaned_data字典中。 这些数据已经为你转换好为Python的字典类型。
此时,你依然可以从request.POST中直接访问到未验证的数据,但是访问验证后的数据更好一些。
八、使用表单模板
你需要做的就是将表单实例放进模板的上下文,如果你的表单在Context中叫做form,那么 {{ form }} 将正确地渲染它的<input>和<label>元素。不要忘记,表单的输出不包含submit标签,和表单的<form>按钮,你必须自己提供它们。
- 表单呈现选项
对于<input>/<label>对,还有几个输出选项:
{{ form.as_table }}:以表格的形式将它们渲染在<tr>标签中。
{{ form.as_p }}:将它们渲染在<p>标签中。
{{ form.as_ul }}:将它们渲染在<li>标签中。
注意,你必须自己提供<ul>或<table>元素。
下面是我们的ContactForm实例的输出{{ form.as_p }}:
1 2 3 4 5 6 7 8 |
<p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" required /></p> <p><label for="id_message">Message:</label> <textarea name="message" id="id_message" required></textarea></p> <p><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" required /></p> <p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p> |
注意,每个表单字段具有一个ID属性并设置为id_<field-name>,它被一起的label标签引用。它对于确保屏幕阅读软件这类的辅助计算非常重要。
- 渲染表单错误消息
当然,这个便利性的代价是更多的工作。直到现在,我们没有担心如何展示错误信息,因为Django已经帮我们处理好。在下面的例子中,我们将自己处理每个字段的错误和表单整体的各种错误。注意,表单和模板顶部的{{ form.non_field_errors }}查找每个字段的错误。
使用{{ form.name_of_field.errors }} 显示表单错误的一个清单,并渲染成一个ul。 看上去可能像:
1 2 3 |
<ul class="errorlist"> <li>Sender is required.</li> </ul> |
这个ul有一个errorlist CSS类型,你可以用它来定义外观。如果你希望进一步自定义错误信息的显示,你可以迭代它们来实现:
1 2 3 4 5 6 7 |
{% if form.subject.errors %} <ol> {% for error in form.subject.errors %} <li><strong>{{ error|escape }}</strong></li> {% endfor %} </ol> {% endif %} |
非字段错误(以及使用nonfield时渲染的隐藏字段错误)将渲染成一个额外的CSS类型form.as_p()以助于和字段错误信息区分。 例如,{{ form.non_field_errors }} 看上去会像:
1 2 3 |
<ul class="errorlist nonfield"> <li>Generic validation error</li> </ul> |
参见The Forms API以获得关于错误、样式以及在模板中使用表单属性的更多内容。
- 循环在表单的字段
如果你为你的表单使用相同的HTML,你可以使用{% for %}循环迭代每个字段来减少重复的代码:
1 2 3 4 5 6 7 8 9 |
{% for field in form %} <div class="fieldWrapper"> {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.help_text %} <p class="help">{{ field.help_text|safe }}</p> {% endif %} </div> {% endfor %} |
{{ field }}中有用的属性包括:
{{ field.label }}:字段的label,例如Email address。
{{ field.label_tag }}:包含在HTML<label>标签中的字段Label。它包含表单的label_suffix。例如,默认的label_suffix是一个冒号:<label for=”id_email”>Email address:</label>。
{{ field.id_for_label }}:用于这个字段的ID(在上面的例子中是id_email)。如果你正在手工构造label,你可能想使用它代替label_tag。如果你有一些内嵌的JavaScript并且想避免硬编码字段的ID,这也是有用的。
{{ field.value }}:字段的值,例如someone@example.com。
{{ field.html_name }}:输入元素的name属性中将使用的名称,它将考虑到表单的前缀。
{{ field.help_text }}:与该字段关联的帮助文档。
{{ field.errors }}:输出一个<ul class=”errorlist”>,包含这个字段的验证错误信息。你可以使用{% for error in field.errors %}自定义错误的显示。这种情况下,循环中的每个对象只是一个包含错误信息的简单字符串。
{{ field.field }}:表单类中的Field实例,通过BoundField封装。你可以使用它来访问Field属性,例如{{ char_field.field.max_length }}。
最后前面那个form类结合css进行渲染出来,主要代码如下:
forms.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django import forms class ContactForm(forms.Form): subject = forms.CharField(max_length=100,label='主题') message = forms.CharField(widget=forms.Textarea,label='消息') email = forms.EmailField(required=False,label='Email') cc_myself = forms.BooleanField(required=False,label='Myself') def clean_message(self): message = self.cleaned_data['message'] #能到此处说明数据符合字段约束要求; num_words = len(message.split()) if num_words < 4: raise forms.ValidationError("单词个数低于4个!") if num_words > 9: raise forms.ValidationError("单词个数大于9个!") return message |
views.py
1 2 3 4 5 6 7 8 9 10 11 |
def contact_author(request): if request.method == 'POST': form = ContactForm(request.POST) if form.is_valid(): dt = form.cleaned_data print(dt) return HttpResponse(dt['subject']) else: form = ContactForm() return render(request, 'polls/contact_author.html', {'form': form}) |
contact_author.html
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 30 31 32 33 34 35 36 37 38 39 40 |
<html> <style type="text/css"> {# <ul class="errorlist"></ul> #} {# ul标签下的class="errorlist"的属性进行渲染 #}{# 标签下的属性 #} ul.errorlist { margin: 0; padding: 0; } {# <ul class="errorlist"><li>单词个数低于4个!</li></ul> #} {# errorlist class下的 li标签内的元素进行渲染 #}{# 属性下一级的标签 #} .errorlist li { background-color: red; color: white; display: block; font-size: 10px; margin: 0 0 3px; padding: 4px 5px; } .field{ background-color: gray; } </style> <head> <title>Contact us</title> </head> <body> {% if form.errors %} <p style="color: red;"> </p> {% endif %} <form action="" method="post"> <table> {% csrf_token %} {{ form.as_table }} </table> <input type="submit" value="提交"> </form> </body> </html> |
大概结果如下:
参考文章:http://www.cnblogs.com/edisonfeng/p/3779974.html
中文官方:http://python.usyiyi.cn/translate/Django_111/topics/forms/index.html