form进阶

表单渲染格式

  • {{ form.as_table }} 将表单渲染成一个表格元素,每个输入框作为一个标签
  • {{ form.as_p }} 渲染表单为一系列的p标签,每个p标签包含一个字段。
  • {{ form.as_ul }} 渲染表单为一系列的li标签,每个li 标签包含一个字段,它不包含ul标签。

注意:Django自动为每个input元素设置了一个id名称,对应label的for参数。

手动渲染表单字段

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField(widget=forms.Textarea)
    sender = forms.EmailField()
    cc_myself = forms.BooleanField(required=False)

如要使用CSS和JS,比如要引入Bootstarps框架,这些都需要对表单内的input元素进行额外控制。

可以通过{{ form.name_of_field }}获取每一个字段,然后分别渲染,如下例所示:

{{ form.non_field_errors }}
<div class="fieldWrapper">
    {{ form.subject.errors }}
    <label for="{{ form.subject.id_for_label }}">Email subject:
</label>
    {{ form.subject }}
</div>

其中的label标签甚至可以用label_tag()方法来生成,于是可以简写成下面的样子:
<div class="fieldWrapper">
    {{ form.message.errors }}
    {{ form.message.label_tag }}
    {{ form.message }}
</div>

<div class="fieldWrapper">
    {{ form.sender.errors }}
    {{ form.sender.label_tag }}
    {{ form.sender }}
</div>
<div class="fieldWrapper">
    {{ form.cc_myself.errors }}
    {{ form.cc_myself.label_tag }}
    {{ form.cc_myself }}
</div>

渲染表单错误信息:

注意上面的例子中,我们使用{{ form.name_of_field.errors }}模板语法,在表单里处理错误信息。对于每一个表单字段的错误,它其实会实际生成一个无序列表,参考下面的样子:

<ul class="errorlist">
    <li>Sender is required.</li>
</ul>

这个列表有个默认的CSS样式类errorlist,如果你想进一步定制这个样式,可以循环错误列表里的内容,然后单独设置样式:

{% if form.subject.errors %}
    <ol>
    {% for error in form.subject.errors %}
        <li><strong>{{ error|escape }}</strong></li>
    {% endfor %}
    </ol>
{% endif %}

一切非字段的错误信息,比如表单的错误,隐藏字段的错误都保存在{{ form.non_field_errors }}中,上面的例子,我们把它放在了表单的外围上面,它将被按下面的HTML和CSS格式渲染:

<ul class="errorlist nonfield">
    <li>Generic validation error</li>
</ul>

循环表单的字段

如果表单字段有相同格式的HMTL表现,那么完全可以循环生成,不必要手动的编写每个字段,减少冗余和重复代码,只需要使用模板语言中的{% for field in form %},通过迭代form,获取其中的所有field。

