Django之Form操作


一、Form基礎

(一)Form的作用

Django的Form主要有以下幾大作用:

  • 生成HTML標簽
  • 驗證用戶數據(顯示錯誤信息)
  • Form提交保留上次提交數據
  • 初始化頁面顯示數據

(二)實例

一般網站都有登錄頁面,如果使用Form應該怎么做呢?

1、創建LoginForm類

from django import forms

class LoginForm(forms.Form):
    username = forms.CharField(max_length=32)
    password = forms.CharField(max_length=32)

2、views中進行實例化

def login(request):
    if request.method == 'GET':
        #實例化LoginForm
        form = LoginForm()
    #發送的是POST請求
    form = LoginForm(data=request.POST)
    #對數據進行驗證
    if form.is_valid():
        print(form.cleaned_data)
        return redirect('/index/')
    return render(request,'login.html',{'form':form})

3、login.html中使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form method="post" action="/login/">
    {% csrf_token %}
    {{ form }}  <!--自動生成html標簽-->
    <input type="submit"/>
</form>

這樣很容易就完成了登錄功能,並且Form的作用也都體現出來了。

(三)forms字段和插件

在上述實例中,使用Form需要創建一個Form相關的類,並且在類中寫入相關的forms字段,這里涉及到Field類,是所有forms字段最根本的基類,它位於django.forms.Field下:

class Field:
    widget = TextInput  # Default widget to use when rendering this type of Field.
    hidden_widget = HiddenInput  # Default widget to use when rendering this as "hidden".
    default_validators = []  # Default set of validators
    # Add an 'invalid' entry to default_error_message if you want a specific
    # field error message not raised by the field validators.
    default_error_messages = {
        'required': _('This field is required.'),
    }
    empty_values = list(validators.EMPTY_VALUES)

    def __init__(self, *, required=True, widget=None, label=None, initial=None,
                 help_text='', error_messages=None, show_hidden_initial=False,
                 validators=(), localize=False, disabled=False, label_suffix=None):
        # required -- Boolean that specifies whether the field is required.
        #             True by default.
        # widget -- A Widget class, or instance of a Widget class, that should
        #           be used for this Field when displaying it. Each Field has a
        #           default Widget that it'll use if you don't specify this. In
        #           most cases, the default widget is TextInput.
        # label -- A verbose name for this field, for use in displaying this
        #          field in a form. By default, Django will use a "pretty"
        #          version of the form field name, if the Field is part of a
        #          Form.
        # initial -- A value to use in this Field's initial display. This value
        #            is *not* used as a fallback if data isn't given.
        # help_text -- An optional string to use as "help text" for this Field.
        # error_messages -- An optional dictionary to override the default
        #                   messages that the field will raise.
        # show_hidden_initial -- Boolean that specifies if it is needed to render a
        #                        hidden widget with initial value after widget.
        # validators -- List of additional validators to use
        # localize -- Boolean that specifies if the field should be localized.
        # disabled -- Boolean that specifies whether the field is disabled, that
        #             is its widget is shown in the form but not editable.
        # label_suffix -- Suffix to be added to the label. Overrides
        #                 form's label_suffix.
        self.required, self.label, self.initial = required, label, initial
        self.show_hidden_initial = show_hidden_initial
        self.help_text = help_text
        self.disabled = disabled
        self.label_suffix = label_suffix
        widget = widget or self.widget
        if isinstance(widget, type):
            widget = widget()
        else:
            widget = copy.deepcopy(widget)

        # Trigger the localization machinery if needed.
        self.localize = localize
        if self.localize:
            widget.is_localized = True

        # Let the widget know whether it should display as required.
        widget.is_required = self.required

        # Hook into self.widget_attrs() for any Field-specific HTML attributes.
        extra_attrs = self.widget_attrs(widget)
        if extra_attrs:
            widget.attrs.update(extra_attrs)

        self.widget = widget

        messages = {}
        for c in reversed(self.__class__.__mro__):
            messages.update(getattr(c, 'default_error_messages', {}))
        messages.update(error_messages or {})
        self.error_messages = messages

        self.validators = list(itertools.chain(self.default_validators, validators))

        super().__init__()

    def prepare_value(self, value):
        return value

    def to_python(self, value):
        return value

    def validate(self, value):
        if value in self.empty_values and self.required:
            raise ValidationError(self.error_messages['required'], code='required')

    def run_validators(self, value):
        if value in self.empty_values:
            return
        errors = []
        for v in self.validators:
            try:
                v(value)
            except ValidationError as e:
                if hasattr(e, 'code') and e.code in self.error_messages:
                    e.message = self.error_messages[e.code]
                errors.extend(e.error_list)
        if errors:
            raise ValidationError(errors)

    def clean(self, value):
        """
        Validate the given value and return its "cleaned" value as an
        appropriate Python object. Raise ValidationError for any errors.
        """
        value = self.to_python(value)
        self.validate(value)
        self.run_validators(value)
        return value

    def bound_data(self, data, initial):
        """
        Return the value that should be shown for this field on render of a
        bound form, given the submitted POST data for the field and the initial
        data, if any.

        For most fields, this will simply be data; FileFields need to handle it
        a bit differently.
        """
        if self.disabled:
            return initial
        return data

    def widget_attrs(self, widget):
        """
        Given a Widget instance (*not* a Widget class), return a dictionary of
        any HTML attributes that should be added to the Widget, based on this
        Field.
        """
        return {}

    def has_changed(self, initial, data):
        """Return True if data differs from initial."""
        # Always return False if the field is disabled since self.bound_data
        # always uses the initial value in this case.
        if self.disabled:
            return False
        try:
            data = self.to_python(data)
            if hasattr(self, '_coerce'):
                return self._coerce(data) != self._coerce(initial)
        except ValidationError:
            return True
        # For purposes of seeing whether something has changed, None is
        # the same as an empty string, if the data or initial value we get
        # is None, replace it with ''.
        initial_value = initial if initial is not None else ''
        data_value = data if data is not None else ''
        return initial_value != data_value

    def get_bound_field(self, form, field_name):
        """
        Return a BoundField instance that will be used when accessing the form
        field in a template.
        """
        return BoundField(form, self, field_name)

    def __deepcopy__(self, memo):
        result = copy.copy(self)
        memo[id(self)] = result
        result.widget = copy.deepcopy(self.widget, memo)
        result.validators = self.validators[:]
        return result
