您的位置:首页 > 编程语言 > Python开发

Python自动化开发学习23-Django下(Form)

2018-05-14 17:01 666 查看

回顾

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__
,看到的时候要认得。

补充内容-项目

在全局验证的钩子里,也可以指定生成对应字段的错误信息。像下面的例子里这样使用 self.add_error():

def clean(self):
for field in admin_obj.readonly_fields:
default_value = getattr(self.instance, field)
cleaned_value = self.cleaned_data.get(field)
if default_value != cleaned_value:
self.add_error(field, ValidationError("只读字段不能修改", code='readonly'))
return self.cleaned_data

另外上面的错误生成的时候都比较简单,虽然可以用,但是不是官方推荐的写法,主要是缺少code。
这部分内容可以去搜索Django官方文档的 "Form and field validation" :https://docs.djangoproject.com/en/2.1/ref/forms/validation/

序列化错误信息-配合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序列化操作的进阶知识。
补充内容:也可以简单一点,不定义整个类,而是只定义default方法,方法名随意。下面是一个序列化时间的default方法:

def json_date_handler(obj):
try:
return obj.strftime("%Y-%m-%d %T")
except AttributeError:  # 这里将异常捕获,什么都不错,留着等原生的default方法来抛出它的异常
pass

# 用的时候直接把方法名传递给default参数
result = json.dumps(ret, default= json_date_handler)

default方法是在正常的序列化执行之后调用的。只有在无法序列化的时候,才会去调用default方法。上面自定义了我们自己的default方法。如果原生的方法无法序列化,会执行default方法。原生的default方法就是直接抛出一个TypeError的异常。这里我们传了一个我们自己的default方法,所以会先执行自定义的default方法。如果还是没有返回,则会继续调用原生的default方法。所以自定义的函数里不要抛出异常,而是将异常捕获什么都不做,留着等原生的default方法来抛出它的异常。主要是这里没有类,无法调用父类的default,不过 json.dumps 里帮我们实现了
顺便抄一个别人实现序列化的代码:

import json
from datetime import date
from datetime import datetime

class CJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj,datetime.datetime):
return obj.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(obj,date):
return obj.strftime('%Y-%m-%d')
else:
return json.JSONEncoder.default(self,obj)

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

这个是官网的连接:https://docs.djangoproject.com/en/2.0/ref/forms/api/
主要是去里面确认到了下面的信息:

这个方法是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"}]

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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  python django