回顾

Form主要的作用,是做数据验证的。并且Form的数据验证功能是强大的。
Form还有另外一个功能,就是帮我么生成html标签。
上面的2个功能,其中验证是主要的,而生成html标签的功能有时候用的到,有时候不需要。建议使用新URL方式(一般是Form表单提交)来操作的时候使用生成html标签的功能,因为这个功能可以帮我么保留上一次提交的值。使用Ajax请求操作的时候,就可以不用Form来帮我们生成html标签了,因为Ajax提交的时候页面不会刷新,页面上不会丢失上一次提交的值。以上是建议,具体用的时候可以尝试一下,感觉不按建议来也许可以,但是至少会麻烦一点。
讲师的博客地址:http://www.cnblogs.com/wupeiqi/articles/6144178.html

创建一个Form

建议在app目录下新建一个forms.py文件来单独存放我们的form文件:

from django.forms import Form
from django.forms import fields
from django.forms import widgets

class User(Form):
    username = fields.CharField(
        required=True,  # 必填,不能为空,不过True是默认参数,所以不写也一样
        widget=widgets.PasswordInput(attrs={'class': 'c1'})  # 在生成html标签的时候,添加标签的属性
    )
    password = fields.CharField(
        max_length=12,
        widget=widgets.PasswordInput(attrs={'class': 'c1'})
    )

操作生成select标签的Form

# 在 models.py 文件了创建表结构
class UserType(models.Model):
    name = models.CharField(max_length=16)  # 这个是用户类型,比如:普通用户、超级用户

# 在 forms.py 文件里创建Form
class UserInfo(Form):
    name = fields.CharField()
    comments = fields.CharField(
        required=False,
        widget=widgets.Textarea(attrs={'class': 'c1'})
    )
    type = fields.ChoiceField(
        # choices=[(1, "超级用户"), (2, "普通用户")]  # 之前是去内存里的选项,想在用下面的方法直接去数据库里取
        # 列表里每一个元素都是元组,如果选项是去数据库获取的话,用.values_list()方法就能直接获取到
        choices=models.UserType.objects.values_list()
    )

# 在 views.py 里创建视图函数
def user_info(request):
    obj = forms.UserInfo()
    return render(request, 'user_info.html', {'obj': obj})

然后再写一个简单的html界面,主要看生成的select标签:

<body>
<p>{{ obj.name }}</p>
<p>{{ obj.comments }}</p>
<p>{{ obj.type }}</p>
</body>

直接用 choices=[(1, "超级用户"), (2, "普通用户")] 的话肯定是没问题的,这是之前学习的内容。
直接去数据库里获取的话,也很方法,因为拿到的数据类型就是元组元素的列表,而且也是可以生成select标签的选项的。但是这里有一个问题,比如要重启我们的django,新的选项才能在页面上生效。
把选项放内存里的好处是用起来比较方便,适合那些设置之后基本不会变化的选项,你调整之后是需要重启django的。如果选项需要可以动态的调整,那么就把它放到数据库里去。不过按照上面的用法,并没有什么用处,因为我们依然要重启django之后才能生效。具体的用法看下面的小节。

操作动态Select数据

上面导致不重启无法更新的原因是,我们使用了静态字段来定义的。在之前学习面向对象的时候,那是也叫公有属性。这里会使用静态字段来称呼。
在程序第一次加载的时候,就执行了一次静态字段,通过静态字段获取了数据库里的内容保存到内存里了。之后在请求的时候就不会再重新去数据库里获取了,而是的内存里去找之前获取到的数据。所以导致无法动态更新数据库里最新的内容。

通过构造方法实现

前端html显示的数据是通过视图函数的obj传过去的。在视图函数里,在获取到静态字段之后,return给前端之前,重新去数据库里获取一下最新的数据,覆盖掉之前的静态数据。:

# views.py 文件
def user_info(request):
    obj = forms.UserInfo()  # 这步是实例化,obj里有所有静态字段的静态的内容
    # 实际上,所有的静态字段都在obj.fields里。下面就把其中的type字段里的choices的值重新去获取一下
    obj.fields['type'].choices = models.UserType.objects.values_list()  # 这里是重新去数据库里再获取一下最新的值
    return render(request, 'user_info.html', {'obj': obj})

