about 4 years ago

  • 表單驗證
  • 表單模型化
    • 建立表單模型
    • 輸出表單
    • 表單的綁定與驗證
    • 客製化的表單輸出
      • 更換表單元件
      • 設定表單元件屬性
      • 設定表單欄位初始值
      • 自定驗證規則
      • 設定欄位標籤名稱
      • 欄位個別輸出與表單樣式客製化

上一篇我們提到了表單的使用與提交,正常來說這樣的功能已經堪用了,但是,在真實的web應用中,不對表單進行驗證是不行的,未進行檢查的表單可能會為資料庫引進錯誤的資料,也有可能造成危險。

表單驗證

能夠進行表單驗證的方式有很多種,javascript就提供了一些不錯的方法,不過那畢竟是在用戶端進行的驗證,我們必須保證來到伺服器端的資料也是正確的,也就是說,我們希望在後端也進行驗證。我們先來試試看手動的進行一些驗證。假設我們的餐廳評價表單的每個欄位都是必填的:

我們修正views.py如下:

mysite/restaurants/views.py
...
def comment(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    error = False
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()
        if not user or not content or not email:
            error = True
        if not error:
            Comment.objects.create(user=user, email=email, content=content, 
            date_time=date_time, restaurant=r)
    return render_to_response('comments.html',locals())

我們多增加了一個error變數,一旦有欄位未填,error就會被設定為True,並且不會新增資料。同樣地我們也要將這個資訊回饋給使用者:

mysite/restaurants/templates/comments.html
...
        {% else %}
            <p>無評價</p>
        {% endif %}

        <br /><br />
        {% if error %}
            <p>* 表單輸入不完整,請重新輸入</p>
        {% endif %}

        <form action="" method="post">
            <table>
...

嗯,有個樣子了,但是我們也很care電子信箱欄位的正確性(你不會希望有人填了一個不是電子郵件的東西吧),於是我們又做了一些修改:

mysite/restaurants/views.py
...
    error = False
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()
        if not user or not content or not email:
            error = True
        if '@' not in email:
            error = True
        if not error:
            Comment.objects.create(user=user, email=email, content=content, 
            date_time=date_time, restaurant=r)
    return render_to_response('comments.html',locals())

這還是不夠完美,因為欄位未填跟email格式錯誤的回報訊息一樣!我們需要更精確的錯誤訊息,幫助用戶debug(誤):

mysite/restaurants/views.py
...
    errors = []
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()
        if not user or not content or not email:
            errors.append('* 有空白欄位,請不要留空')
        if '@' not in email:
            errors.append('* email格式不正確,請重新輸入')
        if not errors:
            Comment.objects.create(user=user, email=email, content=content, date_time=date_time, restaurant=r)
    return render_to_response('comments.html',locals())

我們改利用errors清單來記錄各種錯誤訊息,並且搭配相應的模版:

mysite/restaurants/templates/comments.html
...
        {% else %}
            <p>無評價</p>
        {% endif %}

        <br /><br />
        {% for e in errors %}
            {{ e }} <br />
        {% endfor %}

        <form action="" method="post">
            <table>
...

好多了,但我們還不太滿意,試想,用戶辛苦填了一大堆的評價,結果忘記填他的名字了,雖然你給了他正確的提示,但是難道他得自己重填那令人沮喪的評價欄位嗎?當然不,專業的工程師會幫他們把已填妥的表單復填:

mysite/restaurants/templates/comments.html
...
            <table>
                <tr>
                    <td> <label for="user">留言者:</label> </td>
                    <td> <input id="user" type="text" name="user" value="{{user}}"> </td>
                </tr>
                <tr>
                    <td> <label for="email">電子信箱:</label> </td>
                    <td> <input id="email" type="text" name="email" value="{{email}}"> </td>
                </tr>
                <tr>
                    <td> <label for="content">評價:</label> </td>
                    <td> 
                        <textarea id="content" rows="10" cols="48" name="content">{{content}}</textarea>
                    </td>
                </tr>
            </table>
...

當然如果表單順利填妥,我們必須將表單清空(內容就不復填了!):

mysite/restaurants/views.py
...
    errors = []
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()
        if not user or not content or not email:
            errors.append('* 有空白欄位,請不要留空')
        if '@' not in email:
            errors.append('*    email格式不正確,請重新輸入')
        if not errors:
            Comment.objects.create(user=user, email=email, content=content, 
            date_time=date_time, restaurant=r)
            user = ''
            content = ''
            email = ''
    return render_to_response('comments.html',locals())

幹的好,也許我們解決了大部分的問題了,但是...

如果現在有超級多的表單欄位呢 => 檢查空白的運算式會超長的,而且表單重填的工作量會超大
如果email格式除了@外還有很多要檢查的呢 => 寫那些檢查邏輯就飽了
如果除了email格式外還有其他格式要檢查呢 => ...
如果email是選填的 => ...

讀者可能心理有個疑問,我們先前在建立模型的時候,有些資料欄位會做blank=True的設定,那理所當然的,沒做該項設定的資料欄位應該會被列為必填,難道我們提交表單的時候,資料庫或Django不會抗議嗎?答案是:目前還不會。這是因為blank=True的設定是針對資料驗證而不是資料庫的欄位的描述。大家可以利用python manage.py sqlall 模型名稱去看一次語法,除了多設定為NOT NULL外,Django並沒有對資料表做更多描述,那我們是白描述的嗎?當然不,還記得admin吧,必填欄位一定要輸入資料,不然會得到警告,admin怎麼會知道該欄位必填呢?忘記我們有註冊模型上去了嗎!

表單的檢查到最後會變成壓垮我們的最後一根稻草,其實這種重要但是並非主要的工作應該交給Django來做,也就是我們接下來要介紹的:將表單模型化!

表單模型化

你沒聽錯,除了資料庫可以作物件映射將之模型化之外,表單也可以,透過操作python的class與object,我們就能操作表單的元件。

建立表單模型

我們在restaurants應用的目錄下新增一個forms.py的檔案,他負責處理該應用的表單。

mysite/restaurants/forms.py
from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200)

每個Form類型都繼承自forms.Form,在該class底下我們可以設定該表單所擁有的欄位,不同類型的欄位對應到forms中的不同型別,這邊跟資料庫模型的變數設定很像,只是要記得各欄位現在來自forms庫。除此之外,我們對每個欄位的要求限制可以用參數的方式描述,比如說最大長度max_length或是此欄為選填(非必填)required=False。在這邊,變數設置的順序也很重要,會影響到預設輸出的順序,盡量得依照最後輸出到html上的順序來設置。

操作表單模型

接著我們進入shell中來操作該表單類別:

$ python mangae.py shell
>>> from restaurants.forms import CommentForm
>>> f = CommentForm()
>>> print f
<tr><th><label for="id_user">User:</label></th><td><input id="id_user" maxlength="20" name="user" type="text" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input id="id_email" maxlength="20" name="email" type="email" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input id="id_content" maxlength="200" name="content" type="text" /></td></tr>

在這邊我們建立了一個非綁定的表單f,所謂非綁定即一個未填資料的表單。如果用print將他列印,我們將會發現他以html表格(<table>)的方式輸出,我們稍微地來研究一下輸出的表單元件標籤屬性跟表單模型的變數有什麼關係。

  1. 一個表單模型欄位會產生一個<label>元件和一個<input>元件
  2. <input>元件的會依據表單模型的Field類型來決定它的type屬性如:e.g. EmailField對映到type="email"
  3. 每個<input>會產生一個id屬性,值為 id_表單模型變數名稱
  4. 每個<input>會產生一個name屬性,值為表單模型變數名稱
  5. 另外一些描述性的參數也會加入如:max_length

其實表單模型輸出的方式不只<table>一種,它也支援了<p><ul>的輸出形式:

>>> f.as_p()
u'<p><label for="id_user">User:</label> <input id="id_user" maxlength="20" name="user" type="text" /></p>\n<p><label for="id_email">Email:</label> <input id="id_email" maxlength="20" name="email" type="email" /></p>\n<p><label for="id_content">Content:</label> <input id="id_content" maxlength="200" name="content" type="text" /></p>'
>>> f.as_ul()
u'<li><label for="id_user">User:</label> <input id="id_user" maxlength="20" name="user" type="text" /></li>\n<li><label for="id_email">Email:</label> <input id="id_email" maxlength="20" name="email" type="email" /></li>\n<li><label for="id_content">Content:</label> <input id="id_content" maxlength="200" name="content" type="text" /></li>'

Form物件支援的html輸出方法

- Form.as_table() => 表格輸出
- Form.as_p()     => 段落輸出
- Form.as_ul()    => 列表輸出

我們會發現table的輸出形式不包含<table>標籤,而ul的輸出形式不包含<ul>標籤,這代表我們有自己的彈性空間來添增更多的元件或條整。

輸出表單

這種html的輸出特性讓表單模型可以作為模版上的變量輸出,很輕易的便完成了表單,而且各項屬性都依我們想像的進行了設定。不囉嗦,我們馬上就來試試看:

mysite/restaurants/views.py
...
def comment(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()
        c = Comment(user=user, email=email, content=content, date_time=date_time, restaurant=r)
        c.save()
    f = CommentForm()
    return render_to_response('comments.html',locals())

views.py中我們先省略了手動驗證表單的動作,並且產生了一個非綁定的表單。接著我們撰寫它要使用的模版:

mysite/restaurants/templates/comments.html
...
        <form action="" method="post">
            {{ f.as_table }}
            <input type="hidden" name="ok" value="yes">
            <input type="submit" value="給予評價">
        </form>
...

如此簡單!如此簡潔!我們便將表格輕鬆輸出了。不過這邊也只是利用到一點點模型化的好處,接著我們將可以看到更多模型化後的威力。

表單的綁定與驗證

接著要談談如何利用模型話的表單來進行驗證,有些讀者可能已經發現了,即使我們還沒有加入驗證的功能,但是在電子郵箱的部份,如果格式錯誤,便會出現警告了!這是html的email type<input>給我們的驗證幫助,不過我們不能只依賴於此,我們得做更全面的檢查,首先我們先回到之前提到的綁定與未綁定的概念。

所謂綁定 => 表單已輸入資料、與資料綁定

我們先來個小小範例:

>>> from restaurants.forms import CommentForm
>>> f = CommentForm()
>>> f.is_bound
False
>>> f = CommentForm({'user':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f.is_bound
True

我們只要提供表單類別一個字典參數(該字典的鍵值對剛好對應到表單元件(欄位)的名稱與要填入的值),就可以將表單與資料綁定。透過表單物件的is_bound屬性,我們便可以得知它是否被綁定(已填入資料)。一個已綁定的表單物件,便可以進行驗證(未綁定的表單是不能進行驗證的,這很直覺,沒有填入資料的表單是不能判定對錯的)。接下來我們嘗試利用表單物件的is_valid方法對下列狀況進行驗證:

1. 表單均有輸入
2. 電子郵箱未填
3. 用戶名字未填
4. 電子郵箱錯誤格式
>>> f = CommentForm({'user':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f.is_valid()
True
>>> f = CommentForm({'user':'dokelung','content':'Good!'})
>>> f.is_valid()
True
>>> f = CommentForm({'email':'dokelung@gmail.com','content':'Good!'})
>>> f.is_valid()
False
>>> f = CommentForm({'user':'dokelung','email':'dokelung','content':'Good!'})
>>> f.is_valid()
False

太好了,每種情形都能夠順利的執行驗證且都能符合我們的判定標準,接下來的一個問題是:我們要如何根據不同的錯誤得到不同的警告呢?很簡單,每個表單物件都可以利用表單元件(欄位)的名稱分別獲取個欄位的資訊,其中的errors屬性,提供我們有關於錯誤的訊息:

>>> f = CommentForm({'user':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f['email'].errors
[] 
>>> f = CommentForm({'email':'dokelung','content':'Good!'})
>>> f['user'].errors
[u'This field is required.']
>>> f['email'].errors
[u'Enter a valid email address.']

errors是一個錯誤訊息清單,裡面包含了關於該欄位的所有錯誤訊息字串,如果我們想要一次獲取所有的錯誤,我們可以使用表單物件的errors字典:

>>> f = CommentForm({'email':'dokelung','content':'Good!'})
>>> f.errors()
{'email': [u'Enter a valid email address.'],
 'user': [u'This field is required.']}

最後,表單物件中的cleaned_data字典會提供給我們合法欄位(通過驗證的欄位)的數據,不過該屬性要在表單有進行過任何驗證手段後才存在(包含呼叫is_valid或是存取errors字典等),我們也順便看看一個綁定的表單會怎麼輸出:

>>> f = CommentForm({'email':'dokelung','content':'Good!'})
>>> f.cleaned_data
{'content': u'Good'}
>>> f.as_table
值太醜了,筆者幫大家排版整理如下(省略掉unicode字串的引號和換行符號,只顯示裡面的html內容)
<tr>
    <th>
        <label for="id_user">User:</label>
    </th>
    <td>
        <ul class="errorlist"><li>This field is required.</li></ul>
        <input id="id_user" maxlength="20" name="user" type="text" />
    </td>
</tr>

<tr>
    <th>
        <label for="id_email">Email:</label>
    </th>
    <td>
        <ul class="errorlist"><li>Enter a valid email address.</li></ul>
        <input id="id_email" maxlength="20" name="email" type="email" value="dokelung" />
    </td>
</tr>

<tr>
    <th>
        <label for="id_content">Content:</label>
    </th>
    <td>
        <input id="id_content" maxlength="200" name="content" type="text" value="Good!" />
    </td>
</tr>

我們發現合法的欄位會連帶連綁定的資料一起輸出:value屬性。不合法的欄位會出現一個<ul>列表,裡面一項一項列的是該欄位的錯誤訊息與提示,順帶一提,<ur>標籤的的class屬性會設為errorlist。

了解了上述的屬性與方法後,我們來增強我們的表單頁面,使得各欄位能夠進行驗證:

mysite/restaurants/views.py
...
def comment(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if 'ok' in request.POST:
        f = CommentForm(request.POST)
        if f.is_valid():
            user = f.cleaned_data['user']
            content = f.cleaned_data['content']
            email = f.cleaned_data['email']
            date_time = datetime.datetime.now()
            c = Comment(user=user, email=email, content=content, date_time=date_time, restaurant=r)
            c.save()
            f = CommentForm()
    else:
        f = CommentForm()
    return render_to_response('comments.html',locals())

我們分析一下代碼,當表單確定被提交後,我們會利用request.POST這個類字典當做CommentForm的字典參數產生一個表單物件,再透過is_valid方法檢查表單的正確性,如果正確,我們產生一個評價並存入資料庫,並且重設變量f為未綁定表單(空表單)。若不正確,變量f的各欄位依然會有原先填入的值。當然,如果表單位被提交,使用者將可以看到一個全新的空表單(未綁定表單)。

接著來看模版怎麼改:

mysite/restaurants/templates/comments.html
...
        {% if f.errors %}
        <p style="color:red;">
            Please correct the error{{ f.errors|pluralize }} below.
        </p>
        {% endif %}

        <form action="" method="post">
            {{ f.as_table }}
            <input type="hidden" name="ok" value="yes">
            <input type="submit" value="給予評價">
        </form>
...

pluralize是個過濾器,通常用來幫助單字的單複數顯示,像在本例中,錯誤數量若只有1,Please correct the error below.中的error不會加s,反之則會。

我們在最上方去檢查了表單f的errors屬性,若有錯誤則提示用戶要更正下列提到的錯誤。其次要注意的是,若表單有不正確的欄位被提交,新產生的頁面中會出現錯誤訊息清單,這會破壞預設的表單樣式(其實就是有點醜...),我們將在下一小節提到調整的方法
,而這小節有一點雜,建議讀者多做試驗,反覆體會。

客製化的表單輸出

你對表單元件預設的行為或屬性不滿意嗎!別擔心,所有的一切我們都有辦法客製化,本節會提供許多自定功能或改變預設行為的方法,讀者可以根據自己的web app調整出最適當的表格。

更換表單元件

表單物件是預設以<input>作為html元件的,如果我們想要更動這項設定,可以在表單模型中以參數widget來達成,比如說我們的content欄位想要用<textarea>而不是用預設的<input>,我們可以更動表單模型如下:

mysite/restaurants/forms.py
from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200, widget=forms.Textarea)

我們透過widget參數設定其值為forms.Textarea,這將使得該文字輸入使用<textarea>元件而非<input>元件。這個設計良好的分離了視圖邏輯(widget參數指定用何種html元件呈現)與驗證邏輯(使用了CharField來驗證文字輸入)。

設定表單元件屬性

表單元件的屬性可以透過表單物件的參數來設定,比如說之前我們就用到的max_length參數或是min_length參數都是可設定的。

設定表單欄位初始值

我們可以用initial這個參數來生成具有初始值的表單物件:

mysite/restaurants/views.py
...
def comment(request,id):
    ...
    if 'ok' in request.POST:
        f = CommentForm(request.POST)
        if f.is_valid():
            user = f.cleaned_data['user']
            content = f.cleaned_data['content']
            email = f.cleaned_data['email']
            date_time = datetime.datetime.now()
            c = Comment(user=user, email=email, content=content, date_time=date_time, restaurant=r)
            c.save()
            f = CommentForm(initial={'content':'我沒意見'})
    else:
        f = CommentForm(initial={'content':'我沒意見'})
    return render_to_response('comments.html',locals())

我果我們打開評價頁面,會發現content欄位已經有:我沒意見 的評價存在了,這是因為我們有用字典設定了initial參數,這邊有一點要注意,這裡產生的表單物件雖有預設值(初始值),但並未被綁定,他們是無法被驗證的。

自定驗證規則

隨然表單中的各種欄位已經提供了預設的驗證規則,但畢竟不能滿足每一個人的需要,難道我們要回到老路,在視圖函數中撰寫一大堆檢查嗎?當然不,我們既然已經看到驗證這個工作可以移到表單模型上了,這種驗證邏輯與業務邏輯的分離應該是我們需要維持的,更何況我們希望維持一個統一的錯誤警告模式,解決這個狀況的方法就是在表單模型中加入以clean_表單欄位名稱為名的驗證方法:

mysite/restaurants/forms.py
# -*- coding: utf-8 -*-

from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200, widget=forms.Textarea)

    def clean_content(self):
        content = self.cleaned_data['content']
        if len(content) < 5:
            raise forms.ValidationError('字數不足')
        return content

我們來看看這個方法的運作程序,首先我們從表單物件中拿出content欄位的cleaned_data,這點不用覺得奇怪,我們已經經過了基本的驗證(CharField的驗證,包含欄位不能為空等等),所以這邊自然可以拿到cleaned_data,否則會在更早的地方便知道錯誤,便也不會進行本項檢查了。

接著我們將已經進行完基本檢查的乾淨content數據從cleaned_data中拿出來,並且用len計算字數,小於5字我們便引發一個ValidationError例外,而使用的字串參數將會成為表單欄位驗證錯誤的提示。如果字數足夠,我們會回傳content作為驗證後的表單值(就是形成最新的cleaned_data值,其實你也可以在這邊更動表單數據,比如說always回傳好好吃XD)。

記得,由於裡面使用到了中文,我們要在最上方加入utf-8的使用語句。

為了讓不太清楚的讀者有清楚概念,資料經過驗證和清理的流程如下:

                                                               驗證成功 --> 回傳處理過的cleaned_data
                                                              /
                                驗證成功 --> 根據clean_方法驗證
                              /                               \
原始data --> 根據欄位進行基本驗證                                驗證失敗 --> 引發例外
            (如CharField)     \
                               \
                                  驗證失敗 --> 引發例外,沒有cleaned_data                                           

設定欄位標籤名稱

我們提過,每個表單元件我們都會附上一個<label>標籤,他預設顯示的字串是空格取代底線,第一個字母大寫。沒錯!就跟資料模型在admin中顯示欄位名稱的規則一樣。若想要自定<label>標籤的內容,可以透過在表單模型中個欄位的參數label來指定:

mysite/restaurants
...
class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False, label='E-mail')
    content = forms.CharField(max_length=200, widget=forms.Textarea)
...

欄位個別輸出與表單樣式客製化

介紹到這裡,我想對於頁面的顯示上和一些細部的調整上還不能解決我們的需求,因為我們發現一次輸出整個表單模型會導致我們無法在表單模型的欄位之間做更動或是調整(之外當然是沒問題啦),那我們能不能單獨輸出每一個表單欄位(元件)呢?當然是可以,請看下面範例:

>>> f = CommentForm({'user':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f['user']
<django.forms.forms.BoundField at 0x10400fe90>
>>> print f['user']
<input id="id_user" maxlength="20" name="user" type="text" value="dokelung" />

我們發現,把表單物件當做字典,可以分別輸出指定的欄位(不含label),這個字典的每一個鍵值都是一個BoundField物件,包含了errors這個記錄錯誤的清單屬性。一旦我們有能力單獨輸出欄位了,我們也就能客製化地對表單進行調整。現在我們便來進行一次對表單的整頓:

mysite/restaurants/templates/comments.html
...
        <form action="" method="post">
            <table>
                <tr>
                    <th> <label for="id_user">留言者:</label> </th>
                    <td> {{ f.user  }} </td>
                    <td> {{ f.user.errors }} </td>
                </tr>
                <tr>
                    <th> <label for="id_email">電子信箱:</label> </th>
                    <td> {{ f.email }} </td>
                    <td> {{ f.email.errors }} </td>
                </tr>
                <tr>
                    <th> <label for="id_content">評價:</label> </th>
                    <td> {{ f.content }} </td>
                    <td> {{ f.content.errors }} </td>
                </tr>
            </table>
            <input type="hidden" name="ok" value="yes">
            <input type="submit" value="給予評價">
        </form>
...

本篇描述了表單的模型化手法,如果讀者仔細思考便能發現,表單模型的用途大致上可以分成輸出html表單元件和驗證表單,其實我們能夠只使用一邊的功能或是兩者皆用,或是加入一些自定義或客製化的行為。表單物件這能減少不少我們的工作量,也大幅提昇網站的安全性跟正確性,這不只是方便而已,讀者們也應體會物件導向、邏輯分離帶來的威力。

接下來我們會在下一篇介紹cookie和session兩種提供跨頁面資料的機制用以作為更進階功能的基礎,接著會引入非匿名的網頁機制-用戶,我們將說明如何將web應用與個人帳戶進行綁定,以提供相應的服務與功能,最重要的是這種個人化網頁的註冊、登入與登出也會在下下一篇說明。

← Django筆記(7) - 使用者互動與表單 Django筆記(9) - Cookies 與 Sessions →
 
comments powered by Disqus