26 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('[email protected]')
'[email protected]'
>>> 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 表單更多的學習需要多多去官方上參考相關的文件。