不过上面的方法也不好,因为这样实现,要在每次使用这个字段的时候都多写上这么一句。下面是修改froms,把这个值在每次实例化的时候都重新去数据库里获取一次,具体的做法就是在放到构造方法里执行。
上面只是为了讲清楚前因后果,下面才是最终我们要使用的方法:

# forms.py 文件
class UserInfo(Form):
    name = fields.CharField()
    comments = fields.CharField(
        required=False,
        widget=widgets.Textarea(attrs={'class': 'c1'})
    )
    type = fields.ChoiceField(
        # choices=[(1, "超级用户"), (2, "普通用户")]
        # 列表里每一个元素都是元组,如果选项是去数据库获取的话,用.values_list()方法就能直接获取到
        # choices=models.UserType.objects.values_list()
        choices=[]  # 这个值具体是多少,在这里不重要了,每次实例化的时候都会在构造方法里重新赋值
    )

    def __init__(self, *args, **kwargs):
        super(UserInfo, self).__init__(*args, **kwargs)  # 仅有这一行,就是完全继承父类的构造方法
        # 下面我们再加上我们在构造方法中要额外执行的代码
        self.fields['type'].choices = models.UserType.objects.values_list()  # 从之前的视图函数,搬到这里

如果是使用CharField的widget插件来定义的select,那么关键的2行代码如下:

# 使用CharField来定义select
type2 = fields.CharField(widget=widgets.Select(choices=[]))
# 构造函数里赋值的方法
self.fields['type2'].widget.choices = models.UserType.objects.values_list()

通过 ModelChoiceField 实现

另外还有一个django自带提供的方法,不过不能单独使用,还要去model里构造一个特殊方法,实现显示字段的值而不是对象本身:

    from django.forms.models import ModelChoiceField
    type3 = ModelChoiceField(
        queryset=models.UserType.objects.all()
    )

这样也能动态的更新数据库里的值,但是现在页面上显示的是对象不是数据库库表里的值。这里还得去models里构造一个__str__方法,定义打印对象的时候具体打印处理的内容:

class UserType(models.Model):
    name = models.CharField(max_length=16)

    def __str__(self):
        return self.name

上面这种情况,在admin的操作里可能也会遇到。总之如果页面上显示出来的不是字段的值而是对象的话,是去类构造这个特殊方法应该能解决。
这个函数还有其他的参数:

  • queryset :上面说了,获取choices的选项
  • empty_label="---------" :默认选项显示的内容
  • to_field_name=None :HTML中value的值对应的字段,默认会取id的值,这里赋值'id'效果也是一样的。
  • limit_choices_to=None :ModelForm中对queryset二次筛选
    应该还有其他参数,上面这些是比较有用的
    明确一下,选项的text的值就是__str__方法返回的值,而选项的value的值就是to_field_name定义的字段的值。
    上面是单选,如果需要多选,就用下面的这个方法替代就好了。
    ModelMultipleChoiceField(ModelChoiceField)

    最后这个方法不是完全在form里实现的,还要去models里写一个特殊方法,所以老师条件用上面的方法。全部都是在form里就实现了。

Form内置钩子

先把之前的代码补充完整,html里没有form,也没有提交,这里还关掉了表单前端的验证功能(novalidate="novalidate"):

<body>
<form action="." novalidate="novalidate" method="post">
    {% csrf_token %}
    <p>{{ obj.name }}</p>
    <p>{{ obj.comments }}</p>
    <p>{{ obj.type }}</p>
    <p>{{ obj.type3 }}</p>
    <p><input type="submit"></p>
</form>
</body>

再把处理函数写完整,现在要有 get 和 post 两个方法:

def user_info(request):
    if request.method == 'GET':
        obj = forms.UserInfo({'name': 'Your Name'})  # 这步是实例化,obj里有所有静态字段的静态的内容
        # 实际上,所有的静态字段都在obj.fields里。下面就把其中的type字段里的choices的值重新去获取一下
        # obj.fields['type'].choices = models.UserType.objects.values_list()  # 这里是重新去数据库里再获取一下最新的值
        return render(request, 'user_info.html', {'obj': obj})
    elif request.method == 'POST':
        obj = forms.UserInfo(request.POST)  # 获取POST提交的数据
        res = obj.is_valid()  # 这一步是进行验证
        if res:
            print(obj.cleaned_data)
            return HttpResponse(str(obj.cleaned_data))
        else:
            print(obj.errors)
            return HttpResponse(str(obj.errors))  # 通过str方法后,页面上会直接按html代码处理

