Django学习之八:forms组件【对form舒心了】

目录

  • Django forms组件
    • bound and unbound form instance
    • forms渲染有关
      • 隐藏一个字段,不渲染它
    • form 校验
    • form类
      • ModelForm
      • 利用ModelForm关键就在于model's field mapping to form's field
      • ModelForm.save() 详解
      • class Meta !!!重写覆盖默认的modelField字段(即自定义一些modelform属性)
      • form有关多选择Field的使用
    • form's fields are themselves classes
      • Field class
      • 将form校验错误信息改为中文。
      • BoundField class
      • FileField /ImageField /DateField
    • API
    • widgets class
    • rendering form error messages
    • 定义自己form 实例 包括自定义 局部和全局 钩子
    • reusable form templates
    • 遗留问题
    • 碰到的错误
    • 总结

Django forms组件

    Handle (掌控)一个form是非常复杂的工程,需要做很多功能:不同的类型的数据要有不同的渲染;校验数据;获取检验后的干净数据,并将数据反序列化为相应数据类型如时间对象;保存传递给处理程序等等。Django的forms组件就完成了这些复杂的工作,提供方便的操作form的接口API给我们。

    Form对象有很多的API,参考本文API段落。其实每个API都是对应了Form的一个特点。如:form.auto_id 对应就是设置form中表单标签的id属性;form.errors 对应了form的校验和错误信息。等等。官方文档在讲解Form对象API时,也是按照form的功能和特点,来分类介绍每种API的。

bound and unbound form instance

绑定数据的form实例和没绑定数据的form实例,他们之间的区别是非常重要的,这影响到了,同一个api或者属性,在templates 引擎渲染和weight作用时所表现出来的内容是不同的。
一个form instance 要么绑定要么没绑定

  • 绑定了数据的: 可以调用is_valid等校验api;并且通过该实例可以渲染出html。并且包括inline error messages 校验失败的错误可以渲染到表单后,已提示表单提交用户。
  • 没绑定数据的:不能校验(因为没绑定数据),but it can still render the blank form as html但是它还是能渲染空白内容的表单空间。没有绑定的form是没有cleaned_data属性的。访问的话会抛出异常。
  • 什么是绑定数据行为?通过form类实例化form对象时,需要提供一个字典类型(映射类型)的数据作为第一个位置参数,如{'name':'ZJQ', 'age': 300} 或 request.POST等。这样初始化的form对象就是绑定了数据的form实例,即使提供一个空的{} 也算是提供了。 没有提供这样一个参数,则实例化出来的是一个没有绑定数据的form实例。
  • 绑定数据的form对象或者没有绑定数据的form对象,可以改变其绑定数据值或者添加绑定数据吗?答案是:NO!. 一旦一个Form 实例对象创建了,要知道它的数据是immutable不可改变的,无论是绑定还是非绑定数据from对象。

博文图片挂了临时解决办法
Django学习之八:forms组件【对form舒心了】_第1张图片

forms渲染有关

注意:form对象迭代出来的数据类型。form对象是可迭代的对象,迭代出的是boundfield对象。form对象又是字典类型对象,key是字段名,value是boundfield对象。所以要获取boundfield对象有两种途径,通过for迭代,或者通过字典key访问。以下的field名字没特殊说明,都是boundfield对象。至于获取boundfield对象,刚刚也提到了。下面就来使用它的属性和方法吧:

  1. field.label 是label值,不包括label标签
  2. field.label_tag() 就是一个返回label标签的方法,包含了label值;在渲染标签是指定参数attrs={'class':'foo'} 就能指定标签css class,还可以指定label_suffix=‘::’ 来设置添加label值的后缀.
  3. form相当于整个表单,打印form对象就是一个HTML字符串。
  4. field 打印就是一个表单控件的HTML字符串。
  5. form是可以迭代的,迭代出就是boundfield对象。迭代顺序就是form定义的field的顺序。如果要访问某个具体的定义form时的field对象(非boundfield对象)通过form.fields['字段名'] 可以得到。通过boundfield.field也可以拿到对应的字段对象。(区分form定义时的字段对象,和实例化后的boundfield对象)
  6. 关于检验失败的错误信息: 通过field.errors拿到。这个拿到的是一个错误集合(或者说错误列表),通过访问改错误列表才能拿到错误。其它拿错误的方式也是一样的。
  7. forms对象在template中的渲染是不会有<\form>标签的。因为form不止可以渲染成表单,还可以渲染成table({{ form.as_table }});如下:
There are other output options though for the 
  • tags 相应的,都必须自己提供table或这ul
    1. field.id_for_label 这个是获取label应该设置的对应input的id。
    2. field.errors 打印的话会渲染表单错误为一个无序列表,列表的ul会有一个class='errorlist' ,这个需要用户来定义这个 css class 应该这样显示。 由于这个其实是一个错误列表,所以循环来自己渲染错误,通过循环迭代,拿到具体的错误字符串。
    3. form.non_field_errors()表示表单校验时的非field错误,即全局钩子错误或自己添加的错误。
    4. field.value() 就可以拿到表单具体的value所对应的值或非绑定设置的初始化值。
    5. 在python代码中打印form对象都是由<\tr><\th>包裹的,而template中使用是没有这些标签包裹的。
    6. form.errors 是一个字典(区别对比field.errors),包含所有字段的错误,key就是字段名,对应的value是一个错误列表。特别注意一个全局钩子的错误放在一个key叫做'__all__'中。注意获取form.errors就会触发form的校验,类似is_valid() 触发一样。同时校验过程也只会发生过一次,对于一个form对象。form.errors有很多的接口,可以获取为json字符串form.errors.as_json();参考:https://docs.djangoproject.com/en/2.0/ref/forms/api/#django.forms.Form.errors
    7. 更多模版中渲染有关可以参考:https://docs.djangoproject.com/en/2.0/topics/forms/#looping-over-the-form-s-fields
    8. form 渲染时的注意事项:
      Django学习之八:forms组件【对form舒心了】_第2张图片

    隐藏一个字段,不渲染它

    pass

    form 校验

    所谓校验,就是绑定到form对象的数据,校验其是否符合定义的约束条件。
    关于校验方面,要明白的点:

    1. 对于绑定了数据的form对象,可以进行校验其绑定的数据是否匹配form定义的字段的类型和其它约束。form_obj.is_valid() 和 form_obj.errors 的调用执行 都会隐式触发form_obj的校验过程;而想显示的调用校验可以通过from_obj.full_clean()。要明白一个form的校验过程只会进行一次,校验过了就不会在校验了,直接返回结果。所以form_obj.is_valid() 和 form_obj.errors 谁先调用,谁触发有且仅有一次的校验过程。如果有单个字段对象,可以测试调用单个字段对象的clean(校验数据) 方式,参数就是要校验的数据。
    2. 校验数据完后,结果无非两种情况:a) 校验通过,django对校验过了的form_obj,会将通过了的数据放入form_obj.cleaned_data 字典中。b) 校验不通过, django对这没通过的form_obj 也会将部分校验通过的放入from_obj.cleaned_data 。对于校验失败了的字段,会将错误存放到form_obj.errors 字典中,字典{'字段名1':['错误信息1','错误信息2']}。
    3. 问题:绑定了数据了的字段,会校验哪些方面?第一,定义字段时的一些约束;第二,局部钩子;第三,全局钩子;所以错误信息的字典,主键key是字段,错误信息是一个列表。而全局不是单个字段的,所以Key是不是一个字段名而是‘__all__’ 作为字典key。
    4. 没校验通过的form_obj可以用于渲染,将错误信息和验证过的信息都渲染到form表单中,不会出现form表单没有校验通过,就将部分校验通过的数据也清空掉,会保留校验通过的数据,只清空没有通过的字段的数据。而且没通过的错误信息还用于渲染到表单页面中,提醒提交表单的用户错误。
    5. 除了form.errors存放全部的校验错误信息外。每个field对象也有一个errors属性,里面存放了字段对象的错误信息,是以一个list列表存放的。
    6. form校验的错误信息的返回格式还可以有多种,有form.errros.as_json()得到一个json字符串,特别是对于ajax提交的form数据,响应错误通过这种方式。
    7. form错误是会有一个ul标签来组织错误信息的。
    8. 可以通过直接实例化一个Field对象,通过调用其clean(传入值) 来校验数据是否符合。

    form类

    继承关系:
    Django学习之八:forms组件【对form舒心了】_第3张图片

    1. from django import forms 导入模块
    2. 继承Form类,构造一个自己的表单类。类似于Models类,django通过model操作数据库表。Form对象这是一个表单对象,通过该对象来操纵表单处理的过程,如校验表单字段,渲染表单字段。主要就对这两方面进行操纵。
    3. 关于提交的表单数据的校验,提供了自定义全局和局部钩子,提供了丰富的内置Field类和其对应的widget来约束表单提交的数据。(插曲:所谓钩子,就是访问入口规定好了,我们就添加入口里面的东西就可以了)
    4. 局部钩子注意获取到校验值,进行校验后,符合要返回该值,不符合抛出一个指定的异常 ValidationError 异常
    5. 全局钩子主要用于每个空间的值都局部校验后,进行一个全局校验,比如两次密码是否一样。这样就不必在从clean_data取出来比较了。如果校验成功过,注意返回的是clean_data,失败同样抛出ValidationError异常。全局校验错误信息是存在form.errrors 的__all__的一个key对应的列表中。
    6. is_valid clean_name errors
    7. 关于渲染表单 form为每个field提供了相对应的一个默认widget。当然也可以自定义,在定义form字段是,可以带入参数widget指定widget类或该类的实例对象。如果传入的是widget类,那么会自动实例一个默认的widget对象用于字段渲染。如果传入的是实例,就按照实例的渲染方式进行渲染。
    8. 表单渲染主要就是field对应的widget的作用。当然内置的多种widget都可以传入相同的参数来改变渲染效果,如attrs={'class':'form-control'} 就会给相应标签添加属性。
    9. 表单渲染添加css class可以通过widget。而<\label> 和 错误 通过定义form类是添加类属性 error_css_class 和 required_css_class 明天实验这两个hook钩子????
    10. 其实还是不要用完整的,就用他们的label值和错误值,只用field的渲染就好了。
    11. ValidationError导入使用from django.coreexceptions import ValidationError
    12. 内置widget都在forms模块中。

    form 的实例,可以是空,也可以提前填充数据。归纳总结form实例化数据主要来自三个方面:

    1. 来自model instance
    2. 来自其它数据源
    3. 来自用户提交的表单数据。这种情况通常给用户一个空form后,用户提交,如果无效,再返回一个绑定了数据的form给用户。

    ModelForm

    出现modelform 这种form类的情况是这样的:
         如果你正在开发基于数据库的web app, 很有可能, 你会创建一个forms 是几乎映射到一个django models的。例如,你可能有一个BlogComment model, 然后,你想创建一个form 让用户通过这个form提交博客评论到BlogComment model的表中。在这个例子中,定义form的field types是一种很冗余的做法(django 哲学之避免冗余),因为你已经定义了model的field type了,可以复用给form用。

         因为这个原因, django 提供了一个很有帮助的 class 可以让我们创建一个Form class 通过一个django 的model。这样就复用了django model 中的field的定义。

    代码实例:

    >>> from django.forms import ModelForm
    >>> from myapp.models import Article  # 导入自己建好的django model  
    
    # 创建form class
    >>> class ArticleForm(ModelForm):
    ...     class Meta:
    ...         model = Article
    ...         fields = ['pub_date', 'headline', 'content', 'reporter']
    
    # 创建一个form 用于添加文章
    >>> form = ArticleForm()
    
    # 创建一个form 用于改变一个存在的文章
    >>> article = Article.objects.get(pk=1)
    >>> form = ArticleForm(instance=article)  # 
    
    # 上面创建的两个form都可以用于渲染到模版中,分别用于新增和修改。

    小结:注意这里modelform和普通form实例化的不同。这里传入一个model object instance作为将用于初始化显示的数据。也可以像普通 form一样,传入initail参数。如果两个参数都传递了的话,那么就变成了第三种情况,不过initial会覆盖instance的初始化。
    如果实例化绑定数据时,提供了instance参数,那么在save时就是一个update操作数据库。如果只是给了一个类字典的数据没有instance,那么就是insert新增数据到数据库。
    所以,在实例化modelform时,instance参数除了会影响save()的行为,还会影响初始化参数initail的效果。

    利用ModelForm关键就在于model's field mapping to form's field

         每一个model field 有一个与之对应的缺省form field。例如, 一个在model中的CharField 被表示为 一个在form中的CharField. 而一个model ManyToManyField 被表示为 一个form的ModelMultipleChoiceField.

    缺省对应关系如下图:
    Django学习之八:forms组件【对form舒心了】_第4张图片

    正如你所想的,ForeignKey 和 ManyToManyField model field 类型是特殊情况(OneToOne这则不会有这样的特殊情况):

    • ForeignKey 通过django.forms.ModelChoiceField所表示,这个实际是一个Choice Field,特殊是它的choices 是一个model QuerySet 也就是一个查询出的queryset结果。对于这种ModelChoiceField。在modelform对象层面和ModelChoiceField层面,进行数据绑定和数据clean()校验是不同的。modelform层面实例化是要提供一个queryset作为代替choice参数,利用queryset生成choices。

    • ManyToManyField 通过django.forms.ModelMutipleChoiceField 所表示,这个实际是一个MultipleChoiceField,只是它的choices参数是变为了queryset参数,提供一个queryset对象。因为这个queryset对象可以构建出choice。
    • 至于两种Field在校验后,会将单个model对象(对于ModelChoiceField) 和 多个model对象(对于ModelMutipleChoiceField) 存入到cleand_data中。供后续使用。
    • 而且这两类Field对象,就有了queryset属性,这是一个queryset对象。通过这个queryset的API就可以得到对应关联的model的信息了。如:BoundField.field.queryset.model 就是model class了。这个是访问关联model的重要途径了。
    • ModelMutipleChoiceField和ModelChoiceField 对应的choice显示调用的queryset中model对象的__str__() 方法的结果,所以model定义时,约定都是要定义__str()__方法。
    • ModelMutipleChoiceField和ModelChoiceField都有一个可选参数,empty_label 主要用于控制对应select表单的一个空白选项的显示。默认是'-------------'

    此外,每一个通过model方式生成的modelform field 会设置如下属性:

    1. 如果 model field 有 blank=True, 相应的form将设置required=False。否则,required=True.
    2. 这个继承自ModelForm的form的field 的label属性会被设置为model field的verbose_name属性,并且值将是首字母大写
    3. 而help_text 属性值两者都有,就一一对应了。
    4. 如果 model field 有choices 属性设置,这是fomr field's widget 将被设置为Select,该form字段的choices将来自model字段的choices。这个choices 一般会包括一个blank choice代表的是model的默认值。如果field是required,将强制用户做出选择。The blank choice will not be included if the model field has blank=False and an explicit default value(the default value will be initially selected instead).

    一个完整的实例定义ModelForm:

    # model的 
    from django.db import models
    from django.forms import ModelForm
    
    TITLE_CHOICES = (
        ('MR', 'Mr.'),
        ('MRS', 'Mrs.'),
        ('MS', 'Ms.'),
    )
    
    class Author(models.Model):
        name = models.CharField(max_length=100)
        title = models.CharField(max_length=3, choices=TITLE_CHOICES) # 注意model和form的字段中都有choices这个属性,理解不同与相关。
        birth_date = models.DateField(blank=True, null=True)
    
        def __str__(self):
            return self.name
     
    class Book(models.Model):
        name = models.CharField(max_length=100)
        authors = models.ManyToManyField(Author)
    
    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = ['name', 'title', 'birth_date']
    
    class BookForm(ModelForm):
        class Meta:
            model = Book
            fields = ['name', 'authors']

    ModelForm.save() 详解

    • modelform由于和model产生了关系,所以多出了api来操作model。比如说form.save()可以保存或者更新form数据到数据库中。这里研究研究。这是modelform非常重要的一点,因为我们得到form正确数据后,是要同步到数据库中的,我们不可能将数据再一一ORM操作到数据库中。所以对于form提交的数据提供了这个save() 同步操作到数据库中。但是要注意对应数据库的新增修改操作,在save时是有不同逻辑的,你自己想想也是,如果是提交的数据和库中存量的数据有约束冲突,那必须解决这个冲突;至于新增就简单了直接插入insert就可以了。
    • save() 创建并保存一个model object instance (利用绑定到该modelform的数据)。绑定到modelform数据有两种方式,一种是普通方式,一种是绑定一个相同model类的实例对象(这种方式多用于修改视图的表单)。对于普通方式,save() 将创建一个new instance of the specificed model 也就是利用提供的数据,实例出一个model object,然后save就会保存新增加一个。对于绑定了对象的方式,如果提供了数据且提供了instance实例,这是update这个实例对象。对于普通模式在ORM操作时出现了主键等冲突,就会save()操作报错。
    • 通过modelform的save操作对应的model对象,关更新操作在实例化modelform时必须带上instance参数指明是更新的哪个model 对象数据,不然会编程新增,失去了想要修改操作的意图。如form(request.POST, instance=model_obj) 这样实例化modelform表单对象才行。
    • 自定义集成成ModelForm的类,其class Meta中的fields是使用哪些对应model的字段应用到modelform中。如果fields = ‘__all__'就是全部model字段应用到。
    • save() 接受一个可选的参数commit,参数取值可以是True or False,如果是False,那么方法这个save方法会返回一个model 对象,而不会同步到数据库中,这是就只有手动调用model对象的save() 方法去同步到数据库中。这样就提供了一个方式,可以修改对象,再提交到库中。还有一种情况,如果有一个manytomany字段,创建对象,建立关系可能需要先构建关联表中的数据后,才能保存。这时候可以调用modelform.save_m2m()方法保存对象并建立关系数据到中间表。对于commit=True,就没有上面说的两种情况,就直接同步数据库中。

    小结:modelform初始化时可以使用initial初始化数据吗?可以的,如果还提供了instance参数用于初始化的话,那么initial优先于instance参数中的值。
    modelform的方法和属性除了增加save和save_m2m区别之外,其它和普通form对象API一样。

    class Meta !!!重写覆盖默认的modelField字段(即自定义一些modelform属性)

    通过class Meta可以定义覆盖默认的一些modelField的元素。
    大致在Meta中的属性有:
    model = 映射的model class
    fields = ['fieldname1', 'fieldname2'...] 全部可以设置为['__all__']
    widgets = {'fieldname': widget_obj,...}
    labels = {'fieldname':label_value} 设置渲染时的label值
    help_text = {'fieldname': help_string}
    error_messages = {'fieldname': {'校验code':错误信息}} 通过这个可以改变错误信息为自定义中文
    field_classes = {'fieldname': FieldClass}

    如果要完全覆盖一个字段,就在modelfrom中建立一个字段的定义就会完全覆盖modelform默认生成的。

    参考:https://docs.djangoproject.com/en/2.0/topics/forms/modelforms/#overriding-the-default-fields

    form有关多选择Field的使用

    form表单中可以使用input-checkbox 和 select-option 及 input-radio 来实现多值或者提供选择项给用户展示。
    form表单基本上可以对应数据库中一个表的一条数据。因为数据库中一个表的数据可能关联到其它表的数据(就是常说的manyTomany,manyToone,oneToone)。要通过form表单,操控数据库一条数据,那么表单就要有展示或者操控数据关系的方式。这种方式就是表单的上面提到的三种表单控件了。

    再看回django的form组件。主要就是ChoiceField/ModelChoiceField/ModelMultipleChoiceField的使用。
    三者的区别:

    • ChoiceField对应参数choice,就是要提供一个choice参数。
    • ModelChoiceField对应的是一个queryset参数。**主要利用的是queryset中model对象的pk和对象的__str__的输出。**这个很重要,开始在使用时,要提供一个queryset参数。
    • ModelMultipleChoiceField对应的也是queryset参数,类似ModelChoiceField只不其widget是一个MultipleSelectWidget罢了。

    上面三种Field对应的表单控件默认都是Select,
    而对于要使用input-check,就要给字段重新赋值widget参数为一个Check类型的widget。

    form's fields are themselves classes

    Field class

    Field类实例化对象时,核心参数就五个:

    1. required 是否是必须有数据。用于校验
    2. label 表单的的贴条。主要用于贴示 数据是什么信息。默认是字段名。
    3. widget 主要是表单的渲染,和部分校验。
    4. initial 初始化数据。用于初始化默认值。为后续has_changed()提供对比依据。
    5. help_text
      通过form访问Field对象:form.fields['field名字']

    将form校验错误信息改为中文。

    由于错误提示校验是分类的,每种类型字段有哪几种校验错误,可以到官网查询https://docs.djangoproject.com/en/2.1/ref/forms/fields/#built-in-field-classes。
    知道要改变哪种类型的错误提示后,就在定义field是设置error_messages={'错误类型': 错误信息!}

    BoundField class

    这个BoundField 类 ,主要用于展示HTML 或者 用于访问form实例的一个Field对象 属性。

    BoundField Used to display HTML or access attributes for a single field of a Form instance.
    通过BoundField.field 访问到Field对象。
    这个类的__str__() 就是展示 字段的HTML。所以打印BoundField对象就输出了HTML。
    通过form访问BoundField对象,可以遍历,也可通过字典key操作,因为form是一个类字典的类型。key就是字段字符串啦。

    form中的field负责管理表单数据和表单数据的校验当一个表单被提交后。

    FileField /ImageField /DateField

    和其它的Field不同,有两个特别的Field类型:DateField类与FileField(类似于model中的FileField和ImageField字段比较特别,因为都涉及到文件对象)

    在前端页面,需要通过form上传文件,就需要确定form标签的enctype定义了正确的值“multipart/form-data” 现代浏览器对于有文件的上传都会使用这种编码。
    这样,才能使用正确的格式编码 form表单中的文件对象和其它数据 到http body中,然后通过http协议传输到服务端,服务端也能正确通过编码方式进行解码,才能正确解析出文件对象和其它数据。

    • DateField https://docs.djangoproject.com/en/2.0/ref/forms/fields/#django.forms.DateField

      在normalized转化到python 对象时,需要特别注意该字段。这个字段会将用户表单中填入的字符串,转化为date对象。(用户的键入只能是字符串形式)。
      这个转化过程肯定也是要有依据的,得按照依据规则来,不可能用户随便输入什么字符都能转换换成date对象是吧。所以这个字段在初始化时,需要一个可选参数就是input_formats。提供专业的to_python和to_html 的格式。提供了这些格式,用户输入的时间字符串,就需要按照列表中的格式化提供时间字符串。同时,绑定了值的渲染到页面也是按照其中的格式来的。由于这个时间格式的表示范式,全球各地是不同的,所以会根据整个django项目的F10N参数,来判定默认的input_formats规则是什么。如果F10N=True ,那么input_formats只能用全球同一认可的格式。如下:
      ['%Y-%m-%d', # '2006-10-25'
      '%m/%d/%Y', # '10/25/2006'
      '%m/%d/%y'] # '10/25/06'
      如果F10N=False,那么就会有更多的格式支持。
      ['%b %d %Y', # 'Oct 25 2006'
      '%b %d, %Y', # 'Oct 25, 2006'
      '%d %b %Y', # '25 Oct 2006'
      '%d %b, %Y', # '25 Oct, 2006'
      '%B %d %Y', # 'October 25 2006'
      '%B %d, %Y', # 'October 25, 2006'
      '%d %B %Y', # '25 October 2006'
      '%d %B, %Y'] # '25 October, 2006'

    • ImageField

      对于该字段,实例化时除了带入request.POST外,还需要request.FILES. 也就是要通过form 来handle 上传的文件,需要将文件绑定到form相应的imagefield。

    • FileField https://docs.djangoproject.com/en/2.0/ref/forms/fields/#django.forms.FileField

      对于该字段,实例化时除了带入request.POST外,还需要request.FILES. 也就是要通过form 来handle 上传的文件,需要将文件绑定到form相应的filefield。

    FileField可选参数max_length限制文件对象的文件名。allow_empty_file,文件内容可以为空。

    • 由于FiledField 和ImageField处理类似,这里就已ImageField的form绑定 上传文件为例,来演示实例化这一个form:
    # Bound form with an image field
    >>> from django.core.files.uploadedfile import SimpleUploadedFile
    >>> data = {'subject': 'hello',
    ...         'message': 'Hi there',
    ...         'sender': '[email protected]',
    ...         'cc_myself': True}
    >>> file_data = {'mugshot': SimpleUploadedFile('face.jpg', )}
    ## 这里的file_data中的value是一个SimpleuploadedFile对象,对象实例,提供了文件的名字和文件的句柄作为参数。
    >>> f = ContactFormWithMugshot(data, file_data)

    而在实战中,request为我们handle好了像SimpleuploadFile对象,就是request.FILES.

    f = ContactFormWithMugshot(request.POST, request.FILES)

    问题来了,那如果是一个modelform呢?怎么实例化这样一个带有文件对象的modelform?答:同普通form一样,多带入一个request.FILES。
    modelform 是哪个字段映射到FileField字段呢?
    答:也是form的FileField对应
    那实例化提供了SimpleuploadFile对象后,有怎么通过save()保存到对应的数据库表中记录呢?就算不是modelform,普通的form,是怎么将上传的文件保存在哪里呢?
    猜测,这些可能就是UploadFile对象封装了这些繁琐的事情了吧?后续验证。

    API

    特别提醒:注意将form instance api 与 bound field api 对比查看。
    form instance api

    1. form.has_change() 返回True或False。和初始化对比变化。
    2. form.fields是一个字典类型,存放这form定义时的field对象,注意不是boundfield对象(也就不是迭代form对象的产物)。
    3. 打印form 输出一个HTML
    4. form.label_suffix 设置每个label值后面跟什么字符串,默认是冒号‘:’
    5. form['字段名'] 得到对应的BoundField对象。
    6. form.fields['字段名'] 得到对应的Field对象。
    7. form.is_valid() 返回bool值 True校验通过(包括自定义钩子);Flase校验失败。
    8. form.add_error(field, error) 给参数指定的字段添加错误。如果字段是校验过的,添加错误会将字段数据从form.cleaned_data中删除。
    9. form.errors.as_json() 返回json字符串格式的错误信息
    10. form.has_error(field, code=None) 判定字段是否有指定code的错误。
    11. form.non_field_errors() 返回 全局钩子校验失败的错误信息列表或者通过form.has_error(None,'...) 添加的。记住这个方法,对于容易忘记全局钩子错误的KEY是什么的人,是福利。
    12. form.cleaned_data 得到校验干净的数据,数据会格式化为对应的python对象类型。
    13. form.initial 是初始化数据字典。注意初始化是是不会将form变为绑定数据的form的。
    14. form.error_css_class 属性定义是在form中的类属性,主要是给每行的<\tr>标签添加错误信息时的class。
    15. form.required_css_class 属性定义是在form中的类属性,主要就是给<\label>标签添加css class
    16. form.auto_id 有三种值,True,Flase,String。这个主要控制label标签for属性 和 input等标签中的id属性的。如果是True,值就会是字段名。如果是False就不会有id属性。如果是'id_for_%s' 这一类的格式化字符串,那么%s会被字段名替换,构成一个id值给标签属性中用。
    17. form.use_required_attribute 设置为True这默认全部都有required属性,如果是False默认全部都没有required属性。但是对单个字段定义时的required是没有影响的。
    18. form.field_order 设置一个列表,加入字段来定义渲染是字段的顺序。
    19. form.order_fields(field_order) 通过这个api可以随时改变form中字段的顺序。
    20. form.is_multipart() 返回True是 说明form中有文件类字段,False这是普通form.

    bound field api

    1. boundfield.has_change()
    2. boundfield.field 而遍历form时得到的foundfield对象也可以得到form定义时的field对象,就是boundfield.field。
    3. 打印boundfield 输出一个HTML
    4. boundfield.label_suffix 设置当前label值后面跟什么字符串,默认是冒号‘:’
    5. boundfield.value() 返回当前字段的值,绑定了数据的,就返回绑定了的值;不然返回初始化时提供的值,再不然就返回一个None。
    6. boundfield.auto_id 设置field自己的id形式。
    7. boundfield.data 类似boundfield.value() 方法,都是返回对应的数据。区别是,只有绑定数据才会有值,其它任何情况都是None.小结就是:value()拿到渲染后能看到的值。data拿到绑定了的值。
    8. boundfield.errors 是一个类列表对象。这就到了__str__ 和__repr__ 的区别了.打印的话会call str方法,输出html字符串。只是值的化就是走repr,打印出来就是一个字典字面值。这个同form.errors是一样的。
    9. boundfield.form 就是boundfield所在的form对象
    10. boundfield.help_text 就是field定义是的help_text.如果有定义,可以在渲染是放在input后面作为提示帮助信息。
    11. boundfield.html_name
    12. boundfield.id_for_label 对应字段的id属性值。用于自定义<\label>标签
    13. boundfield.is_hidden 判定是否是隐藏字段
    14. boundfield.label 字段的label自,默认是字段名
    15. boundfield.name field字段的名字。
    16. boundfield.label_tag(contents=None, attrs=None, label_suffix=None)
    17. boundfield.as_hidden()
    18. boundfield.as_widget()
    19. boundfield.css_classes()

    小结:对比后发现,无论是has_changed, as_table ,initail参数等。这些都是form 与 boundfield之间的关系。如form level 的调用会去调用boundfield的对应的调用,has_changed();还是form level的initial初始化字典,影响field的初始化。就连form.as_table / form.as_ul 等,对应都是boundfield的 as_...方式的调用。还有,如果整个form容器里面的boundfield对象都要改变的属性或者特点,那么通过form instance api 进行容器空间全局控制,如果是单个boundfield的改变,就通过对象自己,同时会覆盖form全局的设置。

    widgets class

    每种类型的Field class 都有一个默认对应的Widget class。
    为什么这样设计?为什么不Field class 把这个干完呢?
    因为一个Field class可以有多种Widget表现,即Field 可以和Widget任意匹配,这样能适应的情况就更多了,不然就有超多的细分类型的Field class。同时也可以把一些功能解耦出去。
    后面用的多了再总结这一part

    rendering form error messages

    django官方也一直没定下怎么渲染表单验证错误信息。

    关于设置错误校验错误信息为中文:
    pass 就是通过error_messages

    定义自己form 实例 包括自定义 局部和全局 钩子

    from django import forms
    from django.forms import widgets
    from django.core.exceptions import ValidationError
    
    
    wid_01 = widgets.TextInput(attrs={"class":"form-control"})
    wid_02 = widgets.PasswordInput(attrs={"class":"form-control"})
    wid_03 = widgets.EmailInput(attrs={"class":"form-control"})
    
    class RegForms(forms.Form):
        username = forms.CharField(min_length=4, widget=wid_01)
        password = forms.CharField(min_length=4, widget=wid_01)
        r_password = forms.CharField(min_length=4, widget=wid_01)
        email = forms.EmailField(widget=wid_03)
        telephone = forms.CharField(required=False,widget=wid_01)
        school = forms.ChoiceField(choices=[('male', '男'), ('female', '女')])
        age = forms.BooleanField()
        area = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple(attrs={'class': 'checkbox-inline'}), choices=(('china', '中国'), ('america', '美国'), ('english', '英国')))
    
        def clean_telephone(self):
            val = self.cleaned_data.get('telephone')
            if len(val) == 11 :
                return val
            else:
                raise ValidationError('手机号格式错误!')
    
        def clean(self):
            pwd = self.cleaned_data.get('password')
            r_pwd = self.cleaned_data.get('r_password')
    
            if pwd and r_pwd:
                if pwd == r_pwd :
                    return self.cleaned_data
                else:
                    raise ValidationError('两次密码不相同!')
            return self.cleaned_data

    注意:这里抛出错误是不规范,也是官方不推荐的,官方推荐抛入ValidationError方式,参考:https://docs.djangoproject.com/en/2.1/ref/forms/validation/#raising-validationerror

    reusable form templates

    使用include和 include 。。。 with form = comment_form

    遗留问题

    1. 查看form类的元类 DeclarativeFieldsMetaclass
    2. 怎么给label标签添加class属性,或者只有取label值。以解决,第一form对象的required_css_class属性设置。或者定义form类是添加required_css_class类属性。

    碰到的错误

    1. form.is_valid()是一个方法不是一个属性,千万不要忘记后加上()呀。因为is_valid()调用后才会有clean_data数据(其实获取form.errors也会产生clean_data数据)。
    2. 有关前端的遗漏只是点,就是form标签有一个属性叫 novalidate .加上它之后,前端就不会对required表单进行提示不能为空。添加它的作用,主要是用于方便测试后端form对象对表单的校验空值的功能,而不是前端就提示了。

    总结

    • 发现form就是容器,存放field对象。form和field看成两个Level。很多时候form有的方法,field对象也有。如都有has_changed();都可以设置initial参数等。
    • form的校验数据,不仅仅是校验,还有清洗数据的作用,比如将提交的字符串,转换成对应field类型的数据对象。如日期字符串,通过cleaned_data后,得到的是一个datetime.date的对象。
    • 我觉得,在定义form类时,字段赋值的是一个如CharField的对象。这个对象有包含了Widget对象。也就是CharField对象主要用于校对和渲染的功能。而form实例化后,form迭代出来的是一个叫bounfield的对象,这个对象应该是绑定数据的一个对象,这个boundfield对象是包裹了CharField对象数据的。通过boundfield对象是不能改变其绑定的数据的,但是渲染是可以改变的,就可以通过改变CharField对象的属性,来改变最后利用boundfield对象渲染的输出。如:
    You can access the fields of Form instance from its fields attribute:
    
    >>> for row in f.fields.values(): print(row)
    ...
    
    
    
    >>> f.fields['name']
    
    You can alter the field of Form instance to change the way it is presented in the form:
    
    >>> f.as_table().split('\n')[0]
    'Name:'
    >>> f.fields['name'].label = "Username"  ## 这里修改了CharField对象,最后使用影响了渲染。虽然影响不了绑定的数据。
    >>> f.as_table().split('\n')[0]
    'Username:'
    

    官方文档也说明了boundfield就是wraps包裹form定义的field的。作用就是展示出

    • 所以说要特别注意在form中,说道的field是class定义时的field,还是实例化后form对象迭代出的boundfield对象。form和boundfield两个不同level都是可以得到class定义时的field对象的;通过改变class定义时的field对象的属性,可以影响最后form渲染的效果。
    • 有个心得:Model class 的属性是Field对象(特别是关联Field对象)。Model instance 的属性是值或者Manager管理器;Form class 的属性是Field对象。Form instance 迭代出来的是BoundField对象。

    你可能感兴趣的:(Django学习之八:forms组件【对form舒心了】)