{% 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信息
{{ field.label_tag }} 自动生成字段的label标签,注意与{{ field.label }}的区别。
{{ field.id_for_label }} 自定义字段标签的id
{{ field.value }} 当前字段的值,比如一个Email字段的值someone@example.com
{{ field.html_name }} 指定字段生成的input标签中name属性的值
{{ field.help_text }} 字段的帮助信息
{{ field.errors }} 包含错误信息的元素
{{ field.is_hidden }} 用于判断当前字段是否为隐藏的字段,如果是,返回True
{{ field.field }} 返回字段的参数列表。例如{{ char_field.field.max_length }}

不可见字段的特殊处理:

很多时候,我们的表单中会有一些隐藏的不可见的字段,比如honeypot。我们需要让它在任何时候都仿佛不存在一般,比如有错误的时候,如果你在页面上显示了不可见字段的错误信息,那么用户会很迷惑,这是哪来的呢?所以,通常我们是不显示不可见字段的错误信息的。

Django提供了两种独立的方法,用于循环那些不可见的和可见的字段,hidden_fields()visible_fields()。:

{# 循环那些不可见的字段 #}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{# 循环可见的字段 #}
{% for field in form.visible_fields %}
    <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

重用表单模板

如果在自己的HTML文件中,多次使用同一种表单模板,那么你完全可以把表单模板存成一个独立的HTML文件,然后在别的HTML文件中通过include模板语法将其包含进来,如下例所示:

# 实际的页面文件中:
{% include "form_snippet.html" %}

-----------------------------------------------------

# 单独的表单模板文件form_snippet.html:
{% for field in form %}
    <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

如果你的页面同时引用了好几个不同的表单模板,那么为了防止冲突,你可以使用with参数,给每个表单模板取个别名,如下所示:

{% include "form_snippet.html" with form=comment_form %}

在使用的时候就是:

{% for field in comment_form %}
......

如果你经常做这些重用的工作,建议考虑自定义一个内联标签。

Django表单API详解

以下的Form、表单等术语都指的的广义的Django表单。

Form要么是绑定了数据的,要么是未绑定数据的。

如果是绑定的,那么它能够验证数据,并渲染表单及其数据,然后生成HTML表单。如果未绑定,则无法进行验证(因为没有数据可以验证),但它仍然可以以HTML形式呈现空白表单。

表单类原型:class Form[source]

若要创建一个未绑定的Form实例,只需简单地实例化该类:

f = ContactForm()

若要绑定数据到表单,可以将数据以字典的形式传递给Form类的构造函数:

 >>> data = {'subject''hello',
 ...         'message''Hi there',
 ...         'sender''foo@example.com',
 ...         'cc_myself'True}
 >>> f = ContactForm(data)

在这个字典中,键为字段的名称,它们对应于Form类中的字段。 值为需要验证的数据。

表单的绑定属性

Form.is_bound

如果需要区分绑定的表单和未绑定的表单,可以检查下表单的is_bound属性值:

>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject''hello'})
>>> f.is_bound
True

注意,传递一个空的字典将创建一个带有空数据的绑定的表单:

>>> f = ContactForm({})
>>> f.is_bound
True

如果你、有一个绑定的Form实例但是想改下数据,或者想给一个未绑定的Form表单绑定某些数据,你需要创建另外一个Form实例。因为,Form实例的数据没是自读的,Form实例一旦创建,它的数据将不可变

使用表单验证数据

RegexValidator验证器

from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.validators import RegexValidator

class MyForm(Form):
    user = fields.CharField(
        validators=[RegexValidator(r'^[0-9]+$''请输入数字'), RegexValidator(r'^159[0-9]+$''数字必须以159开头')],
    )

自定义验证函数

import re
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.exceptions import ValidationError


# 自定义验证规则
def mobile_validate(value):
    mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$')
    if not mobile_re.match(value):
        raise ValidationError('手机号码格式错误')  # 自定义验证规则的时候,如果不符合你的规则,需要自己发起错误


class PublishForm(Form):
    phone = fields.CharField(
                            validators=[mobile_validate, ],  # 使用自定义验证规则
                            error_messages={'required''手机不能为空'},
                            widget=widgets.TextInput(attrs={'class'"form-control",'placeholder''手机号码'}))  # form-control:bootstrap中的样式。

Hook钩子方法

Form类中定义钩子函数,来实现自定义的验证功能。

局部钩子

在Fom类中定义 clean_字段名() 方法,就能够实现对特定字段进行校验。

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required""不能为空",
            "invalid""格式错误",
            "min_length""用户名最短8位"
        },
        widget=forms.widgets.TextInput(attrs={"class""form-control"})
    )

    # 定义局部钩子,用来校验username字段,提供了添加一些校验功能的钩子,之前的校验规则还在。
    def clean_username(self):
        value = self.cleaned_data.get("username")   # 其实此时value中已经有了username字段前的合法数据的信息,因此也可以用username之前的相关字段来做相关的判断。
        if "haha" in value:
            raise ValidationError("请你再换一个用户名")
        else:
            return value        # 必须return value不然合法的字段数据会为空,具体可看源代码 
全局钩子

重新实现Form.clean()方法。在Fom类中定义clean()方法,就能够实现对字段进行全局校验,字段全部验证完,局部钩子也全部执行完之后,再执行这个全局钩子校验。

