【Django】表單

HTML表單

HTML中,表單是<form>...</form>之間元素的集合,它們允許訪問者輸入文本、選擇選項、操作對象等等,然后將信息發送回服務器。
某些表單的元素---文本框和復選框---非常簡單而且內建于HTML本身。其它的表單會復雜些:例如彈出一個日期選擇對話框的界面、允許你移動滾動條的界面、使用JavaScriptCSS以及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

處理表單時只會用到GETPOST方法。
Django的登錄表單使用POST方法,在這個方法中瀏覽器組合表單數據,對它們進行編碼以用于傳輸,將它們發送到服務器然后接收它的響應。
相反,GET組合提交的數據為一個字符串,然后使用它來生成一個URL。這個URL將包含數據發送的地址以及數據的鍵和值。
GETPOST用于不同的目的。
用于改變系統狀態的請求---例如,給數據庫帶來變化的請求---應該使用POST。GET只應該用于不會影響系統狀態的請求。

Django在表單中的角色

處理表單是一件很復雜的事情??紤]一下DjangoAdmin站點,不同類型的大量數據項需要在一個表單中準備好、渲染成HTML、使用一個方便的界面編輯、返回給服務器、驗證并清除,然后保存或者向后繼續處理。
Django的表單功能可以簡化并自動化大部分這些工作,并且還可以比大部分程序自己所編寫的代碼更安全。
Django會處理表單工作中的三個顯著不同的部分:

  • 準備數據、重構數據,以便下一步渲染;
  • 為數據創建HTML表單;
  • 接收并處理客戶端提交的表單和數據;

可以手工編寫代碼來實現,但是Django可以幫你完成所有這些工作。

Django中的表單

HTML<form>只是其機制的一部分。
在一個Web應用中,‘表單’可能是指HTML <form>、或者生成它的DjangoForm、或者提交時發送的結構化數據、或者這些部分的總和。

Django的Form類

表單系統的核心部分是DjangoForm類。Django的模型描述一個對象的邏輯結構、行為以及展現給我們的方式,與此類似, Form類描述一個表單并決定它如何工作和展現。
就像模型類的屬性映射到數據庫的字段一樣,表單類的字段會映射到HTML<input>表單的元素(ModelForm通過一個Form映射模型類的字段到HTML表單的<input>元素。DjangoAdmin站點就是基于這個)。
表單的字段本身也是類,它們管理表單的數據并在表單提交時進行驗證。DateFieldFileField處理的數據類型差別很大,必須完成不同的事情。
表單字段在瀏覽器中呈現給用戶的是一個HTML'widget'---用戶界面的一個片段。每個字段類型都有一個合適的默認Widget類,需要時可以覆蓋。

實例化、處理和渲染表單

Django中渲染一個對象時,我們通常:

  1. 在視圖中獲得它(例如,從數據庫中獲取);
  2. 將它傳遞給模板上下文;
  3. 使用模板變量將它擴展為HTML標記語言
    當我們在視圖中處理模型實例時,我們一般從數據庫中獲取它。當我們處理表單時,一般在視圖中實例化它。

構建一個表單

在Django中構建一個表單

Form類

我們已經計劃好了我們的HTML表單應該呈現的樣子。在Django中,我們的起始點是這里:

  forms.py
  from django import forms
  class NameForm(forms.Form):
        your_name = forms.CharField(label='Your name', max_length=100)

它定義一個Form類,只帶有一個字段(your_name)。我們已經對這個字段使用一個友好的標簽,當渲染時它將出現在<label>中。
Form的實例具有一個is_valid()方法,它為所有的字段運行驗證程序。當調用這個方法時,如果所有的字段都包含合法的數據,它將:

  • 返回True

  • 將表單的數據放到cleaned_data屬性中;
    完整的表單,第一次渲染時,看上去將像:

    <label for='your_name'>Your name:</label>
    <input id='your_name' type='text' name='your_name' maxlength='100' required/>
    

注意它不包含<form>標簽和提交按鈕,我們必須自己在模板中手動提供它們。

視圖

發送給Django網站的表單數據通過一個視圖處理,一般和發布這個表單的是同一個視圖。這允許我們重用一些相同的邏輯。
我們需要在URL對應的視圖中實例化我們將要發布的表單。

  views.py
  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 will 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()Ture,我們將能夠在cleaned_data屬性中找到所有合法的表單數據。在發送HTTP重定向給瀏覽器告訴它下一步的去向之前,我們可以用這個數據來更新數據庫或者做其它處理。

