Django 表单使用-Field 使用

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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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 , in <module>
  File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py, line , in clean
    self.run_validators(value)
  File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py, line , in run_validatorsraise 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 valuedef 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:returnerrors = []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 , in <module>
  File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py, line , in clean
    self.validate(value)
  File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py, line , in validateraise 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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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=,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=,max_length=,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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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 shellPython . (default, Dec  , ::) [GCC .  (Red Hat .-)] 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 = CheckboxInputdef 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 = Falseelse: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 valuereturn self.to_python(initial) != self.to_python(data)

CharField:用的最多的,会被渲染成输入框,我们可以通过 widget 属性值控制输入框样式等。这在前面的登录表单例子中也是演示过的。

 name = forms.CharField(label=账号,min_length=,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_valuesuper().__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_valuereturn valuedef 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_lengthmin_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 类的父类,比如 RegexFieldEmailField 等。

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: px/></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=,choices=((, '普通用户'), (, '管理员'), (, '其他')),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 Noneif isinstance(value, datetime.datetime):return value.date()if isinstance(value, datetime.date):return valuereturn super().to_python(value)def strptime(self, value, format):return datetime.datetime.strptime(value, format).date()

DateTimeField:默认的小部件是 DateTimeInput,这里会校验对应的值是否是datetime.datetimedatetime.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 valuedef 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 Noneif 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 类。

EmailFieldEmailField 直接继承 CharField,它和 CharField 的一个主要区别就是多加了一个默认的校验器,主要校验输入的值是否是邮箱格式。其实现的代码如下:

class EmailField(CharField):widget = EmailInput
    default_validators = [validators.validate_email]def __init__(self, **kwargs):super().__init__(strip=True, **kwargs)

IntegerField:对应的小部件是 NumberInput,输入整数字符串。它可以输入 min_Valuemax_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_valueif kwargs.get('localize') and self.widget == NumberInput:# Localized number input is not well supported on most browserskwargs.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 Noneif 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 valuedef widget_attrs(self, widget):attrs = super().widget_attrs(widget)if isinstance(widget, NumberInput):if self.min_value is not None:attrs['min'] = self.min_valueif self.max_value is not None:attrs['max'] = self.max_valuereturn 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_placessuper().__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 Noneif 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 valuedef validate(self, value):super().validate(value)if value in self.empty_values:returnif 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 #20765step = str(Decimal().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 Noneif self.localize:value = formats.sanitize_separators(value)try:value = float(value)except (ValueError, TypeError):raise ValidationError(self.error_messages['invalid'], code='invalid')return valuedef validate(self, value):super().validate(value)if value in self.empty_values:returnif 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 类如 ImageFieldRegexField 就不一一描述了,具体可以参考官方文档以及相应的源码进行学习。

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_safeclass 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] 的执行结果是由下面两条语句得到:

# 得到对应fieldfield = self.fields[name]# 返回的结果field.get_bound_field(self, name)

有了这两个语句,我们可以在 Django  的 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 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 >>>> 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 表单更多的学习需要多多去官方上参考相关的文档。