第七章 表單

第七章 表單

從Google的簡樸的單個搜索框,到常見的Blog評論提交表單,再到複雜的自定義數據輸入接 口,HTML表單一直是交互性網站的支柱。 本章介紹如何用Django對用戶經過表單提交的數據進行訪問、有效性檢查以及其它處理。 與此同時,對象和Form對象。咱們將介紹HttpRequestcss

從Request對象中獲取數據

咱們在第三章講述View的函數時已經介紹過HttpRequest對象了,但當時並無講太多。 讓咱們回憶下:每一個view函數的第一個參數是一個HttpRequest對象,就像下面這個hello()函數:html

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello world")

HttpRequest對象,好比上面代碼裏的request變量,會有一些有趣的、你必須讓本身熟悉的 屬性和方法,以便知道能拿它們來作些什麼。 在view函數的執行過程當中,你能夠用這些屬性來獲取當前request的一些信息(好比,你正在加載這個頁面的用戶是誰,或者用的是什麼瀏覽器)。前端

URL相關信息

HttpRequest對象包含當前請求URL的一些信息:web

屬性/方法 說明 舉例
request.path 除域名之外的請求路徑,以正斜槓開頭 "/hello/"
request.get_host() 主機名(好比,一般所說的域名) "127.0.0.1:8000" or "www.example.com"
request.get_full_path() 請求路徑,可能包含查詢字符串 "/hello/?print=true"
request.is_secure() 若是經過HTTPS訪問,則此方法返回True, 不然返回False True 或者 False

在view函數裏,要始終用這個屬性或方法來獲得URL,而不要手動輸入。 這會使得代碼更加靈活,以便在其它地方重用。 下面是一個簡單的例子:算法

# BAD!
def current_url_view_bad(request):
    return HttpResponse("Welcome to the page at /current/")

# GOOD
def current_url_view_good(request):
    return HttpResponse("Welcome to the page at %s" % request.path)

有關request的其它信息

request.META 是一個Python字典,包含了全部本次HTTP請求的Header信息,好比用戶IP地址和用戶Agent(一般是瀏覽器的名稱和版本號)。 注意,Header信息的完整列表取決於用戶所發送的Header信息和服務器端設置的Header信息。 這個字典中幾個常見的鍵值有:數據庫

  • HTTP_REFERER,進站前連接網頁,若是有的話。 (請注意,它是REFERRER的筆誤。)django

  • HTTP_USER_AGENT,用戶瀏覽器的user-agent字符串,若是有的話。 例如: "Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17" .瀏覽器

  • REMOTE_ADDR 客戶端IP,如:"12.345.67.89" 。(若是申請是通過代理服務器的話,那麼它多是以逗號分割的多個IP地址,如:"12.345.67.89,23.456.78.90" 。)服務器

注意,由於 request.META 是一個普通的Python字典,所以當你試圖訪問一個不存在的鍵時,會觸發一個KeyError異常。 (HTTP header信息是由用戶的瀏覽器所提交的、不該該給予信任的「額外」數據,所以你老是應該好好設計你的應用以便當一個特定的Header數據不存在時, 給出一個優雅的迴應。)你應該用 try/except 語句,或者用Python字典的 get() 方法來處理這些「可能不存在的鍵」:app

# BAD!
def ua_display_bad(request):
    ua = request.META['HTTP_USER_AGENT']  # Might raise KeyError!
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 1)
def ua_display_good1(request):
    try:
        ua = request.META['HTTP_USER_AGENT']
    except KeyError:
        ua = 'unknown'
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 2)
def ua_display_good2(request):
    ua = request.META.get('HTTP_USER_AGENT', 'unknown')
    return HttpResponse("Your browser is %s" % ua)

咱們鼓勵你動手寫一個簡單的view函數來顯示 request.META 的全部數據,這樣你就知道里面有什麼了。 這個view函數多是這樣的:

def display_meta(request):
    values = request.META.items()
    values.sort()
    html = []
    for k, v in values:
        html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
    return HttpResponse('<table>%s</table>' % '\n'.join(html))