模板

我們不需要在name.html模板中做很多工作,最簡單的例子是:

  <form action="/your-name/" method="post">
      {%csrf_token%}
      {{form}}
      <input type='submit' value="Submit" />
  </form>

根據{{form}},所有的表單字段和它們的屬性將通過Django的模板語言拆分成HTML標記語言。

現在我們有一個可以工作的網頁表單,它通過Django Form描述、通過視圖處理并渲染成一個HTML <form>。

Django Form類詳解

所有的表單類都作為django.forms.Form的子類創建,包括你在Django管理站點中遇到的ModelForm。
模型和表單
實際上,如果你的表單打算直接用來添加和編輯Django的模型,ModelForm可以節省你的許多時間、精力和代碼,因為它將根據Model類構建一個表單以及適當的字段和屬性。

綁定的和未綁定的表單實例

綁定的和未綁定的表單之間的區別非常重要:

  • 未綁定的表單沒有關聯的數據。當渲染給用戶時,它將為空或者包含默認的值。
  • 綁定的表單具有提交地數據,因此可以用來檢驗數據是否合法。如果渲染一個不合法的綁定的表單,它將包含內聯的錯誤信息,告訴用戶如何糾正數據。
    表單的is_bound屬性將告訴你一個表單是否具有綁定的數據。

字段詳解

考慮一個比上面的迷你示例更有用的一個表單,在一個網站上實現“contact me”功能:

  forms.py
  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中直接訪問到未驗證的數據,但是訪問驗證后的數據更好一些。
下面是在視圖中如何處理表單數據:

  views.py
  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, recipient)
     return HttpResponseRedirect('/thanks/')

有些字段類型需要一些額外的處理。例如,使用表單上傳的文件需要不同地處理(它們可以從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" required /></p>
<p><label for="id_message">Message:</label>
<textarea name="message" id="id_message" required></textarea></p>
<p><label for="id_sender">Sender:</label>
<input type="email" name="sender" id="id_sender" required /></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>。

手工渲染字段

我們沒有必要非要讓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??瓷先タ赡芟瘢?/p>

<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 %}

迭代表單字段

如果你為你的每個表單字段使用相同的HTML,你可以使用{%for%}循環迭代每個字段來減少重復的代碼。

{% for field in form %}
<div class="fieldWrapper">
    {{ field.errors }}
    {{ field.label_tag }} {{ field }}
    {% if field.help_text %}
    <p class="help">{{ field.help_text|safe }}</p>
    {% endif %}
</div>
{% endfor %}

迭代隱藏和可見的字段

如果你正在手工布局模板中的一個表單,而不是依賴Django 默認的表單布局,你可能希望將<input type="hidden"> 字段與非隱藏的字段區別對待。例如,因為隱藏的字段不會顯示,在該字段旁邊放置錯誤信息可能讓你的用戶感到困惑 —— 所以這些字段的錯誤應該有區別地來處理。

Django 提供兩個表單方法,它們允許你獨立地在隱藏的和可見的字段上迭代:hidden_fields() 和visible_fields()。下面是使用這兩個方法對前面一個例子的修改:

{# Include the hidden fields #}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{# Include the visible fields #}
{% for field in form.visible_fields %}
<div class="fieldWrapper">
    {{ field.errors }}
    {{ field.label_tag }} {{ field }}
</div>
{% endfor %}

這個示例沒有處理隱藏字段中的任何錯誤信息。通常,隱藏字段中的錯誤意味著表單被篡改,因為正常的表單填寫不會改變它們。然而,你也可以很容易地為這些表單錯誤插入一些錯誤信息顯示出來。

可重用的表單模板

如果你的網站在多個地方對表單使用相同的渲染邏輯,你可以保存表單的循環到一個單獨的模板中來減少重復,然后在其它模板中使用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 %}

綁定的表單和未綁定的表單

表單要么是綁定的,要么是未綁定的。

  • 如果是綁定的,那么能夠驗證數據,并渲染表單及其數據成HTML。
  • 如果是未綁定的,那么它不能夠完成驗證(因為沒有可驗證的數據),但是仍然能渲染成空白的表單成HTML。

class Form

若要創建一個未綁定的表單實例,只需要簡單地實例化該類:

>>>f = ContactForm()

若要綁定數據到表單,可以將數據以字典的形式傳遞給表單類的構造函數的第一個參數:

>>> data = {'subject': 'hello',
...         'message': 'Hi there',
...         'sender': 'foo@example.com',
...         'cc_myself': True}
>>> f = ContactForm(data)

在這個字典中,鍵為字段的名稱,它們對應于表單類中的屬性。值為需要驗證的數據。它們通常為字符串,但是沒有強制要求必須是字符串。傳遞的數據類型取決于字段。

Form.is_bound

如果在運行時你要區分綁定的表單和未綁定的表單,可以檢查下表單is_bound屬性的值。
>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject': 'hello'})
>>> f.is_bound
True
注意,傳遞一個空的字典將創建一個帶有空數據的綁定的表單:
>>> f = ContactForm({})
>>> f.is_bound
True
如果你有一個綁定的表單實例但是想修改下數據,或者你想綁定一個未綁定的表單到某些數據,你需要創建另外一個表單列表。Form實例的數據沒有辦法修改。表單實例一旦創建,你應該將它的數據視為不可變的,無論它有沒有數據。

使用表單來驗證數據

Form.clean()

當你需要為相互依賴的字段添加自定義的驗證時,你可以實現表單的clean()方法。

Form.is_valid()

表單對象的首要任務就是驗證數據。對于綁定的表單實例,可以調用is_valid()方法來執行驗證并返回一個表示數據是否合法的布爾值。
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
讓我們試下非法的數據。下面的情形中,subject 為空(默認所有字段都是必需的)且sender 是一個不合法的郵件地址:
>>> data = {'subject': '',
... 'message': 'Hi there',
... 'sender': 'invalid email address',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
False

Form.errors

訪問errors屬性可以獲得錯誤信息的一個字典。
>>> f.errors
{'sender': ['Enter a valid email address.'], 'subject': ['This field is required.']}
在這個字典中,鍵為字段的名稱,值為表示錯誤信息的Unicode字符串組成的列表。錯誤信息保存在列表中是因為字段可能有多個錯誤信息。
你可以訪問errors而不必須先調用is_valid()。表單的數據將在第一次調用is_valid()或者訪問errors時驗證。
驗證只會調用一次,無論你訪問errors或者調用is_valid()多少次。這意味著,如果驗證過程有副作用,這些副作用將只觸發一次。

Form.non_field_errors()

這個方法返回Form.errors中不是與特定字段相關聯的錯誤。它包含在Form.clean()中引發的ValidationError和使用Form.add_error(None,'...')添加的錯誤。

動態的初始值

Form.initial

表單字段的初始值使用initial聲明。例如,你可能希望使用當前會話的用戶名填充username字段。
使用Form的initial參數可以實現。該參數是字段名到初始值的一個字典。只需要包含你期望給出初始值的字段,不需要包含表單中的所有字段。例如:
>>> f = ContactForm(initial={'subject': 'Hi there!'})
這些值只顯示在沒有綁定的表單中,即使沒有提供特定值它們也不會作為后備的值。
注意,如果字段有定義initial,而實例化表單時也提供initial,那么后面的initial將優先。在下面的例子中,initial在字段和表單實例化中都有定義,此時后者具有優先權。
>>> from django import forms
>>> class CommentForm(forms.Form):
... name = forms.CharField(initial='class')
... url = forms.URLField()
... comment = forms.CharField()
>>> f = CommentForm(initial={'name': 'instance'}, auto_id=False)
>>> print(f)
<tr><th>Name:</th><td><input type="text" name="name" value="instance" required /></td></tr>
<tr><th>Url:</th><td><input type="url" name="url" required /></td> </tr>
<tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>

檢查表單數據是否改變

Form.has_changed()
當你需要檢查表單的數據是否從初始數據發生改變時,可以使用表單的has_changed()方法。
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data, initial=data)
>>> f.has_changed()
False
當提交表單時,我們可以重新構建表單并提供初始值,這樣可以實現比較:
>>> f = ContactForm(request.POST, initial=data)
>>> f.has_changed()
如果request.POST中的數據與inital中的不同,has_changed()將為True,否則為False。計算的結果是通過調用表單每個字段的Field.has_changed()得到的。

訪問'clean'的數據