找到钩子

首先是在 res = obj.is_valid() 这一步进行验证的。这里面会执行一个 self.errors 。再看看这个errors方法,里面会执行一个 self.full_clean() 。最后来到full_clean方法,完整的代码如下:

    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()

这里看最后的3行。

重构钩子方法

所有的钩子方法在我们自己的Form类里重构。
self._clean_fields() 分别对每个字段进行验证
里有下面这几行内容:

                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value

判断是否存在 clean_%s 方法,如果有就执行。对应我们现在的 UserInfo(Form) 这个类,就是可以分别自定义 clean_name clean_comments 这些方法,来自定制验证的规则。
self._clean_form() 对整体进行验证
里面直接调用了一个空的方法,self.clean() 这个就是直接留给我们的钩子。直接在类里重构这个方法来使用。最常见的对于整体进行验证的场景是验证登录的用户名和密码。
self._post_clean() 附加的验证
这个直接就是一个空方法,所以直接重构它就好了。这里并没有展开,应该用上面2个就够了。这个方法是和 self._clean_fields()self._clean_form() 一个级别的,那2个方法里帮我么写好了try,然后在try里调用的钩子。而这个方法完全自定义了,什么都没写。
把下面的代码添加到UserInfo类的最后,添加自定制的验证:

# forms.py 文件
from django.core.exceptions import ValidationError  # 导入异常类型

class UserInfo(Form):
    # 前面的代码不变,先把钩子方法写上,稍后再来完善

    def clean_name(self):
        """单独对每个字段进行验证"""
        name = self.cleaned_data['name']  # 获取name的值,如果验证通过,直接返回给clean_data
        if name == 'root':
            raise ValidationError("不能使用“root”做用户名")
        return name

    def clean_comments(self):
        # 这里可以再补充验证的内容
        return self.cleaned_data['comments']

    def clean(self):
        """对form表单的整体内容进行验证"""
        # 如果字段为空,但是验证要求不能为空,那么在完成字段验证的时候,cleaned_data里是没有name这个键值的
        # 但是会继续往下进行之后的验证,下面要用get,否则没有这个键的话会报错
        name = self.cleaned_data.get('name')
        comments = self.cleaned_data.get('comments')
        if name == comments:
            raise ValidationError("用户名和备注的内容不能一样")
        return self.cleaned_data

    def _post_clean(self):
        pass

其他补充的内容

这里ValidationError的异常类型,第一个参数message是错误信息,上面已经有了。第二个参数code是错误类型,这里没有定义,貌似也没什么效果。之前学习Form的时候,自定义错误信息,就用到过把原生的错误信息根据错误类型修改为自定义的错误信息。比如: "required" 、 "max_length" 、 "min_length" 。
在views处理函数里,obj.errors里存放的是所有的错误信息。其中字段验证错误信息的键值就是字段名。而之后的整体的验证产生的错误信息是存放在一个特殊的键值里的 __all__ 。另外也可能看到不直接写 __all__ 的,而是用到下面的常量:

from django.core.exceptions import NON_FIELD_ERRORS
# 在源码是这这样定义的
NON_FIELD_ERRORS = '__all__'

所以还是 __all__ ,看到的时候要认得。

序列化错误信息-配合Ajax

这个就是上一回最后提到的内容。
首先准备前端的html页面,还是用上面的user_info.html。前面的东西都不用改,只在后面追加jQuery的代码就好了,这里全部贴上:

<body>
<form action="." novalidate="novalidate" method="post">
    {% csrf_token %}
    <p>{{ obj.name }}</p>
    <p>{{ obj.comments }}</p>
    <p>{{ obj.type }}</p>
    <p>{{ obj.type3 }}</p>
    <p><input type="submit"></p>
</form>
{% load static %}
<script src="{% static "jquery-1.12.4.min.js" %}"></script>
<script>
    $(function () {
        $('form').on("submit", function (event) {
            event.preventDefault();  // 阻止默认操作,我们要执行自己的Ajax操作
            $.ajax({
                url: '.',
                type: 'POST',
                data: $(this).serialize(),
                success: function (arg){
                    console.log(arg)
                },
                error: function (arg) {
                    alert("请求执行失败")
                }
            })
        })
    })
</script>
</body>

