iT邦幫忙

0

Django學習紀錄 11.表單的驗證與模型化

之前有在用Django寫一些小網站,現在暑假想說再來複習一下之前買的這本書
https://ithelp.ithome.com.tw/upload/images/20190724/20118889bj9fH1vhuR.jpg
於是我就把它寫成一系列的文章,也方便查語法
而且因為這本書大概是2014年出的,如今Django也已經出到2.多版
有些內容也變得不再支援或適用,而且語法或許也改變了
所以我會以最新版的Python和Django來修正這本書的內容跟程式碼

目錄:django系列文章-Django學習紀錄

11. 表單的驗證與模型化

11.1 表單驗證

views.py

...
def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    error = False
    if request.POST:
        visitor = request.POST['visitor']
        content = request.POST['content']
        email = request.POST['email']
        date_time = timezone.localtime(timezone.now())  # 擷取現在時間
        error = any(not request.POST[k] for k in request.POST)
        if '@' not in email:
            error = True
        if not error:
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
    return render(request, 'comments.html', locals())
...

comments.html

...
    {% else %}
        <p>無評價</p>
    {% endif %}
    <br/><br/>
    {% if error %}
        <p style="color: red;">* 表單輸入不完整,請重新輸入</p>
    {% endif %}
    <form action="" method="post"> {% csrf_token %}
        <table>
...

一旦有欄位未填,error就會被設為True,並且不會新增資料
然後將錯誤訊息顯示給使用者
https://ithelp.ithome.com.tw/upload/images/20190720/20118889tAaLUPOZGS.png
接下來我們希望檢查更精確
views.py

...
def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    errors = []
    if request.POST:
        visitor = request.POST['visitor']
        content = request.POST['content']
        email = request.POST['email']
        date_time = timezone.localtime(timezone.now())  # 擷取現在時間
        if any(not request.POST[k] for k in request.POST):
            errors.append('* 有空白欄位,請不要留空')
        if '@' not in email:
            errors.append('* email格式不正確,請重新輸入')
        if not errors:
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
    return render(request, 'comments.html', locals())
...

comments.html

...
    {% else %}
        <p>無評價</p>
    {% endif %}
    <br/><br/>
    {% for e in errors %}
        <p style="color: red;">{{ e }}</p>
    {% endfor %}
    <form action="" method="post"> {% csrf_token %}
        <table>
...

https://ithelp.ithome.com.tw/upload/images/20190720/20118889m6dpmusHxS.png
如果用戶不小心忘記填某個資料,雖然我們給了用戶錯誤訊息
但是用戶已經填好的資料我們還是要讓用戶不需要再重新填
comments.html

...
    <table>
        <tr>
            <td> <label for="visitor">留言者:</label> </td>
            <td> <input id="visitor" type="text" name="visitor" value="{{visitor}}"> </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" value="{{content}}"></textarea>
            </td>
        </tr>
    </table>
...

如果表單順利填完送出資料,那就必須將表單內容清空
views.py

def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    errors = []
    if request.POST:
        visitor = request.POST['visitor']
        content = request.POST['content']
        email = request.POST['email']
        date_time = timezone.localtime(timezone.now())  # 擷取現在時間
        if any(not request.POST[k] for k in request.POST):
            errors.append('* 有空白欄位,請不要留空')
        if '@' not in email:
            errors.append('* email格式不正確,請重新輸入')
        if not errors:
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
            visitor, content, email = ('', '', '')
    return render(request, 'comments.html', locals())

我們先前在建立模型的時候,有些資料欄位會做blank=True的設定,那理所當然的,沒做該項設定的資料欄位應該會被列為必填,難道我們提交表單的時候,資料庫或Django不會抗議嗎?答案是:目前還不會
這是因為blank=True的設定是針對資料驗證而不是資料庫的欄位的描述。大家可以利用

python manage.py sqlmigrate APP_NAME

去看一次語法,除了多設定為NOT NULL外,Django並沒有對資料表做更多描述,那我們是白描述的嗎?當然不,還記得admin吧,必填欄位一定要輸入資料,不然會得到警告,admin怎麼會知道該欄位必填呢?忘記我們有註冊模型上去了嗎!(blank=True的用意便是在此)

11.2 表單模型化

在django中除了資料庫可以利用物件映射模型化之外,表單也可以

11.2.1 建立表單模型

restaurants/forms.py

from django import forms

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

required參數代表是不是必填
在這邊,變數設置的順序也很重要,會影響到預設輸出的順序,盡量得依照最後輸出到html上的順序來設置

11.2.2 操作表單模型

進入django shell

python manage.py shell
>>> from restaurants.forms import CommentForm
>>> f = CommentForm()
>>> print(f)
<tr><th><label for="id_visitor">Visitor:</label></th><td><input type="text" name="visitor" maxlength="20" required id="id_visitor"></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" maxlength="20" id="id_email"></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">
</textarea></td></tr>