Form.cleaned_data
表單類中的每個字段不僅負責驗證數據,還負責‘清潔’它們---將它們轉換為正確的格式。這是個非常好用的功能,因為它允許字段以多種方式輸入數據,并總能得到一致的輸出。
一旦你創建一個表單實例并通過驗證后,你就可以通過它的cleaned_data屬性訪問清潔的數據:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'cc_myself': True, 'message': 'Hi there', 'sender': 'foo@example.com', 'subject': 'hello'}
如果你的數據沒有通過驗證,cleaned_data字典中只包含合法的字段:
>>> data = {'subject': '',
... 'message': 'Hi there',
... 'sender': 'invalid email address',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
>>> f.cleaned_data
{'cc_myself': True, 'message': 'Hi there'}
cleaned_data 始終只 包含表單中定義的字段,即使你在構建表單 時傳遞了額外的數據。在下面的例子中,我們傳遞一組額外的字段給ContactForm 構造函數,但是cleaned_data 將只包含表單的字段:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True,
... 'extra_field_1': 'foo',
... 'extra_field_2': 'bar',
... 'extra_field_3': 'baz'}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data # Doesn't contain extra_field_1, etc.
{'cc_myself': True, 'message': 'Hi there', 'sender': 'foo@example.com', 'subject': 'hello'}
當表單合法時,cleaned_data 將包含所有字段的鍵和值,即使傳遞的數據不包含某些可選字段的值。在下面的例子中,傳遞的數據字典不包含nick_name 字段的值,但是cleaned_data 任然包含它,只是值為空:
>>> from django import forms
>>> class OptionalPersonForm(forms.Form):
... first_name = forms.CharField()
... last_name = forms.CharField()
... nick_name = forms.CharField(required=False)
>>> data = {'first_name': 'John', 'last_name': 'Lennon'}
>>> f = OptionalPersonForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'nick_name': '', 'first_name': 'John', 'last_name': 'Lennon'}
在上面的例子中,cleaned_data 中nick_name 設置為一個空字符串,這是因為nick_name 是CharField而 CharField 將空值作為一個空字符串。每個字段都知道自己的“空”值 —— 例如,DateField 的空值是None 而不是一個空字符串。關于每個字段空值的完整細節,參見“內建的Field 類”一節中每個字段的“空值”提示。

表單必填行和錯誤行的樣式表

Form.error_css_class
Form.required_css_class
將必填的表單行和有錯誤的表單行定義不同的樣式表特別常見。例如,你想將必填的表單行以粗體顯示,將錯誤以紅色顯示。
表單類具有一對鉤子,可以使用它們來添加class屬性給必填的行或者有錯誤的行:只需要簡單地設置Form.error_css_class或 Form.required_css_class屬性:

from django import forms
class ContactForm(forms.Form):
    error_css_class = 'error'
    required_css_class = 'required'

    # ... and the rest of your fields here

一旦你設置好,將根據需要,設置行的"error" 或"required" CSS類型。 其HTML 看上去將類似:
>>> f = ContactForm(data)
>>> print(f.as_table())
<tr class="required"><th><label class="required" for="id_subject">Subject:</label> ...
<tr class="required"><th><label class="required" for="id_message">Message:</label> ...
<tr class="required error"><th><label class="required" for="id_sender">Sender:</label> ...
<tr><th><label for="id_cc_myself">Cc myself:<label> ...
>>> f['subject'].label_tag()
<label class="required" for="id_subject">Subject:</label>
>>> f['subject'].label_tag(attrs={'class': 'foo'})
<label for="id_subject" class="foo required">Subject:</label>

從模型創建表單

ModelForm

如果你正在構建一個數據庫驅動的應用,那么你應該會有與Django的模型緊密映射的表單。舉個例子,你也許會有個BlogComment模型,并且你還想創建一個表單讓大家提交評論到這個模型中。在這種情況下,在表單中定義定義字段是冗余的,因為你已經在模型中定義了字段。
基于這個原因,Django提供一個輔助類來讓你從Django的模型創建表單。例如,
>>> from django.forms import ModelForm
>>> from myapp.models import Article

# Create the form class.
>>> class ArticleForm(ModelForm):
...     class Meta:
...         model = Article
...         fields = ['pub_date', 'headline', 'content', 'reporter']

# Creating a form to add an article.
>>> form = ArticleForm()

# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)

字段類型