class LoginForm(forms.Form):
    ...
    password = forms.CharField(
        min_length=6,
        label="密码",
        widget=forms.widgets.PasswordInput(attrs={'class''form-control'}, render_value=True)
    )
    re_password = forms.CharField(
        min_length=6,
        label="确认密码",
        widget=forms.widgets.PasswordInput(attrs={'class''form-control'}, render_value=True)
    )

    # 定义全局的钩子,用来校验密码和确认密码字段是否相同,执行全局钩子的时候,cleaned_data里面肯定是有通过前面验证的所有数据
    def clean(self):
        password_value = self.cleaned_data.get('password')
        re_password_value = self.cleaned_data.get('re_password')
        if password_value == re_password_value:
            return self.cleaned_data # 全局钩子要返回所有的数据
        else:
            self.add_error('re_password''两次密码不一致'# 在re_password这个字段的错误列表中加上一个错误,并且clean_data里面会自动清除这个re_password的值,所以打印clean_data的时候会看不到它。
            raise ValidationError('两次密码不一致')

Field.clean(value)[source]

虽然表单字段的Field类主要使用在Form类中,但也可以直接实例化它们来使用,以便更好地了解它们是如何工作的。每个Field的实例都有一个clean()方法,它接受一个参数,然后返回“清洁的”数据或者抛出一个django.forms.ValidationError异常:

>>> from django import forms
>>> f = forms.EmailField()
>>> f.clean('foo@example.com')
'foo@example.com'
>>> f.clean('invalid email address')
Traceback (most recent call last):
...
ValidationError: ['Enter a valid email address.']

这个clean方法经常被用来在开发或测试过程中对数据进行验证和测试。

Form.is_valid()

调用is_valid()方法来执行绑定表单的数据验证工作,并返回一个表示数据是否合法的布尔值。

>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True}
>>> f = ContactForm(data)
>>> f.is_valid()
True

下面的情形中,subject为空(默认所有字段都是必需的)且sender是一个不合法的邮件地址:

>>> data = {'subject''',
...         'message''Hi there',
...         'sender''invalid email address',
...         'cc_myself'True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
Form.errors

表单的errors属性保存了错误信息字典:

>>> f.errors
{'sender': ['Enter a valid email address.'], 'subject': ['This field is required.']}

在这个字典中,键为字段的名称,值为错误信息的Unicode字符串组成的列表。错误信息保存在列表中是因为字段可能有多个错误信息。

Form.errors.as_data()

返回一个字典,它将字段映射到原始的ValidationError实例。

>>> f.errors.as_data()
{'sender': [ValidationError(['Enter a valid email address.'])],
'subject': [ValidationError(['This field is required.'])]}
Form.errors.as_json(escape_html=False)

返回JSON序列化后的错误信息字典。

>>> f.errors.as_json()
{"sender": [{"message""Enter a valid email address.""code""invalid"}],
"subject": [{"message""This field is required.""code""required"}]}
Form.add_error(field, error)

向表单特定字段添加错误信息。

field参数为字段的名称。如果值为None,error将作为Form.non_field_errors()的一个非字段错误。

Form.has_error(field, code=None)

判断某个字段是否具有指定code的错误。当code为None时,如果字段有任何错误它都将返回True。

Form.non_field_errors()

返回Form.errors中不是与特定字段相关联的错误。

对于没有绑定数据的表单

验证没有绑定数据的表单是没有意义的,下面的例子展示了这种情况:

>>> f = ContactForm()
>>> f.is_valid()
False
>>> f.errors
{}

检查表单数据是否被修改

Form.has_changed()

当你需要检查表单的数据是否从初始数据发生改变时,可以使用has_changed()方法。

>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True}
>>> f = ContactForm(data, initial=data)
>>> f.has_changed()
False

提交表单后,我们可以重新构建表单并提供初始值,进行比较:

>>> f = ContactForm(request.POST, initial=data)
>>> f.has_changed()

如果request.POST与initial中的数据有区别,则返回True,否则返回False。

Form.changed_data

返回有变化的字段的列表。

>>> f = ContactForm(request.POST, initial=data)
>>> if f.has_changed():
...     print("The following fields changed: %s" % ", ".join(f.changed_data))