作爲一個練習,看你本身能不能把上面這個view函數改用Django模板系統來實現,而不是上面這樣來手動輸入HTML代碼。 也能夠試着把前面提到的 request.path 方法或 HttpRequest 對象的其它方法加進去。

提交的數據信息

除了基本的元數據,HttpRequest對象還有兩個屬性包含了用戶所提交的信息: request.GET 和 request.POST。兩者都是類字典對象,你能夠經過它們來訪問GET和POST數據。

類字典對象

咱們說「request.GET和request.POST是類字典對象」,意思是他們的行爲像 Python裏標準的字典對象,但在技術底層上他們不是標準字典對象。 好比說,request.GET和request.POST都有get()、keys()和values()方法,你能夠用用 for key in request.GET 獲取全部的鍵。

那到底有什麼區別呢? 由於request.GET和request.POST擁有一些普通的字典對象所沒有的方法。 咱們會稍後講到。

你可能之前遇到過類似的名字:類文件對象,這些Python對象有一些基本的方法,如read(),用來作真正的Python文件對象的代用品。

POST數據是來自HTML中的〈form〉標籤提交的,而GET數據可能來自〈form〉提交也多是URL中的查詢字符串(the query string)。

一個簡單的表單處理示例

繼續本書一直進行的關於書籍、做者、出版社的例子,咱們如今來建立一個簡單的view函數以便讓用戶能夠經過書名從數據庫中查找書籍。

一般,表單開發分爲兩個部分: 前端HTML頁面用戶接口和後臺view函數對所提交數據的處理過程。 第一部分很簡單;如今咱們來創建個view來顯示一個搜索表單:

from django.shortcuts import render_to_response

def search_form(request):
    return render_to_response('search_form.html')

在第三章已經學過,這個view函數能夠放到Python的搜索路徑的任何位置。 爲了便於討論,我們將它放在 books/views.py 裏。

這個 search_form.html 模板,可能看起來是這樣的:

<html>
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

而 urls.py 中的 URLpattern 多是這樣的:

from mysite.books import views

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    # ...
)

(注意,咱們直接將views模塊import進來了,而不是用相似 from mysite.views import search_form 這樣的語句,由於前者看起來更簡潔。 咱們將在第8章講述更多的關於import的用法。)

如今,若是你運行 runserver 命令,而後訪問http://127.0.0.1:8000/search-form/,你會看到搜索界面。 很是簡單。

不過,當你經過這個form提交數據時,你會獲得一個Django 404錯誤。 這個Form指向的URL /search/ 尚未被實現。 讓咱們添加第二個視圖函數並設置URL:

# urls.py

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    (r'^search/$', views.search),
    # ...
)

# views.py

def search(request):
    if 'q' in request.GET:
        message = 'You searched for: %r' % request.GET['q']
    else:
        message = 'You submitted an empty form.'
    return HttpResponse(message)

暫時先只顯示用戶搜索的字詞,以肯定搜索數據被正確地提交給了Django,這樣你就會知道搜索數據是如何在這個系統中傳遞的。 簡而言之:

  1. 在HTML裏咱們定義了一個變量q。當提交表單時,變量q的值經過GET(method=」get」)附加在URL /search/上。

  2. 處理/search/(search())的視圖經過request.GET來獲取q的值。

須要注意的是在這裏明確地判斷q是否包含在request.GET中。就像上面request.META小節裏面提到,對於用戶提交過來的數據,甚至是正確的數據,都須要進行過濾。 在這裏若沒有進行檢測,那麼用戶提交一個空的表單將引起KeyError異常:

# BAD!
def bad_search(request):
    # The following line will raise KeyError if 'q' hasn't
    # been submitted!
    message = 'You searched for: %r' % request.GET['q']
    return HttpResponse(message)

查詢字符串參數

由於使用GET方法的數據是經過查詢字符串的方式傳遞的(例如/search/?q=django), 因此咱們可使用requet.GET來獲取這些數據。 第三章介紹Django的URLconf系統時咱們比較了Django的簡潔的URL與PHP/Java傳統的URL,咱們提到將在第七章講述如何使用傳 統的URL。經過剛纔的介紹,咱們知道在視圖裏可使用request.GET來獲取傳統URL裏的查詢字符串(例如hours=3)。