生成的表單類中將具有和指定的模型字段對應的表單字段。字段順序為fields屬性中指定的順序。
每個模型字段有一個對應的默認表單字段。比如,模型中的CharField對應表單中的CharField。模型中的ManyToManyField字段會表現成MultipleChoiceField字段。

模型表單的驗證

驗證模型表單主要有兩步:

  1. 驗證表單;
  2. 驗證模型實例;

與普通的表單驗證類似,模型表單的驗證在調用is_valid()或者訪問errors屬性時隱式調用,或者通過full_clean()顯式調用,盡管在實際應用中很少使用后一種方法。
模型的驗證Model.full_clean()在表單驗證這一步的內部觸發,緊跟在表單的clean()方法調用之后。

save()方法

每個模型表單還具有一個save()方法。這個方法根據表單綁定的數據創建并保存數據庫對象。模型表單的子類可以用關鍵字參數instance接收一個已經存在的模型實例。如果提供,save()將更新這個實例。如果沒有提供,save()將創建模型的一個新實例。
>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm

# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)

# Save a new Article object from the form's data.
>>> new_article = f.save()

# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()

注意,如果表單沒有驗證,save()調用將通過檢查form.errors來進行驗證。如果表單中的數據不合法,將引發ValuelError。
save() 接受一個可選的commit 關鍵字參數,其值為True 或False。如果save() 時commit=False,那么它將返回一個還沒有保存到數據庫的對象。這種情況下,你需要調用返回的模型實例的save()。這是很有用的,如果你想在保存之前自定義一些處理了,或者你想使用特定的模型保存選項。
使用commit=False 的另外一個副作用是在模型具有多對多關系的時候。如果模型具有多對多關系而且當你保存表單時指定commit=False,Django 不會立即為多對多關系保存表單數據。這是因為只有實例在數據庫中存在時才可以保存實例的多對多數據。

為了解決這個問題,每當你使用commit=False 保存表單時,Django 將添加一個save_m2m() 方法到你的模型表單子類。在你手工保存有表單生成的實例之后,你可以調用save_m2m() 來保存多對多的表單數據。例如:
# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)

# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)

# Modify the author in some way.
>>> new_author.some_field = 'some_value'

# Save the new instance.
>>> new_author.save()

# Now, save the many-to-many data for the form.
>>> f.save_m2m()

save_m2m() 只在你使用save(commit=False) 時才需要。當你直接使用save(),所有的數據 —— 包括多對多數據 —— 都將保存而不需要任何額外的方法調用。例如:
# Create a form instance with POST data.
>>> a = Author()
>>> f = AuthorForm(request.POST, instance=a)

# Create and save the new author instance. There's no need to do   anything else.
>>> new_author = f.save()

處理關系的字段Fields which handle relationships

兩個字段可用于表示模型之間的關系:ModelChoiceField和ModelMultipleChoiceField。這兩個字段都需要單個queryset參數,用于創建字段的選擇。在表單驗證時,這些字段將把一個模型對象(在ModelChoiceField的情況下)或多個模型對象(在ModelMultipleChoiceField的情況下)放置到表單的cleaned_data字典。
對于更復雜的用法,可以在聲明表單字段時指定queryset=None,然后在表單的__init__()方法中填充queryset。

  class FooMultipleChoiceForm(forms.Form):
        foo_select = forms.ModelMultipleChoiceField(queryset=None)
        def __init__(self, *args, **kwargs):
              super(FooMultipleChoiceForm, self).__init__(*args, **kwargs)
              self.fields['foo_select'].queryset = ...

ModelChoiceField

class ModelChoiceField(**kwargs)

  • 默認的Widget: Select
  • 空值: None
  • 規范化為: 一個模型實例
  • 驗證給定的id是否存在于查詢集中
  • 錯誤信息的鍵: required, invalid_choice

可以選擇一個單獨的模型對象,適用于表示一個外鍵字段。ModelChoiceField默認widget不適用選擇數量很大的情況,在大于100項時應該避免使用它。
需要一個單獨參數:
queryset: 將導出字段選擇的模型對象的QuerySet,將用于驗證用戶的選擇。
ModelChoiceField有兩個可選參數:

  • empty_label: 默認情況下,ModelChoiceField使用的<select>小部件將在列表頂部有一個空選項。您可以使用empty_label屬性更改此標簽的文本(默認為"________"),也可以完全禁用空白標簽通過將empty_label設置為None:

    #A custom empty label
    field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")
    #No empty label
    field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
    

