Django 表单使用-数据校验与属性方法
本小节会介绍 Django 中 Form 对象的相关属性与方法,并结合实战让大家能彻底掌握表单的用法。
1. 关于表单的两个基本实验
表单我们在前面介绍 HTML 基础的时候介绍过。下面是之前完成的一个简单的表单示例,模仿普通网站的登录表单:
(django-manual) [root@server first_django_app]# cat templates/test_form1.html
{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}" />
{% if not success %}
<form action="/hello/test_form_view1/" method="POST">
{% csrf_token %}
<div><span>账号:</span><input class="input-text" type="text" placeholder="请输入登录手机号/邮箱" name="name" required/></div>
<div><span>密码:</span><input class="input-text" type="password" placeholder="请输入密码" name="password" required/></div>
<div>
<label style="font-size: 10px; color: grey">
<input type="checkbox" checked="checked" name="save_login"/>7天自动登录
</label>
</div>
<div><input class="input-text input-red" type="submit" value="登录" style="width: 214px"/></div>
{% if err_msg %}
<div><label class="color-red">{{ err_msg }}</label</div>
{% endif %}
</form>
{% else %}
<p>登录成功</p>
{% endif %}
准备好视图函数:
class TestFormView1(TemplateView):
template_name = 'test_form1.html'
# template_name = 'register.html'
def get(self, requests, *args, **kwargs):
return self.render_to_response(context={'success': False})
def post(self, requests, *args, **kwargs):
success = True
err_msg = ""
name = requests.POST.get('name')
password = requests.POST.get('password')
if name != 'spyinx' or password != '123456':
success = False
err_msg = "用户名密码不正确"
return self.render_to_response(context={'success': success, 'err_msg': err_msg})
最后编写 URLConf,要和表单中的 action 属性值保持一致:
urlpatterns = [
# ...
# 表单测试
path('test_form_view1/', views.TestFormView1.as_view(), name='test_form_view1'),
]
接下来启动服务然后放对应的登录页面。操作如下:
这是一个简单的手写表单提交功能。但是实际上,我们不用写前端的那么 input 之类的,这些可以有 Django 的 Form 表单帮我们完成这些,不过基本的页面还是要有的。我们现在用 Django 的 Form 表单模块来实现和上面相同的功能,同时还能对表单中的元素进行校验,这能极大的简化我们的 Django 代码,不用在视图函数中进行 if-else
校验。
首先准备好静态资源,包括模板文件以及 css 样式文件:
/* 代码位置:static/css/main.css */
/* 忽略部分无关样式 */
.input-text {
margin-top: 5px;
margin-bottom: 5px;
height: 30px;
}
.input-red {
background-color: red
}
.color-red {
color: red;
font-size: 12px;
}
.checkbox {
font-size: 10px;
color: grey;
}
{# 代码位置:template/test_form2.html #}
{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}" />
{% if not success %}
<form action="/hello/test_form_view2/" method="POST">
{% csrf_token %}
<div><span>{{ form.name.label }}:</span>{{ form.name }}
<div><span>{{ form.password.label }}:</span>{{ form.password }}
<div>
{{ form.save_login }}{{ form.save_login.label }}
</div>
<div><input class="input-text input-red" type="submit" value="登录" style="width: 214px"/></div>
{% if err_msg %}
<div><label class="color-red">{{ err_msg }}</label</div>
{% endif %}
</form>
{% else %}
<p>登录成功</p>
{% endif %}
注意:这个时候,我们用 form 表单对象中定义的属性来帮我们生成对应的 input 或者 checkbox 等元素。
同样继续视图函数的编写。此时,我们需要使用 Django 的 Form 表单功能,先看代码,后面会慢慢介绍代码中的类、函数以及相关的参数含义:
# 源码位置:hello_app/view.py
# ...
# 自定义密码校验
def password_validate(value):
"""
密码校验器
"""
pattern = re.compile(r'^(?=.*[0-9].*)(?=.*[A-Z].*)(?=.*[a-z].*).{6,20}$')
if not pattern.match(value):
raise ValidationError('密码需要包含大写、小写和数字')
# 定义的表单,会关联到前端页面,生成表单中的元素
class LoginForm(forms.Form):
name = forms.CharField(
label="账号",
min_length=4,
required=True,
error_messages={'required': '账号不能为空', "min_length": "账号名最短4位"},
widget=forms.TextInput(attrs={'class': "input-text",
'placeholder': '请输入登录账号'})
)
password = forms.CharField(
label="密码",
validators=[password_validate, ],
min_length=6,
max_length=20,
required=True,
# invalid时对应的错误信息
error_messages={'required': '密码不能为空', "invalid": "密码需要包含大写、小写和数字", "min_length": "密码最短8位", "max_length": "密码最>长20位"},
widget=forms.TextInput(attrs={'class': "input-text",'placeholder': '请输入密码', 'type': 'password'})
)
save_login = forms.BooleanField(
required=False,
label="7天自动登录",
initial="checked",
widget=forms.widgets.CheckboxInput(attrs={'class': "checkbox"})
)
class TestFormView2(TemplateView):
template_name = 'test_form2.html'
def get(self, request, *args, **kwargs):
form = LoginForm()
return self.render_to_response(context={'success': False, 'form': form})
def post(self, request, *args, **kwargs):
# 将数据绑定到表单,这样子才能使用is_valid()方法校验表单数据的合法性
form = LoginForm(request.POST)
success = True
err_msg = ""
if form.is_valid():
login_data = form.clean()
name = login_data['name']
password = login_data['password']
if name != 'spyinx' or password != 'SPYinx123456':
success = False
err_msg = "用户名密码不正确"
else:
success = False
err_msg = form.errors['password'][0]
print(success, err_msg, form.errors)
return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
最后,添加相应的 URLConf 配置,如下:
# 代码位置:hello_app/urls.py
urlpatterns = [
# ...
# 表单2测试
path('test_form_view2/', views.TestFormView2.as_view(), name='test_form_view2'),
]
最后,继续启动 first_django_app 工程,然后访问此 form 表单接口,可以发现效果和原来的是一样的。此外,我们通过直接在 form 表单中设置好相应的校验规则,Django 会自动帮我们处理校验问题,并通过 is_valid()
方法来帮我们验证表单参数的合法性。比如上面我们自定义了一个密码校验器,输入的密码必须包含大写字母、小写字母和数字,三者缺一不可。我们只需要添加校验器,放到定义的 Form 的对应属性字段中即可,使用起来非常方便。参见下面的演示:
2. Django 中的 Form 模块
2.1 Bound and unbound forms
注意下这两个概念,官方文档描述的,比较容易理解,就是这个 forms 有没有绑定相关的数据,绑定了就是 bound,没绑定就是 unbound。我们利用前面实验2中定义的 LoginForm 来进行演示,具体操作如下:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login_unbound = LoginForm()
>>> login_bound = LoginForm({'name': 'test11', 'password': '111111', 'save_login': False})
>>> login_unbound.is_bound
False
>>> login_bound.is_bound
True
这里 Form 类提供了一个 is_bound 属性去判断这个 Form 实例是 bound 还是 unbound 的。可以看下 is_bound 的赋值代码就知道其含义了:
# 源码路径: django/forms/forms.py
# ...
@html_safe
class BaseForm:
# ...
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
self.is_bound = data is not None or files is not None
# ...
# ...
# ...
2.2 使用 Form 校验数据
使用表单校验传过来的数据是否合法,这大概是使用 Form 类的优势之一。比如前面的实验2中,我们完成了一个对输入密码字段的校验,要求输入的密码必须含有大小写以及数字,三者缺一不可。在 Form 中,对于使用 Form 校验数据,我们会用到它的如下几个方法:
Form.clean():默认是返回一个 cleaned_data。对于 cleaned_data 的含义,后面会在介绍 Form 属性字段时介绍到,我们看源码可知,clean() 方法只是返回 Form 中得到的统一清洗后的正确数据。
# 源码位置:django/forms/forms.py
# ...
@html_safe
class BaseForm:
# ...
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() has been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field named '__all__'.
"""
return self.cleaned_data
# ...
# ...
我们继续在前面的命令行上完成实验。上面输入的数据中由于 password 字段不满足条件,所以得到的cleaned_data 只有 name 字段。 想要调用 clean() 方法,必须要生成 cleaned_data 的值,而要生成cleaned_data 的值,就必须先调用 full_clean() 方法,操作如下:
>>> login_bound.full_clean()
>>> login_bound.clean()
{'name': 'test11'}
>>> login_bound.cleaned_data
{'name': 'test11'}
来从源代码中看看为什么要先调用 full_clean() 方法才有 cleaned_data 数据:
# 源码位置:django/forms/forms.py
@html_safe
class BaseForm:
# ...
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
self._post_clean()
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
# ...
可以看到,全局搜索 cleaned_data 字段,可以发现 cleaned_data 的初始赋值在 full_clean()
方法中,然后会在 _clean_fields()
方法中对 Form 中所有通过校验的数据放入到 cleaned_data,形成相应的值。
Form.is_valid():这个方法就是判断 Form 表单中的所有字段数据是否都通过校验。如果有一个没有通过就是 False,全部正确才是 True:
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login_bound = LoginForm({'name': 'test11', 'password': '111111', 'save_login': False})
>>> login_bound.is_valid()
>>> login_bound.clean()
{'name': 'test11'}
>>> login_bound.cleaned_data
{'name': 'test11'}
注意:我们发现在使用了 is_valid()
方法后,对应 Form 实例的 cleaned_data
属性值也生成了,而且有了数据。参看源码可知 is_valid()
方法同样调用了 full_clean()
方法:
# 源码位置:django/forms/forms.py
# ...
@html_safe
class BaseForm:
# ...
@property
def errors(self):
"""Return an ErrorDict for the data provided for the form."""
if self._errors is None:
self.full_clean()
return self._errors
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
# ...
# ...
在看到这段源代码的时候,我们可以这样考虑下,如果想让上面的 is_valid()
方法调用后并不继续调用 full_clean()
方法,这样 cleaned_data
就不会生成,再调用 clean()
方法就会报错。那么我们如何做呢?很简单,只需要控制 if self._errors is None
这个语句不成立即可:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login_bound = LoginForm({'name': 'test11', 'password': 'SPYinx1111', 'save_login': False})
>>> print(login_bound._errors)
None
>>> login_bound._errors=[]
>>> login_bound.is_valid()
True
>>> login_bound.cleaned_data
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'LoginForm' object has no attribute 'cleaned_data'
>>> login_bound.clean()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/forms.py", line 430, in clean
return self.cleaned_data
AttributeError: 'LoginForm' object has no attribute 'cleaned_data'
看到了源码之后,我们要学会操作,学会分析一些现象,然后动手实践,这样才会对 Django 的源码越来越熟悉。
Form.errors:它是一个类属性,保存的是校验错误的信息。该属性还有一些有用的方法,我们在下面实践中熟悉:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login_bound = LoginForm({'name': 'te', 'password': 'spyinx1111', 'save_login': False})
>>> login_bound.errors
{'name': ['账号名最短4位'], 'password': ['密码需要包含大写、小写和数字']}
>>> login_bound.errors.as_data()
{'name': [ValidationError(['账号名最短4位'])], 'password': [ValidationError(['密码需要包含大写、小写和数字'])]}
# 中文编码
>>> login_bound.errors.as_json()
'{"name": [{"message": "\\u8d26\\u53f7\\u540d\\u6700\\u77ed4\\u4f4d", "code": "min_length"}], "password": [{"message": "\\u5bc6\\u7801\\u9700\\u8981\\u5305\\u542b\\u5927\\u5199\\u3001\\u5c0f\\u5199\\u548c\\u6570\\u5b57", "code": ""}]}'
看到最后的 as_json() 方法,发现转成 json 的时候中文乱码,这个输出的是 unicode 编码结果。第一眼看过去特别像 json.dumps() 的包含中文的情况。为了能解决此问题,我们先看源码,找到原因:
# 源码位置:django/forms/forms.py
@html_safe
class BaseForm:
# ...
@property
def errors(self):
"""Return an ErrorDict for the data provided for the form."""
if self._errors is None:
self.full_clean()
return self._errors
# ...
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict()
# ...
# ...
# 源码位置: django/forms/utils.py
@html_safe
class ErrorDict(dict):
"""
A collection of errors that knows how to display itself in various formats.
The dictionary keys are the field names, and the values are the errors.
"""
def as_data(self):
return {f: e.as_data() for f, e in self.items()}
def get_json_data(self, escape_html=False):
return {f: e.get_json_data(escape_html) for f, e in self.items()}
def as_json(self, escape_html=False):
return json.dumps(self.get_json_data(escape_html))
# ...
可以看到,errors
属性的 as_json()
方法最后调用的就是 json.dumps()
方法。一般要解决它的中文输出显示问题,只需要加上一个 ensure_ascii=False
即可。这里我们也只需要改下源码:
(django-manual) [root@server first_django_app]# vim ~/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/utils.py
# ...
def as_json(self, escape_html=False):
return json.dumps(self.get_json_data(escape_html), ensure_ascii=False)
# ...
然后我们再次进行 shell 命令行下,执行刚才的命令,发现中文输出已经正常了。
django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login_bound = LoginForm({'name': 'te', 'password': 'spyinx1111', 'save_login': False})
>>> login_bound.errors
{'name': ['账号名最短4位'], 'password': ['密码需要包含大写、小写和数字']}
>>> login_bound.errors.as_json()
'{"name": [{"message": "账号名最短4位", "code": "min_length"}], "password": [{"message": "密码需要包含大写、小写和数字", "code": ""}]}'
>>>
2.3 表单的一些有用的属性与方法
现在我们简单介绍一些 Form 的属性与方法,主要参考的是官方文档。大家可以在这个地址上多多学习和实践。
-
Form.fields:通过
Form
实例的fields
属性可以访问实例的字段;>>> for row in login.fields.values(): print(row) ... <django.forms.fields.CharField object at 0x7f8b3c081e20> <django.forms.fields.CharField object at 0x7f8b3c081cd0> <django.forms.fields.BooleanField object at 0x7f8b3c081910>
-
Form.cleaned_data:
Form
类中的每个字段不仅可以验证数据,还可以清理数据,形成统一的格式; -
Form.has_changed():检查表单数据是否已从初始数据更改。
2.4 表单输出
Form 对象的另一个作用是将自身转为HTML。为此,我们只需简单地使用 print()
方法就可以看到 From 对象的 HTML 输出。
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> f = LoginForm()
>>> print(f)
<tr><th><label for="id_name">账号:</label></th><td><input type="text" name="name" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></td></tr>
<tr><th><label for="id_password">密码:</label></th><td><input type="password" name="password" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></td></tr>
<tr><th><label for="id_save_login">7天自动登录:</label></th><td><input type="checkbox" name="save_login" value="checked" class="checkbox" id="id_save_login" checked></td></tr>
如果表单绑定了数据,则 HTML 输出包含数据的 HTML 文本,我们接着上面的 shell 继续执行:
>>> login = LoginForm({'name': 'te', 'password': 'spyinx1111', 'save_login': False})
>>> print(login)
<tr><th><label for="id_name">账号:</label></th><td><ul class="errorlist"><li>账号名最短4位</li></ul><input type="text" name="name" value="te" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></td></tr>
<tr><th><label for="id_password">密码:</label></th><td><ul class="errorlist"><li>密码需要包含大写、小写和数字</li></ul><input type="password" name="password" value="spyinx1111" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></td></tr>
<tr><th><label for="id_save_login">7天自动登录:</label></th><td><input type="checkbox" name="save_login" class="checkbox" id="id_save_login"></td></tr>
>>> login = LoginForm({'name': 'texxxxxxxx', 'password': 'SPYinx123456', 'save_login': False})
>>> print(login)
<tr><th><label for="id_name">账号:</label></th><td><input type="text" name="name" value="texxxxxxxx" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></td></tr>
<tr><th><label for="id_password">密码:</label></th><td><input type="password" name="password" value="SPYinx123456" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></td></tr>
<tr><th><label for="id_save_login">7天自动登录:</label></th><td><input type="checkbox" name="save_login" class="checkbox" id="id_save_login"></td></tr>
上面我们测试了两种情况,一种错误数据,一种是正常情况显示的 HTML。此默认输出的每个字段都有一个<tr>
。需要注意以下几点:
- 输出不会包括
<table>
和</table>
以及<form>
和</form>
,这些需要我们自己写到模板文件中去; - 每一个 Field 类型都会被翻译成一个固定的 HTML 语句,比如
CharField
表示的是<input type="text">
,EmailField
对应着<input type="email">
,BooleanField
对应着<input type="checkbox">
; - 上面 HTML 代码里的 input 元素中的 name 属性值会从对应 Field 的属性值直接获取;
- 每个字段的文本都会有
<label>
元素,同时会有默认的标签值 (可以在 Field 中用 label 参数覆盖默认值);
另外,Form 类还提供了以下几个方法帮我们调整下 Form 的输出 HTML:
- Form.as_p():将表单翻译为一系列
<p>
标签; - Form.as_ul():将表单翻译成一系列的
<li>
标签; - Form.as_table():这个和前面
print()
的结果一样。实际上,当你调用print()
时,内部实际上时调用as_table()
方法。
对上面的三个方法我们先进行实操演练,然后在看其源码,这样能更好的帮助我们理解这三个方法调用背后的逻辑。其操作过程和源码如下:
>>> from hello_app.views import LoginForm
>>> from django import forms
>>> login = LoginForm({'name': 'texxxxxxxx', 'password': 'SPYinx123456', 'save_login': False})
>>> login.as_p()
'<p><label for="id_name">账号:</label> <input type="text" name="name" value="texxxxxxxx" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></p>\n<p><label for="id_password">密码:</label> <input type="password" name="password" value="SPYinx123456" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></p>\n<p><label for="id_save_login">7天自动登录:</label> <input type="checkbox" name="save_login" class="checkbox" id="id_save_login"></p>'
>>> login.as_ul()
'<li><label for="id_name">账号:</label> <input type="text" name="name" value="texxxxxxxx" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></li>\n<li><label for="id_password">密码:</label> <input type="password" name="password" value="SPYinx123456" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></li>\n<li><label for="id_save_login">7天自动登录:</label> <input type="checkbox" name="save_login" class="checkbox" id="id_save_login"></li>'
>>> login.as_table()
'<tr><th><label for="id_name">账号:</label></th><td><input type="text" name="name" value="texxxxxxxx" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id_name"></td></tr>\n<tr><th><label for="id_password">密码:</label></th><td><input type="password" name="password" value="SPYinx123456" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id_password"></td></tr>\n<tr><th><label for="id_save_login">7天自动登录:</label></th><td><input type="checkbox" name="save_login" class="checkbox" id="id_save_login"></td></tr>'
# 源码位置:django/forms/forms.py
# ...
@html_safe
class BaseForm:
# ...
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
"Output HTML. Used by as_table(), as_ul(), as_p()."
top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
output, hidden_fields = [], []
for name, field in self.fields.items():
html_class_attr = ''
bf = self[name]
bf_errors = self.error_class(bf.errors)
if bf.is_hidden:
if bf_errors:
top_errors.extend(
[_('(Hidden field %(name)s) %(error)s') % {'name': name, 'error': str(e)}
for e in bf_errors])
hidden_fields.append(str(bf))
else:
# Create a 'class="..."' attribute if the row should have any
# CSS classes applied.
css_classes = bf.css_classes()
if css_classes:
html_class_attr = ' class="%s"' % css_classes
if errors_on_separate_row and bf_errors:
output.append(error_row % str(bf_errors))
if bf.label:
label = conditional_escape(bf.label)
label = bf.label_tag(label) or ''
else:
label = ''
if field.help_text:
help_text = help_text_html % field.help_text
else:
help_text = ''
output.append(normal_row % {
'errors': bf_errors,
'label': label,
'field': bf,
'help_text': help_text,
'html_class_attr': html_class_attr,
'css_classes': css_classes,
'field_name': bf.html_name,
})
if top_errors:
output.insert(0, error_row % top_errors)
if hidden_fields: # Insert any hidden fields in the last row.
str_hidden = ''.join(hidden_fields)
if output:
last_row = output[-1]
# Chop off the trailing row_ender (e.g. '</td></tr>') and
# insert the hidden fields.
if not last_row.endswith(row_ender):
# This can happen in the as_p() case (and possibly others
# that users write): if there are only top errors, we may
# not be able to conscript the last row for our purposes,
# so insert a new, empty row.
last_row = (normal_row % {
'errors': '',
'label': '',
'field': '',
'help_text': '',
'html_class_attr': html_class_attr,
'css_classes': '',
'field_name': '',
})
output.append(last_row)
output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
else:
# If there aren't any rows in the output, just append the
# hidden fields.
output.append(str_hidden)
return mark_safe('\n'.join(output))
def as_table(self):
"Return this form rendered as HTML <tr>s -- excluding the <table></table>."
return self._html_output(
normal_row='<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
error_row='<tr><td colspan="2">%s</td></tr>',
row_ender='</td></tr>',
help_text_html='<br><span class="helptext">%s</span>',
errors_on_separate_row=False,
)
def as_ul(self):
"Return this form rendered as HTML <li>s -- excluding the <ul></ul>."
return self._html_output(
normal_row='<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
error_row='<li>%s</li>',
row_ender='</li>',
help_text_html=' <span class="helptext">%s</span>',
errors_on_separate_row=False,
)
def as_p(self):
"Return this form rendered as HTML <p>s."
return self._html_output(
normal_row='<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
error_row='%s',
row_ender='</p>',
help_text_html=' <span class="helptext">%s</span>',
errors_on_separate_row=True,
)
可以看到,转换输出的三个方法都是调用 _html_output()
方法。这个方法总体上来看代码量不大,涉及的调用也稍微有点多,但是代码的逻辑并不复杂,是可以认真看下去的。
给大家留个思考题:上面的 _html_output()
方法在哪一步将 field
转成对应的 html 元素的,比如我们前面提到的 CharField
将会被翻译成 <input type="text">
这样的 HTML 标签。这个答案我将会在下一小节中给大家解答。
3. 小结
在本节中我们先用两个简单的实验例子对 Django 中的 Form 表单有了初步的印象。接下来深入介绍了 Django 表单中为我们提供的Form 类,并依据官方文档提供的顺序依次介绍 Form 类的相关属性和方法,并对大部分的属性和方法对着源码介绍了其含义和用法。接下来将继续介绍 Form 部分的 Field 类,同样会介绍其各种属性和方法以及 Django 定义的各种内置 Field。