獲取使用POST方法的數據與GET的類似,只是使用request.POST代替了 request.GET。那麼,POST與GET之間有什麼不一樣?當咱們提交表單僅僅須要獲取數據時就能夠用GET; 而當咱們提交表單時須要更改服務器數據的狀態,或者說發送e-mail,或者其餘不只僅是獲取並顯示數據的時候就使用POST。 在這個搜索書籍的例子裏,咱們使用GET,由於這個查詢不會更改服務器數據的狀態。 (若是你有興趣瞭解更多關於GETPOST的知識,能夠參見http://www.w3.org/2001/tag/doc/whenToUseGet.html。)

既然已經確認用戶所提交的數據是有效的,那麼接下來就能夠從數據庫中查詢這個有效的數據(一樣,在views.py裏操做):

from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')

讓咱們來分析一下上面的代碼:

除了檢查q是否存在於request.GET以外,咱們還檢查來reuqest.GET[‘q’]的值是否爲空。

咱們使用Book.objects.filter(title__icontains=q)獲取數據庫中標題包含q的書籍。 icontains是一個查詢關鍵字(參看第五章和附錄B)。這個語句能夠理解爲獲取標題裏包含q的書籍,不區分大小寫。

這是實現書籍查詢的一個很簡單的方法。 咱們不推薦在一個包含大量產品的數據庫中使用icontains查詢,由於那會很慢。 (在真實的案例中,咱們可使用以某種分類的自定義查詢系統。 在網上搜索「開源 全文搜索」看看是否有好的方法)

最後,咱們給模板傳遞來books,一個包含Book對象的列表。 查詢結果的顯示模板search_results.html以下所示:

<p>You searched for: <strong>{{ query }}</strong></p>

{% if books %}
    <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
    <ul>
        {% for book in books %}
        <li>{{ book.title }}</li>
        {% endfor %}
    </ul>
{% else %}
    <p>No books matched your search criteria.</p>
{% endif %}

注意這裏pluralize的使用,這個過濾器在適當的時候會輸出s(例如找到多本書籍)。

改進表單

同上一章同樣,咱們先從最爲簡單、有效的例子開始。 如今咱們再來找出這個簡單的例子中的不足,而後改進他們。

首先,search()視圖對於空字符串的處理至關薄弱——僅顯示一條」Please submit a search term.」的提示信息。 若用戶要從新填寫表單必須自行點擊「後退」按鈕, 這種作法既糟糕又不專業。若是在現實的案例中,咱們這樣子編寫,那麼Django的優點將蕩然無存。

在檢測到空字符串時更好的解決方法是從新顯示錶單,並在表單上面給出錯誤提示以便用戶馬上從新填寫。 最簡單的實現方法既是添加else分句從新顯示錶單,代碼以下:

from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search_form(request):
    return render_to_response('search_form.html')

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
        **return render_to_response('search_form.html', {'error': True})**

(注意,將search_form()視圖也包含進來以便查看)

這段代碼裏,咱們改進來search()視圖:在字符串爲空時從新顯示search_form.html。 而且給這個模板傳遞了一個變量error,記錄着錯誤提示信息。 如今咱們編輯一下search_form.html,檢測變量error:

<html>
<head>
    <title>Search</title>
</head>
<body>
    **{% if error %}**
        **<p style="color: red;">Please submit a search term.</p>**
    **{% endif %}**
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

咱們修改了search_form()視圖所使用的模板,由於search_form()視圖沒有傳遞error變量,因此在條用search_form視圖時不會顯示錯誤信息。

經過上面的一些修改,如今程序變的好多了,可是如今出現一個問題: 是否有必要專門編寫search_form()來顯示錶單? 按實際狀況來講,當一個請求發送至/search/(未包含GET的數據)後將會顯示一個空的表單(帶有錯誤信息)。 因此,只要咱們改變search()視圖:當用戶訪問/search/並未提交任何數據時就隱藏錯誤信息,這樣就移去search_form()視圖以及 對應的URLpattern。

def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})

在改進後的視圖中,若用戶訪問/search/而且沒有帶有GET數據,那麼他將看到一個沒有錯誤信息的表單; 若是用戶提交了一個空表單,那麼它將看到錯誤提示信息,還有表單; 最後,若用戶提交了一個非空的值,那麼他將看到搜索結果。