Field

1、字段的基本參數

在這個類中的__init__方法中有很多參數,這也就是字段參數:

    required=True,               是否允許為空
    widget=None,                 HTML插件
    label=None,                  用於生成Label標簽或顯示內容
    initial=None,                初始值
    help_text='',                幫助信息(在標簽旁邊顯示)
    error_messages=None,         錯誤信息 {'required': '不能為空', 'invalid': '格式錯誤'}
    show_hidden_initial=False,   是否在當前插件后面再加一個隱藏的且具有默認值的插件(可用於檢驗兩次輸入是否一直)
    validators=[],               自定義驗證規則
    localize=False,              是否支持本地化
    disabled=False,              是否可以編輯
    label_suffix=None            Label內容后綴

2、字段的基本方法

在這個類中還有一些方法,也就是字段對象的方法:

  • clean
    def clean(self, value):
        """
        Validate the given value and return its "cleaned" value as an
        appropriate Python object. Raise ValidationError for any errors.
        """
        value = self.to_python(value)
        self.validate(value)
        self.run_validators(value)
        return value
clean

每個Field實例都有一個clean()方法,它接受一個參數並引發django.forms.ValidationError 異常或返回clean值:

python manage.py shell

In [1]: from django import forms
In [2]: f = forms.EmailField()
In [3]: f.clean('62514@qq.com')
Out[3]: '62514@qq.com'
...
In [4]: f.clean('62514')
ValidationError: ['Enter a valid email address.']
  • bound_data
    def bound_data(self, data, initial):
        """
        Return the value that should be shown for this field on render of a
        bound form, given the submitted POST data for the field and the initial
        data, if any.

        For most fields, this will simply be data; FileFields need to handle it
        a bit differently.
        """
        if self.disabled:
            return initial
        return data
bound_data

