【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用于不同的目的。
用于改变系统状态的请求---例如,给数据库带来变化的请求---应该使用POSTGET只应该用于不会影响系统状态的请求。

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。看上去可能像:

<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是简单的函数(或可调用对象),它们接收一个参数并对非法的输入抛出ValidationErrorValidators在字段的to_pythonvalidate方法调用之后运行。
表单的验证分成几个步骤,它们可以定制或覆盖:

  • 字段的to_python()方法是验证的第一步。它将值强制转换为正确的数据类型,如果不能转换则引发ValidationError。这个方法从Widget接收原始的值并返回转换后的值。例如,FloatField将数据转换为Pythonfloat或引发ValidationError
  • 字段的validate()方法处理字段的特别的验证,这种验证不适合validator。它接收一个已经转换成正确数据类型的值,并在发现错误时引发ValidationError。这个方法不返回任何东西且不应该改变任何值。当您遇到不能或不想放在validator中的验证逻辑时,应该覆盖它来处理验证。
  • 字段的run_validators()方法运行字段所有的Validator,并将所有的错误信息聚合成一个单一的ValidationError。你应该不需要覆盖这个方法。
  • Filed子类的clean()方法。它负责以正确的顺序运行to_pythonvalidaterun_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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容