最後,咱們再稍微改進一下這個表單,去掉冗餘的部分。 既然已經將兩個視圖與URLs合併起來,/search/視圖管理着表單的顯示以及結果的顯示,那麼在search_form.html裏表單的action值就沒有必要硬編碼的指定URL。 原先的代碼是這樣:

<form action="/search/" method="get">

如今改爲這樣:

<form action="" method="get">

action=」「意味着表單將提交給與當前頁面相同的URL。 這樣修改以後,若是search()視圖不指向其它頁面的話,你將沒必要再修改action

簡單的驗證

咱們的搜索示例仍然至關地簡單,特別從數據驗證方面來說;咱們僅僅只驗證搜索關鍵值是否爲空。 而後許多HTML表單包含着比檢測值是否爲空更爲複雜的驗證。 咱們都有在網站上見過相似如下的錯誤提示信息:

  • 請輸入一個有效的email地址, foo’ 並非一個有效的e-mail地址。

  • 請輸入5位數的U.S 郵政編碼, 123並不是是一個有效的郵政編碼。

  • 請輸入YYYY-MM-DD格式的日期。

  • 請輸入8位數以上並至少包含一個數字的密碼。

關於JavaScript驗證

可使用Javascript在客戶端瀏覽器裏對數據進行驗證,這些知識已超出本書範圍。 要注意: 即便在客戶端已經作了驗證,可是服務器端仍必須再驗證一次。 由於有些用戶會將JavaScript關閉掉,而且還有一些懷有惡意的用戶會嘗試提交非法的數據來探測是否有能夠攻擊的機會。

除了在服務器端對用戶提交的數據進行驗證(例如在視圖裏驗證),咱們沒有其餘辦法。 JavaScript驗證能夠看做是額外的功能,但不能做爲惟一的驗證功能。

咱們來調整一下search()視圖,讓她可以驗證搜索關鍵詞是否小於或等於20個字符。 (爲來讓例子更爲顯著,咱們假設若是關鍵詞超過20個字符將致使查詢十分緩慢)。那麼該如何實現呢? 最簡單的方式就是將邏輯處理直接嵌入到視圖裏,就像這樣:

def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        **elif len(q) > 20:**
            **error = True**
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})

如今,若是嘗試着提交一個超過20個字符的搜索關鍵詞,系統不會執行搜索操做,而是顯示一條錯誤提示 信息。 可是,search_form.html裏的這條提示信息是:」Please submit a search term.」,這顯然是錯誤的, 因此咱們須要更精確的提示信息:

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term 20 characters or shorter.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

但像這樣修改以後仍有一些問題。 咱們包含萬象的提示信息很容易令人產生困惑: 提交一個空表單怎麼會出現一個關於20個字符限制的提示? 因此,提示信息必須是詳細的,明確的,不會產生疑議。

問題的實質在於咱們只使用來一個布爾類型的變量來檢測是否出錯,而不是使用一個列表來記錄相應的錯誤信息。 咱們須要作以下的調整:

def search(request):
    **errors = []**
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            **errors.append('Enter a search term.')**
        elif len(q) > 20:
            **errors.append('Please enter at most 20 characters.')**
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {**'errors': errors** })

接着,咱們要修改一下search_form.html模板,如今須要顯示一個errors列表而不是一個布爾判斷。

<html>
<head>
    <title>Search</title>
</head>
<body>
    **{% if errors %}**
        **<ul>**
            **{% for error in errors %}**
            **<li>{{ error }}</li>**
            **{% endfor %}**
        **</ul>**
    **{% endif %}**
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

編寫Contact表單

雖然咱們一直使用書籍搜索的示例表單,並將起改進的很完美,可是這仍是至關的簡陋: 只包含一個字段,q。這簡單的例子,咱們不須要使用Django表單庫來處理。 可是複雜一點的表單就須要多方面的處理,咱們如今來一下一個較爲複雜的例子: 站點聯繫表單。