它有兩個參數,一個是request.POST提交的data,另一個是initial參數初始化的數據,這個方法返回的就是request.POST提交的那個字段值。

  • has_changed
    def has_changed(self, initial, data):
        """Return True if data differs from initial."""
        # Always return False if the field is disabled since self.bound_data
        # always uses the initial value in this case.
        if self.disabled:
            return False
        try:
            data = self.to_python(data)
            if hasattr(self, '_coerce'):
                return self._coerce(data) != self._coerce(initial)
        except ValidationError:
            return True
        # For purposes of seeing whether something has changed, None is
        # the same as an empty string, if the data or initial value we get
        # is None, replace it with ''.
        initial_value = initial if initial is not None else ''
        data_value = data if data is not None else ''
        return initial_value != data_value
has_changed

它有兩個參數,一個是request.POST提交的data,另一個是initial參數初始化的數據,這個方法返回的是布爾值,如果提交的data與初始化數據不一樣的話就返回True。

  • get_bound_field
    def get_bound_field(self, form, field_name):
        """
        Return a BoundField instance that will be used when accessing the form
        field in a template.
        """
        return BoundField(form, self, field_name)
get_bound_field

它有兩個參數,一個是form實例,另一個是創建的Form類字段的字段名稱field_name,而它的返回值是BoundField對象。

3、常用字段

CharField(Field)
    max_length=None,             #最大長度
    min_length=None,             #最小長度
    strip=True                   #是否移除用戶輸入空白
 
IntegerField(Field)
    max_value=None,              #最大值
    min_value=None,              #最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              #最大值
    min_value=None,              #最小值
    max_digits=None,             #總長度
    decimal_places=None,         #小數位長度
 
BaseTemporalField(Field)
    input_formats=None          #時間格式化   
 
DateField(BaseTemporalField)    #格式:2015-09-01
TimeField(BaseTemporalField)    #格式:11:12
DateTimeField(BaseTemporalField)#格式:2015-09-01 11:12
 
DurationField(Field)            #時間間隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      #自定制正則表達式
    max_length=None,            #最大長度
    min_length=None,            #最小長度
    error_message=None,         #忽略,錯誤信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     #是否允許空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模塊,pip3 install Pillow
    以上兩個字典使用時,需要注意兩點:
        - form表單中 enctype="multipart/form-data"
        - view函數中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                #選項,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             #是否必填
    widget=None,               #插件,默認select插件
    label=None,                #Label內容
    initial=None,              #初始值
    help_text='',              #幫助提示
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查詢數據庫中的數據
    empty_label="---------",   # 默認空顯示內容
    to_field_name=None,        # HTML中value的值對應的字段
    limit_choices_to=None      # ModelForm中對queryset二次篩選
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        #django.forms.models.ModelMultipleChoiceField
 
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   #對選中的值進行一次轉換
    empty_value= ''            #空值的默認值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   #對選中的每一個值進行一次轉換
    empty_value= ''            #空值的默認值
 
ComboField(Field)
    fields=()                  #使用多個驗證,如下:即驗證最大長度20,又驗證郵箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)         #抽象類,子類中可以實現聚合多個字典去匹配一個值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   #格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    #格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     #文件選項,目錄下文件顯示在頁面中
    path,                      #文件夾路徑
    match=None,                #正則匹配
    recursive=False,           #遞歸下面的文件夾
    allow_files=True,          #允許文件
    allow_folders=False,       #允許文件夾
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           #both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          #解析ipv4地址,如果是::ffff:192.0.2.1時候,可解析為192.0.2.1, PS:protocol必須為both才能啟用
 
SlugField(CharField)           #數字,字母,下划線,減號(連字符)
    ...
 
UUIDField(CharField)           #uuid類型
常用字段

一個字段可包含以下參數:

from django.forms import fields as ffields
from django.forms import widgets as fwidgets

group_name = ffields.CharField(
        max_length=32,
        required=True,
        widget=fwidgets.TextInput(attrs={'class': 'form-control'}),
        error_messages={
            'required': '組別名稱不能為空',
        })

4、常用插件

每一個字段都有一個默認的插件,這時為了在頁面生成html標簽時,生成該字段對應的html標簽:

class CharField(Field):
    def __init__(self, *, max_length=None, min_length=None, strip=True, empty_value='', **kwargs):
        self.max_length = max_length
        self.min_length = min_length
        self.strip = strip
        self.empty_value = empty_value
        super().__init__(**kwargs)
        if min_length is not None:
            self.validators.append(validators.MinLengthValidator(int(min_length)))
        if max_length is not None:
            self.validators.append(validators.MaxLengthValidator(int(max_length)))
        self.validators.append(validators.ProhibitNullCharactersValidator())

    def to_python(self, value):
        """Return a string."""
        if value not in self.empty_values:
            value = str(value)
            if self.strip:
                value = value.strip()
        if value in self.empty_values:
            return self.empty_value
        return value

    def widget_attrs(self, widget):
        attrs = super().widget_attrs(widget)
        if self.max_length is not None and not widget.is_hidden:
            # The HTML attribute is maxlength, not max_length.
            attrs['maxlength'] = str(self.max_length)
        if self.min_length is not None and not widget.is_hidden:
            # The HTML attribute is minlength, not min_length.
            attrs['minlength'] = str(self.min_length)
        return attrs
CharField

在CharField中,它繼承了Field,所以它的默認Widget是Field中TextInput,它在HTML 中生成一個<input type="text">。Django的內置插件有:

TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget
內置插件

5、選擇插件

  • 常用選擇插件
# 單radio,值為字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
 
# 單radio,值為字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.RadioSelect
# )
 
# 單select,值為字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.Select(choices=((1,'上海'),(2,'北京'),))
# )
 
# 單select,值為字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.Select
# )
 
# 多選select,值為列表
# user = fields.MultipleChoiceField(
#     choices=((1,'上海'),(2,'北京'),),
#     initial=[1,],
#     widget=widgets.SelectMultiple
# )
 
 
# 單checkbox
# user = fields.CharField(
#     widget=widgets.CheckboxInput()
# )
 
 
# 多選checkbox,值為列表
# user = fields.MultipleChoiceField(
#     initial=[2, ],
#     choices=((1, '上海'), (2, '北京'),),
#     widget=widgets.CheckboxSelectMultiple
# )

注意:

  city = fields.TypedChoiceField(
  coerce=lambda x: int(x),
  choices=[(1,'上海',),(2,'北京'),(3,'沙河'),],
  initial=2
  )
  • 數據的實時更新

使用選擇插件可以從數據庫中獲取數據,但是由於字段是靜態的,在修改數據后不會進行立即更新,所以需要進行一些處理:

from app01.models import *
from django.forms.models import ModelChoiceField  #方法二實時更新
class classesForm(forms.Form):
#方法二,切記model類中需要有__str()__方法 class_select_model =ModelChoiceField( queryset=classesAll.objects.all(), to_field_name = 'id',#指的是value值 )
  
#方法一 class_select = fields.ChoiceField( # choices=classesAll.objects.values_list('id','name'),可以省略 widget=widgets.Select() ) def __init__(self,*args,**kwargs): super(classesForm,self).__init__(*args,**kwargs) self.fields['class_select'].widget.choices=classesAll.objects.values_list('id','name')

比如:

class ProcessHours(models.Model):
    """
    工時庫
    """
    create_user = models.ForeignKey(UserInfo, verbose_name='生產管理者', null=True, blank=True)
    process_hours_group = models.ForeignKey(ProcessGroup, verbose_name='組別名稱',null=True,blank=True)
    process_hours_type = models.ForeignKey(ProcessPrice,verbose_name='工序類別',null=True,blank=True)
    process_hours_name = models.CharField(verbose_name='工序名稱',null=True, max_length=128,blank=True)
    process_hours_all = models.DecimalField(verbose_name='工時',null=True, blank=True,decimal_places=2,max_digits=10)

    def __str__(self):
        return '%s-%s' % (self.process_hours_group, self.process_hours_name)
ProcessHours
from django.forms import widgets as fwidgets
from django.forms import Form
from django.forms import fields as ffields

