HTML 表單
在HTML中,表單是<form>...</form> 之間元素的集合,它們允許訪問者輸入文本、選擇選項、操作對象和控制等等,然后將信息發送回服務器。
某些表單的元素 —— 文本輸入和復選框 —— 非常簡單而且內建於HTML 本身。其它的表單會復雜些;例如彈出一個日期選擇對話框的界面、允許你移動滾動條的界面、使用JavaScript 和CSS 以及HTML 表單<input> 元素來實現操作控制的界面。
與<input> 元素一樣,一個表單必須指定兩樣東西:
- 目的地:響應用戶輸入數據的URL
- 方式:發送數據所使用的HTTP 方法
例如,Django Admin 站點的登錄表單包含幾個<input> 元素:type="text" 用於用戶名,type="password" 用於密碼,type="submit" 用於“Log in" 按鈕。它還包含一些用戶看不到的隱藏的文本字段,Django 使用它們來決定下一步的行為。
它還告訴瀏覽器表單數據應該發往<form> 的action 屬性指定的URL —— /admin/,而且應該使用method 屬性指定的HTTP 方法 —— post。
當觸發<input type="submit" value="Log in"> 元素時,數據將發送給/admin/。
GET 和 POST
處理表單時候只會用到GET 和 POST 方法。
Django 的登錄表單使用POST 方法,在這個方法中瀏覽器組合表單數據、對它們進行編碼以用於傳輸、將它們發送到服務器然后接收它的響應。
相反,GET 組合提交的數據為一個字符串,然后使用它來生成一個URL。這個URL 將包含數據發送的地址以及數據的鍵和值。如果你在Django 文檔中做一次搜索,你會立即看到這點,此時將生成一個https://docs.djangoproject.com/search/?q=forms&release=1 形式的URL。
GET 和POST 用於不同的目的。
用於改變系統狀態的請求 —— 例如,給數據庫帶來變化的請求 —— 應該使用POST。GET 只應該用於不會影響系統狀態的請求。
GET 還不適合密碼表單,因為密碼將出現在URL 中,以及瀏覽器的歷史和服務器的日志中,而且都是以普通的文本格式。它還不適合數據量大的表單和二進制數據,例如一張圖片。使用GET 請求作為管理站點的表單具有安全隱患:攻擊者很容易模擬表單請求來取得系統的敏感數據。POST,如果與其它的保護措施結合將對訪問提供更多的控制,例如Django 的CSRF 保護。
另一個方面,GET 適合網頁搜索這樣的表單,因為這種表示一個GET 請求的URL 可以很容易地作為書簽、分享和重新提交。
Django 在表單中的角色
處理表單是一件很復雜的事情。考慮一下Django 的Admin 站點,不同類型的大量數據項需要在一個表單中准備好、渲染成HTML、使用一個方便的界面編輯、返回給服務器、驗證並清除,然后保存或者向后繼續處理。
Django 的表單功能可以簡化並自動化大部分這些工作,而且還可以比大部分程序員自己所編寫的代碼更安全。
Django 會處理表單工作中的三個顯著不同的部分:
- 准備數據、重構數據,以便下一步提交。
- 為數據創建HTML 表單
- 接收並處理客戶端提交的表單和數據
可以手工編寫代碼來實現,但是Django 可以幫你完成所有這些工作。
Django 中的表單
我們已經簡短講述HTML 表單,但是HTML的<form> 只是其機制的一部分。
在一個Web 應用中,"表單"可能指HTML <form>、或者生成它的Django 的Form、或者提交時發送的結構化數據、或者這些部分的總和。
Django 的Form 類
表單系統的核心部分是Django 的Form 類。Django 的模型描述一個對象的邏輯結構、行為以及展現給我們的方式,與此類似,Form 類描述一個表單並決定它如何工作和展現。
就像模型類的屬性映射到數據庫的字段一樣,表單類的字段會映射到HTML 的<input>表單的元素。(ModelForm 通過一個Form 映射模型類的字段到HTML 表單的<input> 元素;Django 的Admin 站點就是基於這個)。
表單的字段本身也是類;它們管理表單的數據並在表單提交時進行驗證。DateField 和FileField 處理的數據類型差別很大,必須完成不同的事情。
表單字段在瀏覽器中呈現給用戶的是一個HTML 的“widget” —— 用戶界面的一個片段。每個字段類型都有一個合適的默認Widget 類,需要時可以覆蓋。
實例化、處理和渲染表單
在Django 中渲染一個對象時,我們通常:
- 在視圖中獲得它(例如,從數據庫中獲取)
- 將它傳遞給模板上下文
- 使用模板變量將它擴展為HTML 標記
在模板中渲染表單和渲染其它類型的對象幾乎一樣,除了幾個關鍵的差別。
在模型實例不包含數據的情況下,在模板中對它做處理很少有什么用處。但是渲染一個未填充的表單卻非常有意義 —— 我們希望用戶去填充它。
所以當我們在視圖中處理模型實例時,我們一般從數據庫中獲取它。當我們處理表單時,我們一般在視圖中實例化它。
當我們實例化表單時,我們可以選擇讓它為空還是預先填充它,例如使用:
- 來自一個保存后的模型實例的數據(例如用於編輯的管理表單)
- 我們從其它地方獲得的數據
- 從前面一個HTML 表單提交過來的數據
最后一種情況最令人關注,因為它使得用戶可以不只是閱讀一個網站,而且可以給網站返回信息。
構建一個表單
需要完成的工作
假設你想在你的網站上創建一個簡單的表單,以獲得用戶的名字。你需要類似這樣的模板:
<form action="/your-name/" method="post"> <label for="your_name">Your name: </label> <input id="your_name" type="text" name="your_name" value="{{ current_name }}"> <input type="submit" value="OK"> </form>
這告訴瀏覽器發送表單的數據到URL /your-name/,並使用POST 方法。它將顯示一個標簽為"Your name:"的文本字段,和一個"OK"按鈕。如果模板上下文包含一個current_name 變量,它將用於預填充your_name 字段。
你將需要一個視圖來渲染這個包含HTML 表單的模板,並提供合適的current_name 字段。
當表單提交時,發往服務器的POST 請求將包含表單數據。
現在你還需要一個對應/your-name/ URL 的視圖,它在請求中找到正確的鍵/值對,然后處理它們。
這是一個非常簡單的表單。實際應用中,一個表單可能包含幾十上百個字段,其中大部分需要預填充,而且我們預料到用戶將來回編輯-提交幾次才能完成操作。
我們可能需要在表單提交之前,在瀏覽器端作一些驗證。我們可能想使用非常復雜的字段,以允許用戶做類似從日歷中挑選日期這樣的事情,等等。
這個時候,讓Django 來為我們完成大部分工作是很容易的。
在Django 中構建一個表單
Form 類
我們已經計划好了我們的 HTML 表單應該呈現的樣子。在Django 中,我們的起始點是這里:
from django import forms class NameForm(forms.Form): your_name = forms.CharField(label='Your name', max_length=100)
它定義一個Form 類,只帶有一個字段(your_name)。我們已經對這個字段使用一個友好的標簽,當渲染時它將出現在<label> 中(在這個例子中,即使我們省略它,我們指定的label還是會自動生成)。
字段允許的最大長度通過max_length 定義。它完成兩件事情。首先,它在HTML 的<input> 上放置一個maxlength="100" (這樣瀏覽器將在第一時間阻止用戶輸入多於這個數目的字符)。它還意味着當Django 收到瀏覽器發送過來的表單時,它將驗證數據的長度。
Form 的實例具有一個is_valid() 方法,它為所有的字段運行驗證的程序。當調用這個方法時,如果所有的字段都包含合法的數據,它將:
- 返回True
- 將表單的數據放到cleaned_data 屬性中。
完整的表單,第一次渲染時,看上去將像:
<label for="your_name">Your name: </label> <input id="your_name" type="text" name="your_name" maxlength="100">
注意它不包含 <form> 標簽和提交按鈕。我們必須自己在模板中提供它們。
視圖
發送給Django 網站的表單數據通過一個視圖處理,一般和發布這個表單的是同一個視圖。這允許我們重用一些相同的邏輯。
要操作一個通過URL發布的表單,我們要在視圖中實例表單。
from django.shortcuts import render from django.http import HttpResponseRedirect from .forms import NameForm def get_name(request): # if this is a POST request we need to process the form data if request.method == 'POST': # create a form instance and populate it with data from the request: form = NameForm(request.POST) # check whether it's valid: if form.is_valid(): # process the data in form.cleaned_data as required # ... # redirect to a new URL: return HttpResponseRedirect('/thanks/') # if a GET (or any other method) we'll create a blank form else: form = NameForm() return render(request, 'name.html', {'form': form})
如果訪問視圖的是一個GET 請求,它將創建一個空的表單實例並將它放置到要渲染的模板的上下文中。這是我們在第一次訪問該URL 時預期發生的情況。
如果表單的提交使用POST 請求,那么視圖將再次創建一個表單實例並使用請求中的數據填充它:form =NameForm(request.POST)。這叫做”綁定數據至表單“(它現在是一個綁定的表單)。
我們調用表單的is_valid() 方法;如果它不為True,我們將帶着這個表單返回到模板。這時表單不再為空(未綁定),所以HTML 表單將用之前提交的數據填充,然后可以根據要求編輯並改正它。
如果is_valid() 為True,我們將能夠在cleaned_data 屬性中找到所有合法的表單數據。在發送HTTP 重定向給瀏覽器告訴它下一步的去向之前,我們可以用這個數據來更新數據庫或者做其它處理。
模板
我們不需要在name.html 模板中做很多工作。最簡單的例子是:
<form action="/your-name/" method="post"> {% csrf_token %} {{ form }} <input type="submit" value="Submit" /> </form>
根據{{ form }},所有的表單字段和它們的屬性將通過Django 的模板語言拆分成HTML 標記 。
表單和跨站請求偽造的防護
Django 原生支持一個簡單易用的跨站請求偽造的防護。當提交一個啟用CSRF 防護的POST 表單時,你必須使用上面例子中的csrf_token 模板標簽。然而,因為CSRF 防護在模板中不是與表單直接捆綁在一起的,這個標簽在這篇文檔的以下示例中將省略。
HTML5 輸入類型和瀏覽器驗證
如果你的表單包含URLField、EmailField 或其它整數字段類型,Django 將使用url、email和 number 這樣的HTML5 輸入類型。默認情況下,瀏覽器可能會對這些字段進行它們自身的驗證,這些驗證可能比Django 的驗證更嚴格。如果你想禁用這個行為,請設置form 標簽的novalidate 屬性,或者指定一個不同的字段,如TextInput。
現在我們有了一個可以工作的網頁表單,它通過Django Form 描述、通過視圖處理並渲染成一個HTML <form>。
這是你入門所需要知道的所有內容,但是表單框架為了便利提供了更多的內容。一旦你理解了上面描述的基本處理過程,你應該可以理解表單系統的其它功能並准備好學習更多的底層機制。
Django Form 類詳解
所有的表單類都作為django.forms.Form 的子類創建,包括你在Django 管理站點中遇到的ModelForm。
模型和表單
實際上,如果你的表單打算直接用來添加和編輯Django 的模型,ModelForm 可以節省你的許多時間、精力和代碼,因為它將根據Model 類構建一個表單以及適當的字段和屬性。
綁定的和未綁定的表單實例
綁定的和未綁定的表單 之間的區別非常重要:
- 未綁定的表單沒有關聯的數據。當渲染給用戶時,它將為空或包含默認的值。
- 綁定的表單具有提交的數據,因此可以用來檢驗數據是否合法。如果渲染一個不合法的綁定的表單,它將包含內聯的錯誤信息,告訴用戶如何糾正數據。
表單的is_bound 屬性將告訴你一個表單是否具有綁定的數據。
字段詳解
考慮一個比上面的迷你示例更有用的一個表單,我們可以用它來在一個個人網站上實現“contact me”功能:
from django import forms class ContactForm(forms.Form): subject = forms.CharField(max_length=100) message = forms.CharField(widget=forms.Textarea) sender = forms.EmailField() cc_myself = forms.BooleanField(required=False)
我們前面的表單只使用一個字段your_name,它是一個CharField。在這個例子中,我們的表單具有四個字段:subject、message、sender 和cc_myself。共用到三種字段類型:CharField、EmailField 和 BooleanField;完整的字段類型列表可以在表單字段中找到。
窗口小部件
每個表單字段都有一個對應的Widget 類,它對應一個HTML 表單Widget,例如<input type="text">。
在大部分情況下,字段都具有一個合理的默認Widget。例如,默認情況下,CharField 具有一個TextInput Widget,它在HTML 中生成一個<input type="text">。如果你需要<textarea>,在定義表單字段時你應該指定一個合適的Widget,例如我們定義的message字段。
字段的數據¶
不管表單提交的是什么數據,一旦通過調用is_valid() 成功驗證(is_valid() 返回True),驗證后的表單數據將位於form.cleaned_data 字典中。這些數據已經為你轉換好為Python 的類型。
注
此時,你依然可以從request.POST 中直接訪問到未驗證的數據,但是訪問驗證后的數據更好一些。
在上面的聯系表單示例中,cc_myself 將是一個布爾值。類似地,IntegerField 和FloatField 字段分別將值轉換為Python 的int和float。
下面是在視圖中如何處理表單數據:
from django.core.mail import send_mail if form.is_valid(): subject = form.cleaned_data['subject'] message = form.cleaned_data['message'] sender = form.cleaned_data['sender'] cc_myself = form.cleaned_data['cc_myself'] recipients = ['info@example.com'] if cc_myself: recipients.append(sender) send_mail(subject, message, sender, recipients) return HttpResponseRedirect('/thanks/')
提示
關於Django 中如何發送郵件的更多信息,請參見發送郵件。
有些字段類型需要一些額外的處理。例如,使用表單上傳的文件需要不同地處理(它們可以從request.FILES 獲取,而不是request.POST)。如何使用表單處理文件上傳的更多細節,請參見綁定上傳的文件到一個表單。
使用表單模板
你需要做的就是將表單實例放進模板的上下文。如果你的表單在Context 中叫做form,那么 {{ form }} 將正確地渲染它的<label> 和<input>元素。
表單渲染的選項
表單模板的額外標簽
不要忘記,表單的輸出不 包含<form> 標簽,和表單的submit 按鈕。你必須自己提供它們。
對於<label>/<input> 對,還有幾個輸出選項:
- {{ form.as_table }} 以表格的形式將它們渲染在<tr> 標簽中
- {{ form.as_p }} 將它們渲染在<p> 標簽中
- {{ form.as_ul }} 將它們渲染在<li> 標簽中
注意,你必須自己提供<table> 或<ul> 元素。
下面是我們的ContactForm 實例的輸出{{ form.as_p }}:
<p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></p> <p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p> <p><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></p> <p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p>
注意,每個表單字段具有一個ID 屬性並設置為id_<field-name>,它被一起的label 標簽引用。它對於確保屏幕閱讀軟件這類的輔助計算非常重要。你還可以自定義label 和 id 生成的方式。
更多信息參見 輸出表單為HTML。
手工渲染字段
我們沒有必要非要讓Django 來分拆表單的字段;如果我們喜歡,我們可以手工來做(例如,這樣允許重新對字段排序)。每個字段都是表單的一個屬性,可以使用{{ form.name_of_field }} 訪問,並將在Django 模板中正確地渲染。例如:
{{ form.non_field_errors }} <div class="fieldWrapper"> {{ form.subject.errors }} <label for="{{ form.subject.id_for_label }}">Email subject:</label> {{ form.subject }} </div> <div class="fieldWrapper"> {{ form.message.errors }} <label for="{{ form.message.id_for_label }}">Your message:</label> {{ form.message }} </div> <div class="fieldWrapper"> {{ form.sender.errors }} <label for="{{ form.sender.id_for_label }}">Your email address:</label> {{ form.sender }} </div> <div class="fieldWrapper"> {{ form.cc_myself.errors }} <label for="{{ form.cc_myself.id_for_label }}">CC yourself?</label> {{ form.cc_myself }} </div>
完整的<label> 元素還可以使用label_tag() 生成。例如:
<div class="fieldWrapper"> {{ form.subject.errors }} {{ form.subject.label_tag }} {{ form.subject }} </div>
渲染表單的錯誤信息
當然,這個便利性的代價是更多的工作。直到現在,我們沒有擔心如何展示錯誤信息,因為Django 已經幫我們處理好。在下面的例子中,我們將自己處理每個字段的錯誤和表單整體的各種錯誤。注意,表單和模板頂部的{{ form.non_field_errors }} 查找每個字段的錯誤。
使用{{ form.name_of_field.errors }} 顯示表單錯誤的一個清單,並渲染成一個ul。看上去可能像:
<ul class="errorlist"> <li>Sender is required.</li> </ul>
這個ul 有一個errorlist CSS 類型,你可以用它來定義外觀。如果你希望進一步自定義錯誤信息的顯示,你可以迭代它們來實現:
{% if form.subject.errors %} <ol> {% for error in form.subject.errors %} <li><strong>{{ error|escape }}</strong></li> {% endfor %} </ol> {% endif %}
非字段錯誤(以及使用form.as_p() 時渲染的隱藏字段錯誤)將渲染成一個額外的CSS 類型nonfield 以助於和字段錯誤信息區分。例如,{{ form.non_field_errors }} 看上去會像:
<ul class="errorlist nonfield"> <li>Generic validation error</li> </ul>
添加上面示例中提到的nonfield CSS 類型。
參見Forms API 以獲得關於錯誤、樣式以及在模板中使用表單屬性的更多內容。
迭代表單的字段
如果你為你的表單使用相同的HTML,你可以使用{% for %} 循環迭代每個字段來減少重復的代碼:
{% for field in form %} <div class="fieldWrapper"> {{ field.errors }} {{ field.label_tag }} {{ field }} </div> {% endfor %}
{{ field }} 中有用的屬性包括:
- {{ field.label }}
- 字段的label,例如Email address。
- {{ field.label_tag }}
-
包含在HTML <label> 標簽中的字段Label。它包含表單的label_suffix。例如,默認的label_suffix 是一個冒號:
<label for="id_email">Email address:</label>
- {{ field.id_for_label }}
- 用於這個字段的ID(在上面的例子中是id_email)。 如果你正在手工構造label,你可能想使用它代替label_tag。 如果你有一些內嵌的JavaScript 並且想避免硬編碼字段的ID,這也是有用的。
- {{ field.value }}
- 字段的值,例如someone@example.com。
- {{ field.html_name }}
- 輸入元素的name 屬性中將使用的名稱。 它將考慮到表單的前綴。
- {{ field.help_text }}
- 與該字段關聯的幫助文檔。
- {{ field.errors }}
- 輸出一個<ul class="errorlist">,包含這個字段的驗證錯誤信息。 你可以使用{% for error in field.errors %}自定義錯誤的顯示。 這種情況下,循環中的每個對象只是一個包含錯誤信息的簡單字符串。
- {{ field.is_hidden }}
- 如果字段是隱藏字段,則為True,否則為False。 作為模板變量,它不是很有用處,但是可以用於條件測試,例如:
{% if field.is_hidden %} {% endif %}
- {{ field.field }}
- 表單類中的Field 實例,通過BoundField 封裝。 你可以使用它來訪問Field 屬性,例如{% char_field.field.max_length %}。
可重用的表單模板
如果你的網站在多個地方對表單使用相同的渲染邏輯,你可以保存表單的循環到一個單獨的模板中來減少重復,然后在其它模板中使用include 標簽來重用它:
# In your form template:
{% include "form_snippet.html" %} # In form_snippet.html: {% for field in form %} <div class="fieldWrapper"> {{ field.errors }} {{ field.label_tag }} {{ field }} </div> {% endfor %}
如果傳遞到模板上下文中的表單對象具有一個不同的名稱,你可以使用include 標簽的with 參數來對它起個別名:
{% include "form_snippet.html" with form=comment_form %}
如果你發現自己經常這樣做,你可能需要考慮一下創建一個自定義的 inclusion 標簽。