這個表單包括用戶提交的反饋信息,一個可選的e-mail回信地址。 當這個表單提交而且數據經過驗證後,系統將自動發送一封包含題用戶提交的信息的e-mail給站點工做人員。

咱們從contact_form.html模板入手:

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject"></p>
        <p>Your e-mail (optional): <input type="text" name="email"></p>
        <p>Message: <textarea name="message" rows="10" cols="50"></textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

咱們定義了三個字段: 主題,e-mail和反饋信息。 除了e-mail字段爲可選,其餘兩個字段都是必填項。 注意,這裏咱們使用method=」post」而非method=」get」,由於這個表單會有一個服務器端的操做:發送一封e-mail。 而且,咱們複製了前一個模板search_form.html中錯誤信息顯示的代碼。

若是咱們順着上一節編寫search()視圖的思路,那麼一個contact()視圖代碼應該像這樣:

from django.core.mail import send_mail
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response

def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html',
        {'errors': errors})

(若是按照書中的示例作下來,這這裏可能乎產生一個疑問:contact()視圖是否要放在 books/views.py這個文件裏。 可是contact()視圖與books應用沒有任何關聯,那麼這個視圖應該能夠放在別的地方? 這毫無緊要,只要在URLconf里正確設置URL與視圖之間的映射,Django會正確處理的。 筆者我的喜歡建立一個contact的文件夾,與books文件夾同級。這個文件夾中包括空的__init__.py和views.py兩個文件。

如今來分析一下以上的代碼:

確認request.method的值是’POST’。用戶瀏覽表單時這個值並不存在,當且僅當表單 被提交時這個值纔出現。 (在後面的例子中,request.method將會設置爲’GET’,由於在普通的網頁瀏覽中,瀏覽器都使用GET,而非POST)。判斷 request.method的值很好地幫助咱們將表單顯示與表單處理隔離開來。

咱們使用request.POST代替request.GET來獲取提交過來的數據。 這是必須的,由於contact_form.html裏表單使用的是method=」post」。若是在視圖裏經過POST獲取數據,那麼request.GET將爲空。

這裏,有兩個必填項,subject 和 message,因此須要對這兩個進行驗證。 注意,咱們使用request.POST.get()方法,並提供一個空的字符串做爲默認值;這個方法很好的解決了鍵丟失與空數據問題。

雖然email非必填項,但若是有提交她的值則咱們也需進行驗證。 咱們的驗證算法至關的薄弱,僅驗證值是否包含@字符。 在實際應用中,須要更爲健壯的驗證機制(Django提供這些驗證機制,稍候咱們就會看到)。

咱們使用了django.core.mail.send_mail函數來發送e-mail。 這個函數有四個必選參數: 主題,正文,寄信人和收件人列表。 send_mail是Django的EmailMessage類的一個方便的包裝,EmailMessage類提供了更高級的方法,好比附件,多部分郵 件,以及對於郵件頭部的完整控制。

注意,若要使用send_mail()函數來發送郵件,那麼服務器須要配置成可以對外發送郵件,而且在Django中設置出站服務器地址。 參見規範:http://docs.djangoproject.com/en/dev/topics/email/

當郵件發送成功以後,咱們使用HttpResponseRedirect對象將網頁重定向至一個包含 成功信息的頁面。 包含成功信息的頁面這裏留給讀者去編寫(很簡單 一個視圖/URL映射/一份模板便可),可是咱們要解釋一下爲什麼重定向至新的頁面,而不是在模板中直接調用render_to_response()來輸 出。

緣由就是: 若用戶刷新一個包含POST表單的頁面,那麼請求將會從新發送形成重複。 這一般會形成非指望的結果,好比說重複的數據庫記錄;在咱們的例子中,將致使發送兩封一樣的郵件。 若是用戶在POST表單以後被重定向至另外的頁面,就不會形成重複的請求了。

咱們應每次都給成功的POST請求作重定向。 這就是web開發的最佳實踐。

contact()視圖能夠正常工做,可是她的驗證功能有些複雜。 想象一下假如一個表單包含一打字段,咱們真的將必須去編寫每一個域對應的if判斷語句?

另一個問題是表單的從新顯示。若數據驗證失敗後,返回客戶端的表單中各字段最好是填有原來提交的數據,以便用戶查看哪裏出現錯誤(用戶也不需再次填寫正確的字段值)。 咱們能夠手動地將原來的提交數據返回給模板,而且必須編輯HTML裏的各字段來填充原來的值。

# views.py

def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', `'noreply@example.com`_'),
                [`'siteowner@example.com`_'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html', {
        'errors': errors,
        **'subject': request.POST.get('subject', ''),**
        **'message': request.POST.get('message', ''),**
        **'email': request.POST.get('email', ''),**
    })