class ProcessHoursForm(Form):
    create_user_id = ffields.ChoiceField(
        widget=fwidgets.Select(attrs={'class': 'form-control'}),
        choices=[],

    )
    process_hours_group_id = ffields.ChoiceField(
        widget=fwidgets.Select(attrs={'class': 'form-control'}),
        choices=[],
    )
    process_hours_type_id = ffields.ChoiceField(
        widget = fwidgets.Select(attrs={'class': 'form-control'}),
        choices=[],
    )
    def __init__(self, *args, **kwargs):
        super(ProcessHoursForm, self).__init__(*args, **kwargs)
        self.fields['create_user_id'].choices = models.UserInfo.objects.values_list('id','nickname')
        self.fields['process_hours_group_id'].choices = models.ProcessGroup.objects.values_list('id','group_name')
        self.fields['process_hours_type_id'].choices = models.ProcessPrice.objects.values_list('id','process_type')

(四)表單和字段驗證

1、正則驗證

from django.forms import Form
from django.forms import fields
from django.core.validators import RegexValidator
 
class TestForm(Form):

   v1= fields.CharField(
        disabled=True
    )
   v2= fields.CharField(
        validators=[RegexValidator(r'^[0-9]+$', '請輸入數字')],
    )

2、對每個字段定義鈎子函數

from django.core.exceptions import ValidationError

...
...

#方法二、對每個字段定義鈎子函數,num是字段名

    def clean_v1(self):
        value = self.cleaned_data('v1')
        if classesAll.objects.all().count():
            raise ValidationError('班級已經存在')
        return value

3、聯合驗證,這是在以上驗證結束后在進行此驗證

   def clean(self):
        v1 = self.cleaned_data['v1']
        v2 = self.cleaned_data['v2']
        if v1 == 'aa' and v2 =='bb':
            raise ValidationError('信息錯誤')
        return self.cleaned_data

二、Form源碼一覽

(一)Form實操

from django.shortcuts import render
from django.shortcuts import redirect
from app01 import models
from app01.forms import UserForm

#增加數據
def add_user(request):
     user_list = models.UserInfo.objects.all() #用於初始化頁面,相當於獲取所有用戶列表
  if request.method == 'GET':
    obj = UserForm(user_list )
  return render(request,'add_user.html',{'obj':obj})
  else:
    obj = UserForm(request.POST)
    if obj.is_valid():
      models.UserInfo.objects.create(**obj.cleaned_data)
      return redirect('/users/')
    else:
      return render(request,'add_user.html',{'obj':obj})

#修改數據
def edit_user(request,nid):
  if request.method == "GET":
    data = models.UserInfo.objects.filter(id=nid).first()
    obj = UserForm({'username':data.username,'email':data.email})
    return render(request,'edit_user.html',{'obj':obj,'nid':nid})
  else:
    obj = UserForm(request.POST)
    if obj.is_valid():
      models.UserInfo.objects.filter(id=nid).update(**obj.cleaned_data)
      return redirect('/users/')
    else:
      return render(request,'edit_user.html',{'obj':obj,'nid':nid})

(二)源碼一覽

1、Form實例化

可以看到無論是GET還是POST請求都需要進行Form類的實例化時:

class BaseForm:
    """
    The main implementation of all the Form logic. Note that this class is
    different than Form. See the comments by the Form class for more info. Any
    improvements to the form API should be made to this class, not to the Form
    class.
    """
    default_renderer = None
    field_order = None
    prefix = None
    use_required_attribute = True

    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, label_suffix=None,
                 empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
        self.is_bound = data is not None or files is not None
        self.data = {} if data is None else data
        self.files = {} if files is None else files
        self.auto_id = auto_id
        if prefix is not None:
            self.prefix = prefix
        self.initial = initial or {}
        self.error_class = error_class
        # Translators: This is the default suffix added to form field labels
        self.label_suffix = label_suffix if label_suffix is not None else _(':')
        self.empty_permitted = empty_permitted
        self._errors = None  # Stores the errors after clean() has been called.

        # The base_fields class attribute is the *class-wide* definition of
        # fields. Because a particular *instance* of the class might want to
        # alter self.fields, we create self.fields here by copying base_fields.
        # Instances should always modify self.fields; they should not modify
        # self.base_fields.
        self.fields = copy.deepcopy(self.base_fields)
        self._bound_fields_cache = {}
        self.order_fields(self.field_order if field_order is None else field_order)

        if use_required_attribute is not None:
            self.use_required_attribute = use_required_attribute

        # Initialize form renderer. Use a global default if not specified
        # either as an argument or as self.default_renderer.
        if renderer is None:
            if self.default_renderer is None:
                renderer = get_default_renderer()
            else:
                renderer = self.default_renderer
                if isinstance(self.default_renderer, type):
                    renderer = renderer()
        self.renderer = renderer
