25 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 = True
err_msg = ""
name = requests.POST.get('name')
password = requests.POST.get('password')
if name != 'spyinx' or password != '123456':
success = False
err_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: 5px;
margin-bottom: 5px;
height: 30px;
}
.input-red {
background-color: red
}
.color-red {
color: red;
font-size: 12px;
}
.checkbox {
font-size: 10px;
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: 214px"/></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=4,
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=6,
max_length=20,
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 = True
err_msg = ""
if form.is_valid():
login_data = form.clean()
name = login_data['name']
password = login_data['password']
if name != 'spyinx' or password != 'SPYinx123456':
success = False
err_msg = "使用者名稱密碼不正確"
else:
success = False
err_msg = form.errors['password'][0]
print(success, err_msg, form.errors)
return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
最後,新增相應的 URLConf 配置,如下:
# 程式碼位置:hello_app/urls.py
urlpatterns = [
# ...
# 表單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 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 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_bound
False
>>> login_bound.is_bound
True
這裡 Form 類提供了一個 is_bound 屬性去判斷這個 Form 例項是 bound 還是 unbound 的。可以看下 is_bound 的賦值程式碼就知道其含義了:
# 原始碼路徑: django/forms/forms.py
# ...
@html_safe
class 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_safe
class 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_safe
class 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.
return
self.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():
return
self._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] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except 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_safe
class BaseForm:
# ...
@property
def errors(self):
"""Return an ErrorDict for the data provided for the form."""
if self._errors is None:
self.full_clean()
return self._errors
def 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 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 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 1, in <module>
AttributeError: 'LoginForm' object has no attribute 'cleaned_data'
>>> login_bound.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/forms.py", line 430, in clean
return self.cleaned_data
AttributeError: 'LoginForm' object has no attribute 'cleaned_data'
看到了原始碼之後,我們要學會操作,學會分析一些現象,然後動手實踐,這樣才會對 Django 的原始碼越來越熟悉。
Form.errors:它是一個類屬性,儲存的是校驗錯誤的資訊。該屬性還有一些有用的方法,我們在下面實踐中熟悉:
(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 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_safe
class BaseForm:
# ...
@property
def 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_safe
class 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 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 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 0x7f8b3c081e20> <django.forms.fields.CharField object at 0x7f8b3c081cd0> <django.forms.fields.BooleanField object at 0x7f8b3c081910>
-
Form.cleaned_data:
Form
類中的每個欄位不僅可以驗證資料,還可以清理資料,形成統一的格式; -
Form.has_changed():檢查表單資料是否已從初始資料更改。
2.4 表單輸出
Form 物件的另一個作用是將自身轉為HTML。為此,我們只需簡單地使用 print()
方法就可以看到 From 物件的 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 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">7天自動登入:</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>賬號名最短4位</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">7天自動登入:</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">7天自動登入:</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_safe
class 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_classes
if 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_text
else:
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(0, 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[-1]
# 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[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
else:
# 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。