# contact_form.html

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject" **value="{{ subject }}"** ></p>
        <p>Your e-mail (optional): <input type="text" name="email" **value="{{ email }}"** ></p>
        <p>Message: <textarea name="message" rows="10" cols="50">**{{ message }}**</textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

這看起來雜亂,且寫的時候容易出錯。 但願你開始明白使用高級庫的用意——負責處理表單及相關校驗任務。

第一個Form類

Django帶有一個form庫,稱爲django.forms,這個庫能夠處理咱們本章所提到的包括HTML表單顯示以及驗證。 接下來咱們來深刻了解一下form庫,並使用她來重寫contact表單應用。

Django的newforms庫

在Django社區上會常常看到django.newforms這個詞語。當人們討論django.newforms,其實就是咱們本章裏面介紹的django.forms。

更名其實有歷史緣由的。 當Django一次向公衆發行時,它有一個複雜難懂的表單系統:django.forms。後來它被徹底重寫了,新的版本改叫做:django.newforms,這樣人們還能夠經過名稱,使用舊版本。 當Django 1.0發佈時,舊版本django.forms就再也不使用了,而django.newforms也終於能夠名正言順的叫作:django.forms

表單框架最主要的用法是,爲每個將要處理的HTML的`` <Form>`` 定義一個Form類。 在這個例子中,咱們只有一個`` <Form>`` ,所以咱們只需定義一個Form類。 這個類能夠存在於任何地方,甚至直接寫在`` views.py`` 文件裏也行,可是社區的慣例是把Form類都放到一個文件中:forms.py。在存放`` views.py`` 的目錄中,建立這個文件,而後輸入:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField()

這看上去簡單易懂,而且很像在模塊中使用的語法。 表單中的每個字段(域)做爲Form類的屬性,被展示成Field類。這裏只用到CharFieldEmailField類型。 每個字段都默認是必填。要使email成爲可選項,咱們須要指定required=False

讓咱們鑽研到Python解釋器裏面看看這個類作了些什麼。 它作的第一件事是將本身顯示成HTML:

>>> from contact.forms import ContactForm
>>> f = ContactForm()
>>> print f
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" name="subject" id="id_subject" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="text" name="email" id="id_email" /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr>

爲了便於訪問,Django用`` <label>`` 標誌,爲每個字段添加了標籤。 這個作法使默認行爲儘量合適。

默認輸出按照HTML的<`` table`` >格式,另外有一些其它格式的輸出:

>>> print f.as_ul()
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></li>
<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></li>
<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li>
>>> print f.as_p()
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></p>
<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p>

請注意,標籤<table>、<ul>、<form>的開閉合標記沒有包含於輸出當中,這樣你就能夠添加額外的行或者自定義格式。

這些類方法只是通常狀況下用於快捷顯示完整表單的方法。 你一樣能夠用HTML顯示個別字段:

>>> print f['subject']
<input type="text" name="subject" id="id_subject" />
>>> print f['message']
<input type="text" name="message" id="id_message" />

Form對象作的第二件事是校驗數據。 爲了校驗數據,咱們建立一個新的對Form象,而且傳入一個與定義匹配的字典類型數據:

>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 'message': 'Nice site!'})

一旦你對一個Form實體賦值,你就獲得了一個綁定form:

>>> f.is_bound
True

調用任何綁定form的is_valid()方法,就能夠知道它的數據是否合法。 咱們已經爲每一個字段傳入了值,所以整個Form是合法的:

>>> f.is_valid()
True

若是咱們不傳入email值,它依然是合法的。由於咱們指定這個字段的屬性required=False

>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'})
>>> f.is_valid()
True