BaseForm中的__init__方法

注意到在__init__方法中:

 self.fields = copy.deepcopy(self.base_fields)

獲取字段:

self.fields={
            "username":"字段規則對象",
            "password":"字段規則對象",

        }

2、is_valid

    def is_valid(self):
        """Return True if the form has no errors, or False otherwise."""
        return self.is_bound and not self.errors

源碼說如果這個form沒有錯誤就返回True,否則就是False。

self.is_bound = data #data是傳入的request.POST

而self.errors中:

    @property
    def errors(self):
        """Return an ErrorDict for the data provided for the form."""
        if self._errors is None:
            self.full_clean()
        return self._error
errors
    def full_clean(self):
        """
        Clean all of self.data and populate self._errors and self.cleaned_data.
        """
        self._errors = ErrorDict()
        if not self.is_bound:  # Stop further processing.
            return
        self.cleaned_data = {}
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        self._post_clean()

所以在驗證通過的情況下:

  • 初始化 self.cleaned_data = {}
  • 執行每一個字段的自定義驗證鈎子函數  self._clean_fields()
  • 執行全局驗證的鈎子函數  self._clean_form()

注意:在最后還預留了一個鈎子,可用於擴展功能

    def _post_clean(self):
        """
        An internal hook for performing additional cleaning after form cleaning
        is complete. Used for model validation in model forms.
        """
        pass

3、self._clean_fields()

    def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            if field.disabled:
                value = self.get_initial_for_field(field, name)
            else:
                value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
            try:
                if isinstance(field, FileField):
                    initial = self.get_initial_for_field(field, name)
                    value = field.clean(value, initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    #執行每一個字段的鈎子函數
                    value = getattr(self, 'clean_%s' % name)()
                    #將驗證完畢的字段加入到self.cleaned_data
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)

4、self._clean_form()

    def _clean_form(self):
        try:
            #返回cleaned_data
            cleaned_data = self.clean()
        except ValidationError as e:
            self.add_error(None, e)
        else:
            if cleaned_data is not None:
                self.cleaned_data = cleaned_data

5、屬性、方法

從源碼中就可以看出來,驗證完畢后cleaned_data就有數據了,此時獲取驗證完畢的數據可以使用:

#獲取所有驗證成功的數據
form.cleaned_data
form.cleaned_data["username"]

錯誤信息使用:

form.errors

#每個字段錯誤信息
form.errors["username"]

6、頁面渲染

#渲染username字段錯誤
{{ form.Username.errors }}

#渲染username字段標簽
 {{ form.Username }}

三、擴展

(一)BoundField類

BoundField類是什么呢?看個例子就知道了。

...

    form = LoginForm(data=request.POST)

    for field in form:
        print(type(field),field)

...

上述輸出:

<class 'django.forms.boundfield.BoundField'> <input type="text" name="username" value="admin" id="id_username" maxlength="32" required />
<class 'django.forms.boundfield.BoundField'> <input type="text" name="password" value="123" id="id_password" maxlength="32" required />

這個就是form對象中的字段將自定義Form類中的字段與form實例進行綁定聯系,具體方法可以參考下面源碼。