請注意如果需要用到ModelChoiceField有一個默認的初始值,則不會創建空選項(不管empty_label的值)。

  • to_field_name: 這個參數用于指定要用作字段小部件選項的值的字段。確保它是模型的唯一字段,否則選定的值可以匹配多個對象。默認情況下,它設置為None,在這種情況下,將使用每個對象的主鍵。

    #No custom to_field_name
    field1 = forms.ModelChoiceField(queryset=...)
    

會渲染成:

  <select id='id_field1' name='field1'>
  <option value='obj1.pk'>Object1</option>
  <option value='obj2.pk'>Object2</option>
  ...
  </select>

和:

  #to_field_name provided
  field2 = forms.ModelChoiceField(queryset=... to_field_name='name')

會生成:

  <select id='id_field2' name='field2'>
  <option value='obj1.name'>Object1</option>
  <option value='obj2.name'>Object2</option>
  ...
  </select>

ModelMultipleChoiceField

  • 默認的Widget: SelectMultiple
  • 控制: QuerySet(self.queryset.none())
  • 規范化為: 模型實例的一個QuerySet。
  • 錯誤信息的鍵: required, list, invalid_choice, invalid_pk_value

允許選擇適合于表示多對多關系的一個或多個模型對象。queryset是必需的參數。
queryset:將導出字段選擇的模型對象的QuerySet,將用于驗證用戶的選擇。

表單驗證和字段驗證

表單驗證發生在數據清洗時。如果需要自定義這個過程,有幾個不同的地方可以修改,每個地方的目的不一樣。表單處理過程中要運行三種類別的驗證方法:它們通常在你調用表單的is_valid()方法時執行。還有其它方法可以觸發驗證過程(訪問errors屬性或直接調用full_clean()),但是通常情況下不需要。
一般情況下,如果處理的數據有問題,每個驗證方法都會引發ValidationError,并將相關信息傳遞給ValidationError構造器。如果沒有引發ValidationError,這些方法應該返回驗證后的(規整化的,清洗后的)數據的Python對象。
大部分驗證應該可以使用validators完成,它們可以很容易地重用。Validators是簡單的函數(或可調用對象),它們接收一個參數并對非法的輸入拋出ValidationError。Validators在字段的to_pythonvalidate方法調用之后運行。
表單的驗證分成幾個步驟,它們可以定制或覆蓋:

  • 字段的to_python()方法是驗證的第一步。它將值強制轉換為正確的數據類型,如果不能轉換則引發ValidationError。這個方法從Widget接收原始的值并返回轉換后的值。例如,FloatField將數據轉換為Pythonfloat或引發ValidationError。
  • 字段的validate()方法處理字段的特別的驗證,這種驗證不適合validator。它接收一個已經轉換成正確數據類型的值,并在發現錯誤時引發ValidationError。這個方法不返回任何東西且不應該改變任何值。當您遇到不能或不想放在validator中的驗證邏輯時,應該覆蓋它來處理驗證。
  • 字段的run_validators()方法運行字段所有的Validator,并將所有的錯誤信息聚合成一個單一的ValidationError。你應該不需要覆蓋這個方法。
  • Filed子類的clean()方法。它負責以正確的順序運行to_python、validaterun_validators并傳播它們的錯誤。如果任何時刻、任何方法引發ValidationError,驗證將停止并引發這個錯誤。這個方法返回驗證后的數據,這個數據在后面將插入到表單的cleaned_data字典中。
  • 表單子類中的clean_<fieldname>方法---<fieldname>通過表單中的字段名稱替換。這個方法完成于特定屬性相關的驗證,這個驗證與字段的類型無關。這個方法沒有任何輸入的參數。你需要查找self.cleaned_data中該字段的值,記住此時它已經是一個Python對象而不是表單中提交的原始字符串(它位于cleaned_data中是因為字段的clean()方法已經驗證過一次數據)。
    例如,如果你想驗證名為serialnumberCharField的內容是否唯一,clean_serialnumber()將是實現這個功能的理想之處。你需要的不是一個特別的字段(它只是一個CharField),而是一個特定于表單字段的特定驗證,并規整化數據。
    這個方法返回的值將代替cleaned_data中已經存在的值,因此它必須是cleaned_data中字段的值或一個新的清洗后的值。
  • 表單子類的clean()方法。這個方法可以實現需要同時訪問表單多個字段的驗證。這里你可以驗證如果提供字段A,那么字段B必須包含一個合法的郵件地址以及類似的功能。這個方法可以返回一個完全不同的字典,該字典將用作cleaned_data。
    因為字段的驗證方法在調用clean()時會運行,你還可以訪問表單的errors屬性,它包含驗證每個字段時的所有錯誤。
    注意,你覆蓋的Form.clean()引發的任何錯誤將不會與任何特定的字段關聯。它們位于一個特定的‘字段’(叫做__all__)中,如果需要可以通過non_field_errors()方法訪問。如果你想添加一個特定字段的錯誤到表單中,需要調用add_error()。
    還要注意,覆蓋ModelForm子類的clean()方法需要特殊的考慮。