可是,若是留空subjectmessage,整個Form就再也不合法了:

>>> f = ContactForm({'subject': 'Hello'})
>>> f.is_valid()
False
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.is_valid()
False

你能夠逐一查看每一個字段的出錯消息:

>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f['message'].errors
[u'This field is required.']
>>> f['subject'].errors
[]
>>> f['email'].errors
[]

每個邦定Form實體都有一個errors屬性,它爲你提供了一個字段與錯誤消息相映射的字典表。

>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.errors
{'message': [u'This field is required.']}

最終,若是一個Form實體的數據是合法的,它就會有一個可用的cleaned_data屬性。 這是一個包含乾淨的提交數據的字典。 Django的form框架不但校驗數據,它還會把它們轉換成相應的Python類型數據,這叫作清理數據。

>>> f = ContactForm({subject': Hello, email: adrian@example.com, message: Nice site!})
>>> f.is_valid()
True
>>> f.cleaned_data
{message': uNice site!, email: uadrian@example.com, subject: uHello}

咱們的contact form只涉及字符串類型,它們會被清理成Unicode對象。若是咱們使用整數型或日期型,form框架會確保方法使用合適的Python整數型或datetime.date型對象。

在視圖中使用Form對象

在學習了關於Form類的基本知識後,你會看到咱們如何把它用到視圖中,取代contact()代碼中不整齊的部分。 一下示例說明了咱們如何用forms框架重寫contact()

# views.py

from django.shortcuts import render_to_response
from mysite.contact.forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm()
    return render_to_response('contact_form.html', {'form': form})

# contact_form.html

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

看看,咱們能移除這麼多不整齊的代碼! Django的forms框架處理HTML顯示、數據校驗、數據清理和表單錯誤重現。

嘗試在本地運行。 裝載表單,先留空全部字段提交空表單;繼而填寫一個錯誤的郵箱地址再嘗試提交表單;最後再用正確數據提交表單。 (根據服務器的設置,當send_mail()被調用時,你將獲得一個錯誤提示。而這是另外一個問題。)

改變字段顯示

你可能首先注意到:當你在本地顯示這個表單的時,message字段被顯示成`` input type=」text」`` ,而它應該被顯示成<`` textarea`` >。咱們能夠經過設置* widget* 來修改它:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField(**widget=forms.Textarea** )

forms框架把每個字段的顯示邏輯分離到一組部件(widget)中。 每個字段類型都擁有一個默認的部件,咱們也能夠容易地替換掉默認的部件,或者提供一個自定義的部件。

考慮一下Field類表現* 校驗邏輯* ,而部件表現* 顯示邏輯* 。

設置最大長度

一個最常用的校驗要求是檢查字段長度。 另外,咱們應該改進ContactForm,使subject限制在100個字符之內。 爲此,僅需爲CharField提供max_length參數,像這樣:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(**max_length=100** )
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

選項min_length參數一樣可用。

設置初始值

讓咱們再改進一下這個表單:爲字subject段添加* 初始值* : "I love your site!" (一點建議,但沒壞處。)爲此,咱們能夠在建立Form實體時,使用initial參數:

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', `'noreply@example.com`_'),
                [`'siteowner@example.com`_'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm(
            **initial={'subject': 'I love your site!'}**
        )
    return render_to_response('contact_form.html', {'form': form})

如今,subject字段將被那個句子填充。

請注意,傳入* 初始值* 數據和傳入數據以* 綁定* 表單是有區別的。 最大的區別是,若是僅傳入* 初始值* 數據,表單是unbound的,那意味着它沒有錯誤消息。

自定義校驗規則

假設咱們已經發布了反饋頁面了,email已經開始源源不斷地涌入了。 這裏有一個問題: 一些提交的消息只有一兩個字,咱們沒法得知詳細的信息。 因此咱們決定增長一條新的校驗: 來點專業精神,最起碼寫四個字,拜託。

咱們有不少的方法把咱們的自定義校驗掛在Django的form上。 若是咱們的規則會被一次又一次的使用,咱們能夠建立一個自定義的字段類型。 大多數的自定義校驗都是一次性的,能夠直接綁定到form類.

咱們但願`` message`` 字段有一個額外的校驗,咱們增長一個`` clean_message()`` 方法到`` Form`` 類:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

    def clean_message(self):
        message = self.cleaned_data['message']
        num_words = len(message.split())
        if num_words < 4:
            raise forms.ValidationError("Not enough words!")
        return message

Django的form系統自動尋找匹配的函數方法,該方法名稱以clean_開頭,並以字段名稱結束。 若是有這樣的方法,它將在校驗時被調用。

特別地,clean_message()方法將在指定字段的默認校驗邏輯執行* 以後* 被調用。(本例中,在必填CharField這個校驗邏輯以後。)由於字段數據已經被部分處理,因此它被從self.cleaned_data中提取出來了。一樣,咱們沒必要擔憂數據是否爲空,由於它已經被校驗過了。

咱們簡單地使用了len()和split()的組合來計算單詞的數量。 若是用戶輸入字數不足,咱們拋出一個forms.ValidationError型異常。這個異常的描述會被做爲錯誤列表中的一項顯示給用戶。

在函數的末尾顯式地返回字段的值很是重要。 咱們能夠在咱們自定義的校驗方法中修改它的值(或者把它轉換成另外一種Python類型)。 若是咱們忘記了這一步,None值就會返回,原始的數據就丟失掉了。

指定標籤

HTML表單中自動生成的標籤默認是按照規則生成的:用空格代替下劃線,首字母大寫。如email的標籤是"Email" 。(好像在哪聽到過? 是的,一樣的邏輯被用於模塊(model)中字段的verbose_name值。 咱們在第五章談到過。)

像在模塊中作過的那樣,咱們一樣能夠自定義字段的標籤。 僅需使用label,像這樣:

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False, **label='Your e-mail address'** )
    message = forms.CharField(widget=forms.Textarea)

定製Form設計

在上面的`` contact_form.html`` 模板中咱們使用`` {{form.as_table}}`` 顯示錶單,不過咱們可使用其餘更精確控制表單顯示的方法。

修改form的顯示的最快捷的方式是使用CSS。 尤爲是錯誤列表,能夠加強視覺效果。自動生成的錯誤列表精確的使用`` <ul class=」errorlist」>``,這樣,咱們就能夠針對它們使用CSS。 下面的CSS讓錯誤更加醒目了:

<style type="text/css">
    ul.errorlist {
        margin: 0;
        padding: 0;
    }
    .errorlist li {
        background-color: red;
        color: white;
        display: block;
        font-size: 10px;
        margin: 0 0 3px;
        padding: 4px 5px;
    }
</style>

雖然,自動生成HTML是很方便的,可是在某些時候,你會想覆蓋默認的顯示。 {{form.as_table}}和其它的方法在開發的時候是一個快捷的方式,form的顯示方式也能夠在form中被方便地重寫。

每個字段部件(<input type=」text」>, <select>, <textarea>, 或者相似)均可以經過訪問{{form.字段名}}進行單獨的渲染。

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <div class="field">
            {{ form.subject.errors }}
            <label for="id_subject">Subject:</label>
            {{ form.subject }}
        </div>
        <div class="field">
            {{ form.email.errors }}
            <label for="id_email">Your e-mail address:</label>
            {{ form.email }}
        </div>
        <div class="field">
            {{ form.message.errors }}
            <label for="id_message">Message:</label>
            {{ form.message }}
        </div>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

{{ form.message.errors }} 會在 <ul class="errorlist"> 裏面顯示,若是字段是合法的,或者form沒有被綁定,就顯示一個空字符串。 咱們還能夠把 form.message.errors 看成一個布爾值或者當它是list在上面作迭代, 例如:

<div class="field{% if form.message.errors %} errors{% endif %}">
    {% if form.message.errors %}
        <ul>
        {% for error in form.message.errors %}
            <li><strong>{{ error }}</strong></li>
        {% endfor %}
        </ul>
    {% endif %}
    <label for="id_message">Message:</label>
    {{ form.message }}
</div>

在校驗失敗的狀況下, 這段代碼會在包含錯誤字段的div的class屬性中增長一個」errors」,在一個有序列表中顯示錯誤信息。

相關文章
相關標籤/搜索