Python / 25 Django 表单使用-数据校验与属性方法

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_dataForm 类中的每个字段不仅可以验证数据,还可以清理数据,形成统一的格式;

  • 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。