上面的jQuery里对form表单的submit绑定事件,首先阻止form默认的操作,就是form表单的submit提交。然后执行自己写的Ajax操作。
forms.py里面之前也已经写了很多的验证了,直接能用上。
views.py里的get也不用改。主要修改的是post最后返回的内容。这也是这里要讲的内容。

def user_info(request):
    if request.method == 'GET':
        obj = forms.UserInfo({'name': 'Your Name'})  # 这步是实例化,obj里有所有静态字段的静态的内容
        # 实际上,所有的静态字段都在obj.fields里。下面就把其中的type字段里的choices的值重新去获取一下
        # obj.fields['type'].choices = models.UserType.objects.values_list()  # 这里是重新去数据库里再获取一下最新的值
        return render(request, 'user_info.html', {'obj': obj})
    elif request.method == 'POST':
        obj = forms.UserInfo(request.POST)  # 获取POST提交的数据
        res = obj.is_valid()  # 这一步是进行验证
        if res:
            # 验证通过的情况,之前已经会处理了。这里主要关注下面验证不通过,如何返回错误信息
            print(obj.cleaned_data)
            return HttpResponse(str(obj.cleaned_data))
        else:
            print(type(obj.errors))  # 先来看看这个错误信息的类型
            return HttpResponse(str(obj.errors))  # 通过str方法后,页面上会直接按html代码处理

上面先用 print(type(obj.errors)) 看看这个存放错误信息的变量是个什么类型,print的结果如下:

<class 'django.forms.utils.ErrorDict'>

这里还是试着去源码里找到它,探究一下。在PyCharm里写上下面这句,然后Ctrl+左键,可以快速的定位到源码:

from django.forms.utils import ErrorDict

貌似内容不多,就全部贴下面了:

@html_safe
class ErrorDict(dict):
    """
    A collection of errors that knows how to display itself in various formats.

    The dictionary keys are the field names, and the values are the errors.
    """
    def as_data(self):
        return {f: e.as_data() for f, e in self.items()}

    def get_json_data(self, escape_html=False):
        return {f: e.get_json_data(escape_html) for f, e in self.items()}

    def as_json(self, escape_html=False):
        return json.dumps(self.get_json_data(escape_html))

    def as_ul(self):
        if not self:
            return ''
        return format_html(
            '<ul class="errorlist">{}</ul>',
            format_html_join('', '<li>{}{}</li>', self.items())
        )

    def as_text(self):
        output = []
        for field, errors in self.items():
            output.append('* %s' % field)
            output.append('\n'.join('  * %s' % e for e in errors))
        return '\n'.join(output)

    def __str__(self):
        return self.as_ul()

之前直接打印处理的就是他的as_ul方法返回的值,看最下面的特殊方法 __str__

通过as_json()方法,进行2次序列化操作

这里先看一下as_json()方法。处理函数修改成下面这样:

def user_info(request):
    if request.method == 'GET':
        obj = forms.UserInfo({'name': 'Your Name'})  # 这步是实例化,obj里有所有静态字段的静态的内容
        # 实际上,所有的静态字段都在obj.fields里。下面就把其中的type字段里的choices的值重新去获取一下
        # obj.fields['type'].choices = models.UserType.objects.values_list()  # 这里是重新去数据库里再获取一下最新的值
        return render(request, 'user_info.html', {'obj': obj})
    elif request.method == 'POST':
        ret = {'status': True, 'errors': None, 'data': None}
        obj = forms.UserInfo(request.POST)  # 获取POST提交的数据
        res = obj.is_valid()  # 这一步是进行验证
        if res:
            # 验证通过的情况,之前已经会处理了。这里主要关注下面验证不通过,如何返回错误信息
            print(obj.cleaned_data)
        else:
            # print(type(obj.errors))  # 先来看看这个错误信息的类型
            ret['status'] = False
            ret['errors'] = obj.errors.as_json()  # as_json()方法里,对error的信息进行了一下dumps
            print(ret)
        # 下面return的时候又对全部的数据进行了一次dumps,所以之后前端获取errors信息的时候也要反解2次
        return HttpResponse(json.dumps(ret))

前端的script标签里的内容修改为:

