1. 程式人生 > Django入門教學 >25 Django 表單使用-資料校驗與屬性方法

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_dataForm 類中的每個欄位不僅可以驗證資料,還可以清理資料,形成統一的格式;

  • 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。