這些方法按照以上給出的順序執行,一次驗證一個字段。也就是說,對于表單中的每個字段(按照它們在表單定義中出現的順序),先運行Field.clean(),然后運行clean_<fieldname>()。每個字段的這兩個方法都執行完之后,最后運行Form.clean()方法,無論前面的方法是否拋出過異常。

下面由上面每個方法的示例。
我們已經提到過,所有這些方法都可以拋出ValidationError。對于每個字段,如果Field.clean()方法拋出ValidationError,那么將不會調用該字段對應的clean_<fieldname>()方法。但是,剩余的字段的驗證方法仍然會執行。

在實踐中應用驗證

使用Validator

Django的表單(以及模型)字段支持使用簡單的函數和類用于驗證,它們叫做Validator。Validator是可調用對象或函數,它接收一個值,如果該值合法則什么也不返回,否則拋出ValidationError。它們可以通過字段的validators參數傳遞給字段的構造函數,或者定義在Field類的default_validators屬性中。
簡單的Validator可以用于在字段內部驗證值,讓我們看下Django的SlugField:

  from django.forms import CharField
  form django.core import validators
  class SlugField(CharField):
        default_validators = [validators.validate_slug]

正如你所看到的,SlugField只是一個帶有自定義Validator的CharField,它們驗證提交的文本符合某些字符規則。這也可以在字段定義時實現,所以:

  slug = forms.SlugField()

等同于:

  slug = forms.CharField(validators=[validtors.validate_slug])

常見的情形,例如驗證郵件地址和正則表達式,可以使用Django中已經存在的Validator類處理。

表單字段的默認驗證

讓我們首先創建一個自定義的表單字段,它驗證其輸入是一個由逗號分隔的郵件地址組成的字符串。完整的類像這樣:

  from django import forms
  from django.core.validators import validate_mail
  class MultiEmailField(forms.Field):
        def to_python(self, value):
            """Nomalize data to a list of strings."""
            #Return an empty list if no input was given.
            if not value:
                return []
             return value.split(',')
        def validate(self, value):
            """Check  if value consists only of valid emails."""
            #Use the parent's handling of required fields, etc.
            super(MultiEmailField, self).validate(value)
            for email in value:
                validate_email(email)

使用這個字段的每個表單都將在處理該字段數據之前運行這些方法。這個驗證特定于該類型的字段,與后面如何使用它無關。
讓我們來創建一個簡單的ContactForm來向你演示如何使用這個字段:

  class ContactForm(forms.Form):
        subject = forms.CharField(max_lenght=100)
        message = forms.CharField()
        sender = forms.EmailField()
        recipients = MultiEmailField()
        cc_myself = forms.BooleanField(required=False)

只需要簡單地使用MultiEmailField,就和其它表達字段一樣。當調用表單的is_valid()方法時,MultiEmailField.clean()方法將作為驗證過程的一部分運行,它將調用自定義的to_python()和validate()方法。

驗證特定字段屬性

繼續上面的例子,假設在ContactForm中,我們想確保recipients字段始終包含"fred@example.com"。這是特定于我們這個表達的驗證,所以我們不打算將它放在通用的MultiEmailField類中,我們將編寫一個運行在recipients字段上的驗證方法,像這樣:

  from django import forms
  class ContactForm(forms.Form):
        #Everything as before.
        ...
        def clean_recipients(self):
              data = self.cleaned_data['recipients']
              if 'fred@example.com' not in data:
                 raise forms.ValidationError("You have forgotten about Fred!")
              #Always return a value to use as the new cleaned data, even if this method didn't change it.
              return data