<script>
    $(function () {
        $('form').on("submit", function (event) {
            event.preventDefault();  // 阻止默认操作,我们要执行自己的Ajax操作
            $.ajax({
                url: '.',
                type: 'POST',
                data: $(this).serialize(),
                success: function (arg){
                    console.log(arg);
                    var obj = JSON.parse(arg);
                    if (obj.status){
                        location.reload()
                    } else {
                        var errors = JSON.parse(obj.errors);  // 这里是第二次反解了,返回的error里的数据要反解2次
                        console.log(errors)
                    }
                },
                error: function (arg) {
                    console.log("请求执行失败")
                }
            })
        })
    })
</script>

上面暂时只把错误信息输出到控制台了。这里还是有问题,因为要JSON正解反解两次。
这个是笨办法,能实现需求,但是肯定不好

通过as_data()方法,使用自定义的方法序列化

现在再看看as_data()方法。通过 print(type(obj.errors.as_data())) 可以查看这个方法返回的数据类型是原生的字典类型。再来打印字典的内容 print(obj.errors.as_data()) ,可以看到字典里嵌套了其他数据类型,是不能直接序列化的:

{'type3': [ValidationError(['未选择你的选项'])], '__all__': [ValidationError(['用户名和备注的内容不能一样'])]}

对于ValidationError类型,我们只需要其中的messages和code,放的分别是错误信息和错误类型。这里需要自定义个类来代替dumps方法进行序列化:

from django.core.exceptions import ValidationError
class JsonCustomEncoder(json.JSONEncoder):
    def default(self, field):
        if isinstance(field, ValidationError):
            return {'code': field.code, 'messages': field.messages}
        else:
            return json.JSONEncoder.default(self, field)

有了上面的类,现在只需要用序列化和反序列化一次了:

ret['status'] = False
ret['errors'] = obj.errors.as_data()
result = json.dumps(ret, cls=JsonCustomEncoder)  # 这里的dumps方法多了一个cls参数
return HttpResponse(result)

关于上面使用的dumps方法。cls里定义了序列化时使用的方法。上面自定义的类继承了默认的方法,然后写了我们需要的额外的处理方法进行序列化。
这个方法就不需要2次序列化了,但是有了下面的方法,这个也不需要了。但是通过这小段代码,学习到了,json序列化操作的进阶知识。

通过get_json_data()方法,2.0新方法

这个是官网的连接:https://docs.djangoproject.com/en/2.0/ref/forms/api/
主要是去里面确认到了下面的信息:
Python自动化开发学习23-Django下(Form)
这个方法是Django 2.0. 新加入了。所以有了它,上面的那两种麻烦的实现方法都不需要了。
下面是和as_data()方法的比较:

temp1 = obj.errors.get_json_data()
print(type(temp1), temp1)
temp2 = obj.errors.as_data()
print(type(temp2), temp2)

下面是打印结果的比较:

<class 'dict'> {'type3': [{'message': 'This field is required.', 'code': 'required'}], '__all__': [{'message': '用户名和备注的内容不能一样', 'code': ''}]}
<class 'dict'> {'type3': [ValidationError(['This field is required.'])], '__all__': [ValidationError(['用户名和备注的内容不能一样'])]}

效果和上面的方法一样,直接就是原生的字典。但是这里直接使用新提供的原生方法就可以了。

序列化QuerySet

现在也把数据库查询(Model操作)的结果也序列化返回给前端。
一直没遇到过这类问题,因为之前都是通过模板语言把数据传到前端的页面的。如果要支持通过Ajax请求的操作,拿到后端的数据,就会用到下面序列化的方法。

方法一

from django.core import serializers

obj = models.UserType.objects.all()
data = serializers.serialize('json', obj)
print(data)  # 打印结果在下面
# [{"model": "app01.usertype", "pk": 1, "fields": {"name": "\u8d85\u7ea7\u7528\u6237"}}]

这里返回的是QuerySet对象。所以不能用json来序列化了。这里用了django提供的方法实现了序列化。其中model是表名,pk是主键即id的值。最后的fields里的内容就是其他的数据了。

方法二

import json

obj = models.UserType.objects.values()
obj = list(obj)  # 直接可以转成一个原生的字典
data = json.dumps(obj)
print(data)  #打印结果如下
# [{"id": 1, "name": "\u8d85\u7ea7\u7528\u6237"}]

这样也可以完成序列化。
但是如果存在日期或时间类型的话,就会有问题了。这个时候参照上面错误信息序列化的第二个方法,自定义一个序列化方法,也可以完成序列化操作。