Django 表单使用-Field 使用
上一节我们主要介绍了 Django 中 Form 类的相关属性和方法,本小节中会继续介绍 Field 类的相关属性与方法,最后还有如何实现自定义的 Field。
1. Field 相关基础
1.1 Field 的 clean() 方法
通过上面两个例子演示,我们对 Django 中的表单应该有了初步的了解。对于 Form 类,最重要的就是定义它的字段(Field),且每个字段都有自定义验证逻辑以及其他一些钩子(hooks)。现在介绍以下 Field 类的一个重要方法: clean()
。这个方法传递一个参数,然后要么抛出异常,要么直接返回对应的值。我们现在 Django 的 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 django import forms
>>> f = forms.EmailField()
>>> f.clean('foo@example.com')
'foo@example.com'
>>> f.clean('invalid email address')
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py", line 150, in clean
self.run_validators(value)
File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py", line 141, in run_validators
raise ValidationError(errors)
django.core.exceptions.ValidationError: ['Enter a valid email address.']
可以看到,当我们输入了异常的邮件值的时候,调用 clean()
方法会抛出异常。我们可以看看 Django 的代码这个 clean()
方法做了哪些工作:
# 源码位置:django/forms/fields.py
# ...
class Field:
# ...
def to\_python(self, value):
return value
def validate(self, value):
if value in self.empty_values and self.required:
raise ValidationError(self.error_messages['required'], code='required')
def run\_validators(self, value):
if value in self.empty_values:
return
errors = []
for v in self.validators:
try:
v(value)
except ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages:
e.message = self.error_messages[e.code]
errors.extend(e.error_list)
if errors:
raise ValidationError(errors)
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
# ...
源码的逻辑很清晰,clean()
方法就是对输入的数据进行校验,当输入不符合该 Field 的要求时抛出异常,否则返回 value 值。接下来,继续介绍 Field 的一些核心参数。
1.2 Field 核心属性
前面的实验中我们用到的 django 的中的 CharField,并在初始化该 Field 示例时传递了一些参数,如 label、min_length 等。接下来,我们首先看看 Field 对象的一些核心属性:
Field.required:默认情况下,每个 Field 类会假定该 Field 的值时必须提供的,如果我们传递的时空值,无论是 None
还是空字符串(""
),在调用 Field 的 clean()
方法时就会抛出异常ValidationError
;
>>> from django import forms
>>> f = forms.CharField()
>>> f.clean('foo')
'foo'
>>> f.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/fields.py", line 149, in clean
self.validate(value)
File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py", line 127, in validate
raise ValidationError(self.error_messages['required'], code='required')
django.core.exceptions.ValidationError: ['This field is required.']
>>> f = forms.CharField(required=False)
>>> f.clean('')
''
Field.label:是给这个 field 一个标签名;
>>> from django import forms
>>> class CommentForm(forms.Form):
... name = forms.CharField(label='名称')
... url = forms.URLField(label='网站地址', required=False)
... comment = forms.CharField()
...
>>> f = CommentForm()
>>> print(f)
<tr><th><label for="id\_name">名称:</label></th><td><input type="text" name="name" required id="id\_name"></td></tr>
<tr><th><label for="id\_url">网站地址:</label></th><td><input type="url" name="url" id="id\_url"></td></tr>
<tr><th><label for="id\_comment">Comment:</label></th><td><input type="text" name="comment" required id="id\_comment"></td></tr>
可以看到,这个 label 参数最后在会变成 HTML 中的 <label>
元素。
Field.label_suffix:这个属性值是在 label 属性值后面统一加一个后缀。
(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 django import forms
>>> class CommentForm(forms.Form):
... name = forms.CharField(label='Your name')
... url = forms.URLField(label='网站地址', label_suffix='?', required=False)
... comment = forms.CharField()
...
>>> c
>>> print(f.as_p())
<p><label for="id\_name">Your name#</label> <input type="text" name="name" required id="id\_name"></p>
<p><label for="id\_url">网站地址?</label> <input type="url" name="url" id="id\_url"></p>
<p><label for="id\_comment">Comment#</label> <input type="text" name="comment" required id="id\_comment"></p>
>>>
注意:Form 也有 label_suffix 属性,会让所有字段都加上这个属性值。但是如果字段自身定义了这个属性值,则会覆盖全局的 label_suffix,正如上述测试的结果。
Field.initial:指定字段的初始值;
Field.widget:这个就是指定该 Field 转成 HTML 的标签,我们
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': '请输入登录账号'})
)
# ...
Field.help_text:给 Field 添加一个描述;
Field.error_messages:该 error_messages
参数可以覆盖由 Form 中对应字段引发错误的默认提示;
Field.validators:可以通过该参数自定义字段数据校验;下面看我们上一讲的实验2中自定义了一个简单的密码校验,如下:
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):
# ...
password = forms.CharField(
label="密码",
validators=[password_validate, ],
min_length=6,
max_length=20,
required=True,
error_messages={'required': '密码不能为空', "invalid": "密码需要包含大写、小写和数字", "min\_length": "密码最短8位", "max\_length": "密码最长20位"},
widget=forms.TextInput(attrs={'class': "input-text",'placeholder': '请输入密码', 'type': 'password'}),
help_text='密码必须包含大写、小写以及数字',
)
# ...
Field.disabled:如果为 True,那么该字段将禁止输入,会在对应生成的 input 标签中加上 disabled
属性
(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 django import forms
>>> class CommentForm(forms.Form):
... name = forms.CharField(label='Your name', disabled=True)
...
>>> f = CommentForm()
>>> print(f)
<tr><th><label for="id\_name">Your name:</label></th><td><input type="text" name="name" required disabled id="id\_name"></td></tr>
Field.widget:这个 widget 的中文翻译是 “小器物,小装置”,每种 Field 都有一个默认的 widget 属性值,Django 会根据它来将 Field 渲染成对应的 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 django.forms.fields import BooleanField,CharField,ChoiceField
>>> BooleanField.widget
<class 'django.forms.widgets.CheckboxInput'>
>>> CharField.widget
<class 'django.forms.widgets.TextInput'>
>>> ChoiceField.widget
<class 'django.forms.widgets.Select'>
2. Django 中的内置 Field
BooleanField:之前演示过,它会被渲染成前端的 checkbox 组件。从源码上看它似乎没有额外特殊的属性。主要就是继承了 Field 类,然后重写了 to_python()
等方法
class BooleanField(Field):
widget = CheckboxInput
def to\_python(self, value):
"""Return a Python boolean object."""
# Explicitly check for the string 'False', which is what a hidden field
# will submit for False. Also check for '0', since this is what
# RadioSelect will provide. Because bool("True") == bool('1') == True,
# we don't need to handle that explicitly.
if isinstance(value, str) and value.lower() in ('false', '0'):
value = False
else:
value = bool(value)
return super().to_python(value)
def validate(self, value):
if not value and self.required:
raise ValidationError(self.error_messages['required'], code='required')
def has\_changed(self, initial, data):
if self.disabled:
return False
# Sometimes data or initial may be a string equivalent of a boolean
# so we should run it through to\_python first to get a boolean value
return self.to_python(initial) != self.to_python(data)
CharField:用的最多的,会被渲染成输入框,我们可以通过 widget 属性值控制输入框样式等。这在前面的登录表单例子中也是演示过的。
name = forms.CharField(
label="账号",
min_length=4,
required=True,
error_messages={'required': '账号不能为空', "min\_length": "账号名最短4位"},
widget=forms.TextInput(attrs={'class': "input-text",
'placeholder': '请输入登录账号'})
)
class CharField(Field):
def \_\_init\_\_(self, \*, max_length=None, min_length=None, strip=True, empty_value='', \*\*kwargs):
self.max_length = max_length
self.min_length = min_length
self.strip = strip
self.empty_value = empty_value
super().__init__(\*\*kwargs)
if min_length is not None:
self.validators.append(validators.MinLengthValidator(int(min_length)))
if max_length is not None:
self.validators.append(validators.MaxLengthValidator(int(max_length)))
self.validators.append(validators.ProhibitNullCharactersValidator())
def to\_python(self, value):
"""Return a string."""
if value not in self.empty_values:
value = str(value)
# 是否去掉首尾空格
if self.strip:
value = value.strip()
if value in self.empty_values:
return self.empty_value
return value
def widget\_attrs(self, widget):
attrs = super().widget_attrs(widget)
if self.max_length is not None and not widget.is_hidden:
# The HTML attribute is maxlength, not max\_length.
attrs['maxlength'] = str(self.max_length)
if self.min_length is not None and not widget.is_hidden:
# The HTML attribute is minlength, not min\_length.
attrs['minlength'] = str(self.min_length)
return attrs
除了 Field 属性外,CharField 还有 max_length
和 min_length
等属性。这些也都会反映在渲染的 input 元素上,同时校验器也会添加该属性的校验:
if min_length is not None:
self.validators.append(validators.MinLengthValidator(int(min_length)))
if max_length is not None:
self.validators.append(validators.MaxLengthValidator(int(max_length)))
CharField 还是很多 Field 类的父类,比如 RegexField
、EmailField
等。
ChoiceField:前面我们在 widget 属性的小实验中看到了 ChoiceField 对应的 widget 属性值是 Select
类,也即对应 select
元素。我们继续使用前面的登录表单来演示下这个 ChoiceField 类。我们除了添加 Field 之外,也需要添加下前端代码,如下所示。
{% 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 }}
<!------- 新增login\_type字段的HTML ------------->
<div><span>{{ form.login_type.label }}:</span>{{ form.login_type }}
<!-------------------------------------------->
<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 %}
login_type = forms.ChoiceField(
label="账号类型",
required=True,
initial=1,
choices=((0, '普通用户'), (1, '管理员'), (2, '其他')),
error_messages={'required': '必选类型' },
widget=forms.Select(attrs={'class': "input-text"}),
)
效果图如下所示。可以看到,这里 initial
属性值表示最开始选中那个选项,而 choices
属性值是一个元组,表示多选框的显示 name 值和实际 value 值。
DateField:默认的小部件是 DateInput
。它有一个比较重要的属性:input_formats,用于将字符串转换为有效datetime.date
对象的格式列表。如果没有提供 input_formats 参数,则默认的格式为:
['%Y-%m-%d', # '2006-10-25'
'%m/%d/%Y', # '10/25/2006'
'%m/%d/%y'] # '10/25/06'
class DateField(BaseTemporalField):
widget = DateInput
input_formats = formats.get_format_lazy('DATE\_INPUT\_FORMATS')
default_error_messages = {
'invalid': _('Enter a valid date.'),
}
def to\_python(self, value):
"""
Validate that the input can be converted to a date. Return a Python
datetime.date object.
"""
if value in self.empty_values:
return None
if isinstance(value, datetime.datetime):
return value.date()
if isinstance(value, datetime.date):
return value
return super().to_python(value)
def strptime(self, value, format):
return datetime.datetime.strptime(value, format).date()
DateTimeField:默认的小部件是 DateTimeInput
,这里会校验对应的值是否是datetime.datetime
、 datetime.date
类型,或者按照 input_formats 参数格式化的字符串。同样,如果没有提供 input_formats 参数,则默认的格式为:
['%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
'%Y-%m-%d', # '2006-10-25'
'%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
'%m/%d/%Y %H:%M', # '10/25/2006 14:30'
'%m/%d/%Y', # '10/25/2006'
'%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59'
'%m/%d/%y %H:%M', # '10/25/06 14:30'
'%m/%d/%y'] # '10/25/06'
class DateTimeField(BaseTemporalField):
widget = DateTimeInput
input_formats = formats.get_format_lazy('DATETIME\_INPUT\_FORMATS')
default_error_messages = {
'invalid': _('Enter a valid date/time.'),
}
def prepare\_value(self, value):
if isinstance(value, datetime.datetime):
value = to_current_timezone(value)
return value
def to\_python(self, value):
"""
Validate that the input can be converted to a datetime. Return a
Python datetime.datetime object.
"""
if value in self.empty_values:
return None
if isinstance(value, datetime.datetime):
return from_current_timezone(value)
if isinstance(value, datetime.date):
result = datetime.datetime(value.year, value.month, value.day)
return from_current_timezone(result)
result = super().to_python(value)
return from_current_timezone(result)
def strptime(self, value, format):
return datetime.datetime.strptime(value, format)
这些类的定义都是比较简单的,都是基于 Field 类,有的基于 CharField 类等。后面我们会重点分析 Field 类。
EmailField:EmailField
直接继承 CharField
,它和 CharField
的一个主要区别就是多加了一个默认的校验器,主要校验输入的值是否是邮箱格式。其实现的代码如下:
class EmailField(CharField):
widget = EmailInput
default_validators = [validators.validate_email]
def \_\_init\_\_(self, \*\*kwargs):
super().__init__(strip=True, \*\*kwargs)
IntegerField:对应的小部件是 NumberInput
,输入整数字符串。它可以输入 min_Value
、max_value
等参数用于控制输入值的范围。其源码如下,和 CharFiled 类的代码比较类似。
class IntegerField(Field):
widget = NumberInput
default_error_messages = {
'invalid': _('Enter a whole number.'),
}
re_decimal = re.compile(r'\.0\*\s\*$')
def \_\_init\_\_(self, \*, max_value=None, min_value=None, \*\*kwargs):
self.max_value, self.min_value = max_value, min_value
if kwargs.get('localize') and self.widget == NumberInput:
# Localized number input is not well supported on most browsers
kwargs.setdefault('widget', super().widget)
super().__init__(\*\*kwargs)
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
def to\_python(self, value):
"""
Validate that int() can be called on the input. Return the result
of int() or None for empty values.
"""
value = super().to_python(value)
if value in self.empty_values:
return None
if self.localize:
value = formats.sanitize_separators(value)
# Strip trailing decimal and zeros.
try:
value = int(self.re_decimal.sub('', str(value)))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def widget\_attrs(self, widget):
attrs = super().widget_attrs(widget)
if isinstance(widget, NumberInput):
if self.min_value is not None:
attrs['min'] = self.min_value
if self.max_value is not None:
attrs['max'] = self.max_value
return attrs
对于 IntegerField 字段输入的值,看 to_python()
方法,首先对于 20.0
这样的形式会先去掉后面的 .0
,然后用 int()
方法强转,如果发生异常,那就表明该字段对应的值不是整数,然后可以抛出异常。
DecimalField:它继承自 IntegerField
,用于输入浮点数。它有如下几个重要参数:
- max_value: 最大值
- min_value: 最小值
- max_digits: 总长度
- decimal_places: 小数位数
来看看它的定义的代码,如下:
class DecimalField(IntegerField):
default_error_messages = {
'invalid': _('Enter a number.'),
}
def \_\_init\_\_(self, \*, max_value=None, min_value=None, max_digits=None, decimal_places=None, \*\*kwargs):
self.max_digits, self.decimal_places = max_digits, decimal_places
super().__init__(max_value=max_value, min_value=min_value, \*\*kwargs)
self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
def to\_python(self, value):
"""
Validate that the input is a decimal number. Return a Decimal
instance or None for empty values. Ensure that there are no more
than max\_digits in the number and no more than decimal\_places digits
after the decimal point.
"""
if value in self.empty_values:
return None
if self.localize:
value = formats.sanitize_separators(value)
value = str(value).strip()
try:
# 使用Decimal()方法转换类型
value = Decimal(value)
except DecimalException:
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def validate(self, value):
super().validate(value)
if value in self.empty_values:
return
if not value.is_finite():
raise ValidationError(self.error_messages['invalid'], code='invalid')
def widget\_attrs(self, widget):
attrs = super().widget_attrs(widget)
if isinstance(widget, NumberInput) and 'step' not in widget.attrs:
if self.decimal_places is not None:
# Use exponential notation for small values since they might
# be parsed as 0 otherwise. ref #20765
step = str(Decimal(1).scaleb(-self.decimal_places)).lower()
else:
step = 'any'
attrs.setdefault('step', step)
return attrs
可以看到在 to_python()
方法中,最后对该字段输入的值使用 Decimal()
方法进行类型转换 。
FloatField:用于渲染成一个只允许输入浮点数的输入框。它同样继承自 IntegerField
,因此它对应的小部件也是 NumberInput
。
class FloatField(IntegerField):
default_error_messages = {
'invalid': _('Enter a number.'),
}
def to\_python(self, value):
"""
Validate that float() can be called on the input. Return the result
of float() or None for empty values.
"""
value = super(IntegerField, self).to_python(value)
if value in self.empty_values:
return None
if self.localize:
value = formats.sanitize_separators(value)
try:
value = float(value)
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def validate(self, value):
super().validate(value)
if value in self.empty_values:
return
if not math.isfinite(value):
raise ValidationError(self.error_messages['invalid'], code='invalid')
def widget\_attrs(self, widget):
attrs = super().widget_attrs(widget)
if isinstance(widget, NumberInput) and 'step' not in widget.attrs:
attrs.setdefault('step', 'any')
return attrs
其余的 Field 类如 ImageField
,RegexField
就不一一描述了,具体可以参考官方文档以及相应的源码进行学习。
3. 上一节的思考题解答
记得上一节留的那个思考题吗?我们来认真解答下这个代码。其实那个翻译 Field 为 HTML 的核心代码就只有一句:bf = self[name]
。我们来详细分析这一行代码的背后,做了哪些事情。
def \_html\_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
# 遍历form中的所有field,生成对应的html文本
for name, field in self.fields.items():
# ...
# 最核心的一句
bf = self[name]
if bf.is_hidden:
# ...
hidden_fields.append(str(bf))
else:
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,
})
# ...
return mark_safe('\n'.join(output))
看到 bf = self[name]
这一句,我们第一反应应该时找 Form 类中定义的 __getitem__()
这个魔法函数,可以看到它的源码如下:
# 源码位置:django/forms/forms.py
# ...
@html_safe
class BaseForm:
# ...
def \_\_getitem\_\_(self, name):
"""Return a BoundField with the given name."""
try:
field = self.fields[name]
except KeyError:
raise KeyError(
"Key '%s' not found in '%s'. Choices are: %s." % (
name,
self.__class__.__name__,
', '.join(sorted(f for f in self.fields)),
)
)
if name not in self._bound_fields_cache:
self._bound_fields_cache[name] = field.get_bound_field(self, name)
return self._bound_fields_cache[name]
从这里我们可以知道,bf = self[name]
的执行结果是由下面两条语句得到:
# 得到对应field
field = self.fields[name]
# 返回的结果
field.get_bound_field(self, name)
有了这两个语句,我们可以在 Django 的 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 django import forms
>>> from hello_app.views import LoginForm
>>> login = LoginForm({'name': 'test1234', 'password': 'SPYinx1234', 'save\_login': False})
>>> bf = login['name']
>>> bf
<django.forms.boundfield.BoundField object at 0x7fd7ad9232e0>
>>> str(bf)
'<input type="text" name="name" value="test1234" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id\_name">'
>>> bf = login['password']
>>> str(bf)
'<input type="password" name="password" value="SPYinx1234" class="input-text" placeholder="请输入密码" maxlength="20" minlength="6" required id="id\_password">'
# 测试后面两条语句
>>> field = login.fields['name']
>>> bf = field.get_bound_field(login, 'name')
>>> print(bf)
<input type="text" name="name" value="test1234" class="input-text" placeholder="请输入登录账号" minlength="4" required id="id\_name">
最后想再继续追下去,弄清楚到底如何翻译成 HTML 代码的,就要继续学习 django/forms/boundfield.py
中的 BoundField
类了。这个就做为课后练习了。
4. 小结
本小节我们介绍了 Django 中 Field 类的相关参数及其含义。接下来我们详细介绍了 Django 为我们准备的内置 Field 并对部分 Field 演示了其前端效果。最后我们还对上一节留下的一个思考题进行了解答。Django Form 表单更多的学习需要多多去官方上参考相关的文档。