驗證相互依賴的字段

假設我們添加另外一個需求到我們的ContactForm表單中:如果cc_myself字段為True,那么subject必須包含單詞"help"。我們的這個驗證包含多個字段,所以表單的clean()方法是個不錯的地方。注意,我們這里討論的是表單的clean()方法,之前我們編寫過字段的clean()方法。區別字段和表單之間的差別非常重要。字段是單個數據,表單是字段的集合。
在調用表單clean()方法的時候,所有字段的驗證方法已經執行完,所以self.cleaned_data填充的是目前為止已經合法的數據。所以你需要記住這個事實,你需要驗證的字段可能沒有通過初始的字段檢查。
在這一步,有兩種方法報告錯誤。最簡單的方法是在表單的頂端顯示錯誤。你可以在clean()方法中拋出ValidationError來創建錯誤。例如,

  from django import forms
  class ContactForm(forms.Form):
        #Everythign as before.
        ...
        def clean(self):
            cleaned_data = super(ContactForm, self).clean()
            cc_myself = cleaned_data.get("cc_myself")
            subject = cleaned_data.get("subject")
            if cc_myself and subject:
            # Only do something if both fields are valid so far.
            if "help" not in subject:
                 raise forms.ValidationError(
                "Did not send for 'help' in the subject despite "
                "CC'ing yourself."
            )

在這段代碼中,如果拋出驗證錯誤,表單將在表單的頂部顯示(通常是)描述該問題的一個錯誤信息。
注意,示例代碼中super(ContactForm, self).clean()的調用是為了維持父類中的驗證邏輯。如果你的表單繼承自另外一個在clean()方法中沒有返回一個cleaned_data字典的表單,那么不要把cleand_data聯系到super()的結果而要使用self.cleaned_data。

  def clean(self):
      super(ContactForm, self).clean()
      cc_myself = self.cleaned_data.get('cc_myself')
      ...

第二種方法涉及將錯誤消息關聯到某個字段。在這種情況下,讓我們在表單的顯示中分別關聯一個錯誤信息到“subject” 和“cc_myself” 行。在實際應用中要小心,因為它可能導致表單的輸出變得令人困惑。我們只是向你展示這里可以怎么做,在特定的情況下,需要你和你的設計人員確定什么是好的方法。我們的新代碼(代替前面的示例)像這樣:

from django import forms
class ContactForm(forms.Form):
# Everything as before.
...

def clean(self):
    cleaned_data = super(ContactForm, self).clean()
    cc_myself = cleaned_data.get("cc_myself")
    subject = cleaned_data.get("subject")

    if cc_myself and subject and "help" not in subject:
        msg = "Must put 'help' in subject when cc'ing yourself."
        self.add_error('cc_myself', msg)
        self.add_error('subject', msg)

add_error() 的第二個參數可以是一個簡單的字符串,但更傾向是ValidationError的一個實例

編寫Validator

驗證器是一個可調用的對象,它接受一個值,并在不符合一些規則時候拋出ValidationError異常。驗證器有助于在不同類型的字段之間重復使用驗證邏輯。
例如,這個驗證器只允許偶數:

  from django.core.exceptions import ValidationError
  from django.utils.translation import ugettext_lazy as _
  def validate_even(value):
      if value % s != 0:
        raise ValidationError(
        _('%(value)s is not an even number'),
        params={'value': value},
      )

你可以通過字段的validators參數將它添加到模型字段中:

   from django.db import models
   class MyModel(models.Model):
         even_field = models.IntegerField(validators=[validate_even])

由于值在驗證器運行之前會轉化為Python,你可以在表單上使用相同的驗證器:

    from django import forms
    class MyForm(forms.Form):
          even_field = forms.IntegerField(validators=[validate_even])

你可以可以使用帶有__call__()方法的類,實現更復雜或可配置的驗證器。

Validator如何運行

參考:

  1. http://python.usyiyi.cn/translate/django_182/topics/forms/index.html
  2. http://python.usyiyi.cn/translate/django_182/ref/forms/api.html
  3. http://python.usyiyi.cn/translate/django_182/topics/forms/modelforms.html
  4. http://python.usyiyi.cn/translate/django_182/ref/forms/fields.html
  5. http://python.usyiyi.cn/translate/django_182/ref/forms/validation.html
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容