@html_safe
class BoundField:
    "A Field plus data"
    def __init__(self, form, field, name):
        self.form = form
        self.field = field
        self.name = name
        self.html_name = form.add_prefix(name)
        self.html_initial_name = form.add_initial_prefix(name)
        self.html_initial_id = form.add_initial_prefix(self.auto_id)
        if self.field.label is None:
            self.label = pretty_name(name)
        else:
            self.label = self.field.label
        self.help_text = field.help_text or ''

    def __str__(self):
        """Render this field as an HTML widget."""
        if self.field.show_hidden_initial:
            return self.as_widget() + self.as_hidden(only_initial=True)
        return self.as_widget()

    @cached_property
    def subwidgets(self):
        """
        Most widgets yield a single subwidget, but others like RadioSelect and
        CheckboxSelectMultiple produce one subwidget for each choice.

        This property is cached so that only one database query occurs when
        rendering ModelChoiceFields.
        """
        id_ = self.field.widget.attrs.get('id') or self.auto_id
        attrs = {'id': id_} if id_ else {}
        attrs = self.build_widget_attrs(attrs)
        return [
            BoundWidget(self.field.widget, widget, self.form.renderer)
            for widget in self.field.widget.subwidgets(self.html_name, self.value(), attrs=attrs)
        ]

    def __bool__(self):
        # BoundField evaluates to True even if it doesn't have subwidgets.
        return True

    def __iter__(self):
        return iter(self.subwidgets)

    def __len__(self):
        return len(self.subwidgets)

    def __getitem__(self, idx):
        # Prevent unnecessary reevaluation when accessing BoundField's attrs
        # from templates.
        if not isinstance(idx, (int, slice)):
            raise TypeError
        return self.subwidgets[idx]

    @property
    def errors(self):
        """
        Return an ErrorList (empty if there are no errors) for this field.
        """
        return self.form.errors.get(self.name, self.form.error_class())

    def as_widget(self, widget=None, attrs=None, only_initial=False):
        """
        Render the field by rendering the passed widget, adding any HTML
        attributes passed as attrs. If a widget isn't specified, use the
        field's default widget.
        """
        if not widget:
            widget = self.field.widget

        if self.field.localize:
            widget.is_localized = True

        attrs = attrs or {}
        attrs = self.build_widget_attrs(attrs, widget)
        auto_id = self.auto_id
        if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
            if not only_initial:
                attrs['id'] = auto_id
            else:
                attrs['id'] = self.html_initial_id

        if not only_initial:
            name = self.html_name
        else:
            name = self.html_initial_name

        kwargs = {}
        if func_supports_parameter(widget.render, 'renderer') or func_accepts_kwargs(widget.render):
            kwargs['renderer'] = self.form.renderer
        else:
            warnings.warn(
                'Add the `renderer` argument to the render() method of %s. '
                'It will be mandatory in Django 2.1.' % widget.__class__,
                RemovedInDjango21Warning, stacklevel=2,
            )
        return widget.render(
            name=name,
            value=self.value(),
            attrs=attrs,
            **kwargs
        )

    def as_text(self, attrs=None, **kwargs):
        """
        Return a string of HTML for representing this as an <input type="text">.
        """
        return self.as_widget(TextInput(), attrs, **kwargs)

    def as_textarea(self, attrs=None, **kwargs):
        """Return a string of HTML for representing this as a <textarea>."""
        return self.as_widget(Textarea(), attrs, **kwargs)

    def as_hidden(self, attrs=None, **kwargs):
        """
        Return a string of HTML for representing this as an <input type="hidden">.
        """
        return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)

    @property
    def data(self):
        """
        Return the data for this BoundField, or None if it wasn't given.
        """
        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)

    def value(self):
        """
        Return the value for this BoundField, using the initial value if
        the form is not bound or the data otherwise.
        """
        data = self.initial
        if self.form.is_bound:
            data = self.field.bound_data(self.data, data)
        return self.field.prepare_value(data)

    def label_tag(self, contents=None, attrs=None, label_suffix=None):
        """
        Wrap the given contents in a <label>, if the field has an ID attribute.
        contents should be mark_safe'd to avoid HTML escaping. If contents
        aren't given, use the field's HTML-escaped label.

        If attrs are given, use them as HTML attributes on the <label> tag.

        label_suffix overrides the form's label_suffix.
        """
        contents = contents or self.label
        if label_suffix is None:
            label_suffix = (self.field.label_suffix if self.field.label_suffix is not None
                            else self.form.label_suffix)
        # Only add the suffix if the label does not end in punctuation.
        # Translators: If found as last label character, these punctuation
        # characters will prevent the default label_suffix to be appended to the label
        if label_suffix and contents and contents[-1] not in _(':?.!'):
            contents = format_html('{}{}', contents, label_suffix)
        widget = self.field.widget
        id_ = widget.attrs.get('id') or self.auto_id
        if id_:
            id_for_label = widget.id_for_label(id_)
            if id_for_label:
                attrs = dict(attrs or {}, **{'for': id_for_label})
            if self.field.required and hasattr(self.form, 'required_css_class'):
                attrs = attrs or {}
                if 'class' in attrs:
                    attrs['class'] += ' ' + self.form.required_css_class
                else:
                    attrs['class'] = self.form.required_css_class
            attrs = flatatt(attrs) if attrs else ''
            contents = format_html('<label{}>{}</label>', attrs, contents)
        else:
            contents = conditional_escape(contents)
        return mark_safe(contents)

    def css_classes(self, extra_classes=None):
        """
        Return a string of space-separated CSS classes for this field.
        """
        if hasattr(extra_classes, 'split'):
            extra_classes = extra_classes.split()
        extra_classes = set(extra_classes or [])
        if self.errors and hasattr(self.form, 'error_css_class'):
            extra_classes.add(self.form.error_css_class)
        if self.field.required and hasattr(self.form, 'required_css_class'):
            extra_classes.add(self.form.required_css_class)
        return ' '.join(extra_classes)

    @property
    def is_hidden(self):
        """Return True if this BoundField's widget is hidden."""
        return self.field.widget.is_hidden

    @property
    def auto_id(self):
        """
        Calculate and return the ID attribute for this BoundField, if the
        associated Form has specified auto_id. Return an empty string otherwise.
        """
        auto_id = self.form.auto_id  # Boolean or string
        if auto_id and '%s' in str(auto_id):
            return auto_id % self.html_name
        elif auto_id:
            return self.html_name
        return ''

    @property
    def id_for_label(self):
        """
        Wrapper around the field widget's `id_for_label` method.
        Useful, for example, for focusing on this field regardless of whether
        it has a single widget or a MultiWidget.
        """
        widget = self.field.widget
        id_ = widget.attrs.get('id') or self.auto_id
        return widget.id_for_label(id_)

    @cached_property
    def initial(self):
        data = self.form.get_initial_for_field(self.field, self.name)
        # If this is an auto-generated default date, nix the microseconds for
        # standardized handling. See #22502.
        if (isinstance(data, (datetime.datetime, datetime.time)) and
                not self.field.widget.supports_microseconds):
            data = data.replace(microsecond=0)
        return data

    def build_widget_attrs(self, attrs, widget=None):
        if not widget:
            widget = self.field.widget
        attrs = dict(attrs)  # Copy attrs to avoid modifying the argument.
        if widget.use_required_attribute(self.initial) and self.field.required and self.form.use_required_attribute:
            attrs['required'] = True
        if self.field.disabled:
            attrs['disabled'] = True
        return attrs