我們建立了一個非綁定的表單f,所謂非綁定即一個未填資料的表單
如果用print將他列印,我們將會發現他以html表格(<table>)的方式輸出,我們稍微地來研究一下輸出的表單元件標籤屬性跟表單模型的變數有什麼關係
一個表單模型欄位會產生一個<label>元件和一個<input>元件
每個<input>元件會依據表單模型的Field類型來決定它的type屬性如:EmailField對映到type="email"
每個<input>會產生一個id屬性,值為 id_表單模型變數名稱
每個<input>會產生一個name屬性,值為表單模型變數名稱
另外一些描述性的參數也會加入如:max_length
其他輸出方法:

>>> f.as_p() # 段落輸出
'<p><label for="id_visitor">Visitor:</label> <input type="text" name="visitor" maxlength="20" required id="id_visitor"></p>\n<p><label for="id_email">Email:</label> <input type="email" name="email" maxlength="20" id="id_email"></p>\n<p><label for="id_content">Content:</label> <textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></p>'
>>> f.as_ul() # 列表輸出
'<li><label for="id_visitor">Visitor:</label> <input type="text" name="visitor" maxlength="20" required id="id_visitor"></li>\n<li><label for="id_email">Email:</label> <input type="email" name="email" maxlength="20" id="id_email"></li>\n<li><label for="id_content">Content:</label> <textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></li>'
>>> f.as_table() # 表格輸出
'<tr><th><label for="id_visitor">Visitor:</label></th><td><input type="text" name="visitor" maxlength="20" required id="id_visitor"></td></tr>\n<tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" maxlength="20" id="id_email"></td></tr>\n<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\n</textarea></td></tr>'

11.2.3 輸出表單

views.py

...
from restaurants.forms import CommentForm

def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    errors = []
    if request.POST:
        visitor = request.POST['visitor']
        content = request.POST['content']
        email = request.POST['email']
        date_time = timezone.localtime(timezone.now())  # 擷取現在時間
        if any(not request.POST[k] for k in request.POST):
            errors.append('* 有空白欄位,請不要留空')
        if '@' not in email:
            errors.append('* email格式不正確,請重新輸入')
        if not errors:
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
            visitor, content, email = ('', '', '')
    f = CommentForm()
    return render(request, 'comments.html', locals())
...

comments.html

...
    <form action="" method="post"> {% csrf_token %}
        <table>
            {{ f.as_table }}
        </table>
        <input type="submit" value="給予評價">
    </form>
...

結果
https://ithelp.ithome.com.tw/upload/images/20190720/201188894Kv52kPgFS.png
如果留空或是格式錯誤便會出現警告了
這是html‵<input>元件中email type的驗證幫助

11.2.4 表單的綁定與驗證

所謂綁定就是表單已輸入資料,與資料綁定
例如

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

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

  1. 表單均有輸入
>>> f = CommentForm({'visitor':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f.is_valid()
True
  1. 電子郵箱未填
>>> f = CommentForm({'visitor':'dokelung','content':'Good!'})
>>> f.is_valid()
True

因為required=False
3. 用戶名字未填

>>> f = CommentForm({'email':'dokelung@gmail.com','content':'Good!'})
>>> f.is_valid()
False

如果沒有放required這個參數,那麼預設會是True
4. 電子郵箱錯誤格式

>>> f = CommentForm({'visitor':'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['visitor'].errors
['This field is required.']
>>> f['email'].errors
['Enter a valid email address.']
>>> f.errors
{'visitor': ['This field is required.'], 'email': ['Enter a valid email address.']}

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

>>> f = CommentForm({'email':'dokelung','content':'Good!'})
>>> f.cleaned_data
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'CommentForm' object has no attribute 'cleaned_data'
>>> f.is_valid()
False
>>> f.cleaned_data
{'content': 'Good!'}

如果沒有呼叫is_valid或是存取errors字典過,就存取cleaned_data,則會出現錯誤

>>> f.as_table()
'<tr><th><label for="id_visitor">Visitor:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="visitor" maxlength="20"
required id="id_visitor"></td></tr>\n<tr><th><label for="id_email">Email:</label></th><td><ul class="errorlist"><li>Enter a valid email address.</li></ul><input type="email" name="email" value="dokelung" maxlength="20" id="id_email"></td></tr>\n<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" cols="40" rows="10" maxlength="200" required id="id_content">\nGood!</textarea></td></tr>'

合法的欄位會連帶連綁定的資料一起輸出(value屬性)
而不合法的欄位會出現一個<ul>列表,裡面一項一項列的是該欄位的錯誤訊息與提示
<ul>標籤的class屬性會設為errorlist,如果想要把錯誤訊息改為紅色
在html的<head>中加入CSS

<style>
    .errorlist{
        color: red;
    }
</style>

即可
我們放棄之前撰寫的驗證改用表單模型作驗證
views.py

...
def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if request.POST:
        f = CommentForm(request.POST)
        if f.is_valid():
            visitor = f.cleaned_data['visitor']
            content = f.cleaned_data['content']
            email = f.cleaned_data['email']
            date_time = timezone.localtime(timezone.now())  # 擷取現在時間
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
            visitor, content, email = ('', '', '')
            f = CommentForm()
    else:
        f = CommentForm()
    return render(request, 'comments.html', locals())
...

comments.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
            .errorlist{
                color: red;
            }
        </style>
    </head>
    <body>
        <h2>{{ r.name }}的評價</h2>
        {% if r.comment_set.all %}
            <p>目前共有{{ r.comment_set.all|length }}條評價</p>
            <table>
                <tr>
                    <th>留言者</th>
                    <th>時間</th>
                    <th>評價</th>
                </tr>
            {% for c in r.comment_set.all %}
                <tr>
                    <td> {{ c.visitor }} </td>
                    <td> {{ c.date_time | date:"F j, Y" }} </td>
                    <td> {{ c.content }} </td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <p>無評價</p>
        {% endif %}
        <br/><br/>
        {% if f.errors %}
        <p style="color:red;">
            請依提示修復表單錯誤
        </p>
        {% endif %}
        <form action="" method="post"> {% csrf_token %}
            <table>
                {{ f.as_table }}
            </table>
            <input type="submit" value="給予評價">
        </form>
    </body>
</html>

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

11.2.5 客製化的表單輸出

更換表單元件

表單文字類欄位預設是以<input>作為html元件,如果想要更改
那就可以在表單模型中設定widget參數
content欄位使用<textarea>而不是<input>

from django import forms

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

設定表單元件屬性

透過表單物件的參數來設定
例如max_length或是min_length

設定表單欄位初始值

利用initial參數

...
def comment(request, id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if request.POST:
        f = CommentForm(request.POST)
        if f.is_valid():
            visitor = f.cleaned_data['visitor']
            content = f.cleaned_data['content']
            email = f.cleaned_data['email']
            date_time = timezone.localtime(timezone.now())  # 擷取現在時間
            Comment.objects.create(visitor=visitor, email=email, content=content, date_time=date_time, restaurant=r)
            visitor, content, email = ('', '', '')
            f = CommentForm(initial={'content': '我沒意見'})
    else:
        f = CommentForm(initial={'content': '我沒意見'})
    return render(request, 'comments.html', locals())
...

https://ithelp.ithome.com.tw/upload/images/20190720/20118889eokSyQc9n8.png
要注意的是,這裡產生的表單物件雖有預設值(初始值),但並未被綁定,他們是無法被驗證的

自訂驗證規則

在表單模型中加入以clean_表單欄位名稱為名的驗證方法
它會在該欄位通過預設檢查後,透過此方法做自定義的檢查:
forms.py

from django import forms

class CommentForm(forms.Form):
    visitor = 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值,你也可以在這邊對數據做一些修改
https://ithelp.ithome.com.tw/upload/images/20190720/20118889vu5fH1mV7l.png
整理一下:
https://ithelp.ithome.com.tw/upload/images/20190720/20118889042uox2G63.png

設定欄位標籤名稱

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

email = forms.EmailField(max_length=20, required=False, label='E-mail')

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

一次輸出整個表單模型我們就無法做更動或調整
因此我們想要單獨輸出每一個元件

>>> from restaurants.forms import CommentForm
>>> f = CommentForm({'visitor':'dokelung','email':'dokelung@gmail.com','content':'Good!'})
>>> f['visitor']
<django.forms.boundfield.BoundField object at 0x03491B30>
>>> print(f['visitor'])
<input type="text" name="visitor" value="dokelung" maxlength="20" required id="id_visitor">

把表單物件當作字典,便可以分別輸出指定的欄位(不含<label>標籤)
這個字典的鍵值都是一個BoundField物件,包含了errors這個紀錄錯誤的屬性
接下來我們來對表單作修改
comments.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
            .errorlist{
                color: red;
            }
        </style>
    </head>
    <body>
        <h2>{{ r.name }}的評價</h2>
        {% if r.comment_set.all %}
            <p>目前共有{{ r.comment_set.all|length }}條評價</p>
            <table>
                <tr>
                    <th>留言者</th>
                    <th>時間</th>
                    <th>評價</th>
                </tr>
            {% for c in r.comment_set.all %}
                <tr>
                    <td> {{ c.visitor }} </td>
                    <td> {{ c.date_time | date:"F j, Y" }} </td>
                    <td> {{ c.content }} </td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <p>無評價</p>
        {% endif %}
        <br/><br/>
        {% if f.errors %}
        <p style="color:red;">
            請依提示修復表單錯誤
        </p>
        {% endif %}
        <form action="" method="post"> {% csrf_token %}
            <table>
                <tr>
                    <th> <label for="id_visitor">留言者:</label> </th>
                    <td> {{ f.visitor }} </td>
                    <td> {{ f.visitor.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="submit" value="給予評價">
        </form>
    </body>
</html>

結果
https://ithelp.ithome.com.tw/upload/images/20190720/201188890zjuL3jPwo.png

上一篇:Django學習紀錄 10.使用者互動與表單

下一篇:Django學習紀錄 12.Cookies與Sessions


尚未有邦友留言

立即登入留言