访问表单中的字段

通过fileds属性访问表单的字段:

>>> for row in f.fields.values(): print(row)
...
<django.forms.fields.CharField object at 0x7ffaac632510>
<django.forms.fields.URLField object at 0x7ffaac632f90>
<django.forms.fields.CharField object at 0x7ffaac3aa050>
>>> f.fields['name']
<django.forms.fields.CharField object at 0x7ffaac6324d0>

可以修改Form实例的字段来改变字段在表单中的表示:

>>> f.as_table().split('\n')[0]
'<tr><th>Name:</th><td><input name="name" type="text" value="instance" required /></td></tr>'
>>> f.fields['name'].label = "Username"
>>> f.as_table().split('\n')[0]
'<tr><th>Username:</th><td><input name="name" type="text" value="instance" required /></td></tr>'

注意不要改变base_fields属性,因为一旦修改将影响同一个Python进程中接下来所有的ContactForm实例:

>>> f.base_fields['name'].label = "Username"
>>> another_f = CommentForm(auto_id=False)
>>> another_f.as_table().split('\n')[0]
'<tr><th>Username:</th><td><input name="name" type="text" value="class" required /></td></tr>'

访问cleaned_data

Form.cleaned_data,Form类中的每个字段不仅负责验证数据,还负责将它们转换为正确的格式。例如,DateField将输入转换为Python的datetime.date对象。无论你传递的是普通字符串'1994-07-15'、DateField格式的字符串、datetime.date对象、还是其它格式的数字,Django将始终把它们转换成datetime.date对象。

一旦创建一个Form实例并通过验证后,就可以通过它的cleaned_data属性访问干净的数据:

>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'cc_myself'True'message''Hi there''sender''foo@example.com''subject''hello'}

如果你的数据没有通过验证,cleaned_data字典中只包含合法的字段:

>>> data = {'subject''',
...         'message''Hi there',
...         'sender''invalid email address',
...         'cc_myself'True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
>>> f.cleaned_data
{'cc_myself'True'message''Hi there'}

cleaned_data字典始终只包含Form中定义的字段,即使你在构建Form时传递了额外的数据。 在下面的例子中,我们传递了一组额外的字段给ContactForm构造函数,但是cleaned_data将只包含表单的字段:

>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True,
...         'extra_field_1''foo',
...         'extra_field_2''bar',
...         'extra_field_3''baz'}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data # Doesn't contain extra_field_1, etc.
{'cc_myself'True'message''Hi there''sender''foo@example.com''subject''hello'}

当Form通过验证后,cleaned_data将包含所有字段的键和值,即使传递的数据中没有提供某些字段的值。 在下面的例子中,提供的实际数据中不包含nick_name字段,但是cleaned_data任然包含它,只是值为空:

>>> from django import forms
>>> class OptionalPersonForm(forms.Form):
...     first_name = forms.CharField()
...     last_name = forms.CharField()
...     nick_name = forms.CharField(required=False)
>>> data = {'first_name''John''last_name''Lennon'}
>>> f = OptionalPersonForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'nick_name''''first_name''John''last_name''Lennon'}

表单的HTML生成方式

Form的第二个任务是将它渲染成HTML代码,默认情况下,根据form类中字段的编写顺序,在HTML中以同样的顺序罗列。 我们可以通过print方法展示出来。

如果表单是绑定的,输出的HTML将包含数据。

>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True}
>>> f = ContactForm(data)
>>> print(f)
<tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" value="hello" required /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" value="Hi there" required /></td></tr>
<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" value="foo@example.com" required /></td></tr>
<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" checked /></td></tr>

注意事项:

  • 每个字段类型都由一个默认的HTML标签展示。注意,这些只是默认的,可以使用Widget特别指定。
  • 每个HTML标签的name属性名直接从ContactForm类中获取。
  • form使用HTML5语法。

为错误信息添加CSS样式

Form.error_css_class

Form.required_css_class

为一些特别强调的或者需要额外显示的内容设置醒目的CSS样式是一种常用做法,也是非常有必要的。比如给必填字段加粗显示,设置错误文字为红色等等。

Form.error_css_classForm.required_css_class属性就是做这个用的:

from django import forms

class ContactForm(forms.Form):
    error_css_class = 'error'
    required_css_class = 'required'

    # ... and the rest of your fields here

通过赋值不同的字符串,表示给这两类属性添加不同的CSS的class属性。以后Django在渲染form成HTML时将自动为error和required行添加对应的CSS样式。

上面的例子,其HTML看上去将类似:

>>> f = ContactForm(data)
>>> print(f.as_table())
<tr class="required"><th><label class="requiredfor="id_subject">Subject:</label>    ...
<tr class="required"><th><label class="requiredfor="id_message">Message:</label>    ...
<tr class="required error"><th><label class="requiredfor="id_sender">Sender:</label>      ...
<tr><th><label for="id_cc_myself">Cc myself:<label> ...
>>> f['subject'].label_tag()
<label class="requiredfor="id_subject">Subject:</label>
>>> f['subject'].label_tag(attrs={'class''foo'})
<label for="id_subject" class="foo required">Subject:</label>

也可以直接在html文件中编写css、js代码以为错误信息添加CSS样式。

将上传的文件绑定到表单

首先,为了上传文件,需要确保from元素定义enctype为"multipart/form-data":

<form enctype="multipart/form-data" method="post" action="/foo/">

其次,当使用表单时,需要绑定文件数据。文件数据的处理与普通的表单数据是分开的,所以如果表单包含FileField和ImageField,绑定表单时你需要指定第二个参数,参考下面的例子。

# 为表单绑定image字段
>>> from django.core.files.uploadedfile import SimpleUploadedFile
>>> data = {'subject''hello',
...         'message''Hi there',
...         'sender''foo@example.com',
...         'cc_myself'True}
>>> file_data = {'mugshot': SimpleUploadedFile('face.jpg', <file data>)}
>>> f = ContactFormWithMugshot(data, file_data)

实际上,一般使用request.FILES作为文件数据的源:

# Bound form with an image field, data from the request
>>> f = ContactFormWithMugshot(request.POST, request.FILES)

构造一个未绑定的表单和往常一样,将表单数据和文件数据同时省略:

# Unbound form with an image field
>>> f = ContactFormWithMugshot()

应用Bootstrap样式

Django form应用Bootstrap样式简单示例:

  <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
  <title>login</title>
</head>
<body>
<div class="container">
  <div class="row">
    <form action="/login2/" method="post" novalidate class="form-horizontal">
      {% csrf_token %}
      <div class="form-group">
        <label for="{{ form_obj.username.id_for_label }}"
               class="col-md-2 control-label">
{{ form_obj.username.label }}</label>
        <div class="col-md-10">
          {{ form_obj.username }}
          <span class="help-block">{{ form_obj.username.errors.0 }}</span>
        </div>
      </div>
      <div class="form-group">
        <label for="{{ form_obj.pwd.id_for_label }}" class="col-md-2 control-label">{{ form_obj.pwd.label }}</label>
        <div class="col-md-10">
          {{ form_obj.pwd }}
          <span class="help-block">{{ form_obj.pwd.errors.0 }}</span>
        </div>
      </div>
      <div class="form-group">
      <label class="col-md-2 control-label">{{ form_obj.gender.label }}</label>
        <div class="col-md-10">
          <div class="radio">
            {% for radio in form_obj.gender %}
              <label for="{{ radio.id_for_label }}">
                {{ radio.tag }}{{ radio.choice_label }}
              </label>
            {% endfor %}
          </div>
        </div>
      </div>
      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <button type="submit" class="btn btn-default">注册</button>
        </div>
      </div>
    </form>
  </div>
</div>

<script src="/static/jquery-3.2.1.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.min.js"></script>

批量添加样式

可通过重写form类的init方法来实现。

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required""不能为空",
            "invalid""格式错误",
            "min_length""用户名最短8位"
        }
    ...

    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        for field in iter(self.fields):
            self.fields[field].widget.attrs.update({
                'class''form-control'
            })
posted @ 2020-08-05 20:46  虫萧  阅读(308)  评论(0编辑  收藏  举报