BoundField

 其中有一些方法可以參考,像errors方法,這也就解釋了為什么字段可以直接調用該方法。

(二)自定義BaseForm

有些時候Form類中需要request或者其它參數,此時需要自定義BaseForm:

1、BaseRequestForm

class BaseRequestForm(object):
    def __init__(self, request, *args, **kwargs):
        self.request = request
        super(BaseRequestForm, self).__init__(*args, **kwargs)

2、BaseForm

class BaseForm(BaseRequestForm,forms.Form):

    def __init__(self, request,*args, **kwargs):
        super(BaseForm, self).__init__(request,*args, **kwargs)
        # 統一給Form生成字段添加樣式
        for name, field in self.fields.items():
            field.widget.attrs['class'] = 'form-control'

3、使用

from django.core.exceptions import ValidationError
from django import forms 
from django.forms import fields 

class LoginForm(BaseForm):

    username = fields.CharField()
    password = fields.CharField()
    code = django_fields.CharField(
        error_messages={'required': '驗證碼不能為空.'}
    )

 #從request的session中取出驗證碼進行驗證    
    def clean_code(self):
        if self.request.session.get('CheckCode').upper() != self.request.POST.get('code').upper():
            raise ValidationError(message='驗證碼錯誤', code='invalid')

在視圖中進行LoginForm實例化時傳入request參數即可:

 form = LoginForm(request=request, data=request.POST)

 

參考文章:https://docs.djangoproject.com/en/2.2/ref/forms/fields/

      https://www.cnblogs.com/wupeiqi/articles/6144178.html

 

                                                                                                                 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM