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 = Trueerr_msg = name = requests.POST.get('name')password = requests.POST.get('password')if name != 'spyinx' or password != '123456':success = Falseerr_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: px;margin-bottom: px;height: px;}.input-red {background-color: red}.color-red {color: red;font-size: px;}.checkbox {font-size: px;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: px/></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=,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=,max_length=,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 = Trueerr_msg = if form.is_valid():login_data = form.clean()name = login_data['name']password = login_data['password']if name != 'spyinx' or password != 'SPYinx123456':success = Falseerr_msg = 用户名密码不正确else:success = Falseerr_msg = form.errors['password'][]print(success, err_msg, form.errors)return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})

最后,添加相应的 URLConf 配置,如下:

# 代码位置:hello_app/urls.pyurlpatterns = [# ...# 表单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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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_boundFalse>>> login_bound.is_boundTrue

这里 Form 类提供了一个 is_bound 属性去判断这个 Form 实例是 bound 还是 unbound 的。可以看下 is_bound 的赋值代码就知道其含义了:

# 源码路径: django/forms/forms.py# ...@html_safeclass 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_safeclass 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_safeclass 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.returnself.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():returnself._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] = valueif hasattr(self, 'clean_%s' % name):value = getattr(self, 'clean_%s' % name)()self.cleaned_data[name] = valueexcept 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_safeclass BaseForm:# ...    
    @propertydef errors(self):Return an ErrorDict for the data provided for the form.if self._errors is None:self.full_clean()return self._errorsdef 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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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 , in <module>AttributeError: 'LoginForm' object has no attribute 'cleaned_data'>>> login_bound.clean()Traceback (most recent call last):
  File <console>, line , in <module>
  File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/forms.py, line , in cleanreturn self.cleaned_data
AttributeError: 'LoginForm' object has no attribute 'cleaned_data'

看到了源码之后,我们要学会操作,学会分析一些现象,然后动手实践,这样才会对 Django 的源码越来越熟悉。

Form.errors:它是一个类属性,保存的是校验错误的信息。该属性还有一些有用的方法,我们在下面实践中熟悉:

(django-manual) [root@server first_django_app]# python manage.py shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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_safeclass BaseForm:# ...
    @propertydef 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_safeclass 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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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 ><django.forms.fields.CharField object at ><django.forms.fields.BooleanField object at >
  • Form.cleaned_dataForm 类中的每个字段不仅可以验证数据,还可以清理数据,形成统一的格式;

  • Form.has_changed():检查表单数据是否已从初始数据更改。

2.4 表单输出

Form 对象的另一个作用是将自身转为HTML。为此,我们只需简单地使用 print()  方法就可以看到 From 对象的 HTML 输出。

(django-manual) [root@server first_django_app]# python manage.py shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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>天自动登录:</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>账号名最短位</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>天自动登录:</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>天自动登录:</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_safeclass 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_classesif 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_textelse: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(, 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[-]# 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[-] = last_row[:-len(row_ender)] + str_hidden + row_enderelse:# 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。