Python自动化开发学习23-Django下(Form)
回顾
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 Admin
- Python自动化开发学习24-Django下(其他)
- Python自动化开发学习24-Django中(AJAX)
- Python自动化开发学习25-Django
- Python学习笔记(3)——Django开发Web系统
- 文章推荐集合页推荐学习Python Django 开发机开发人参考
- Python学习笔记23:Django构建一个简单的博客网站(一个)
- python的web开发框架django学习笔记
- Python自动化开发学习1
- python开发学习-day15(前端部分知识、web框架、Django创建项目)
- Python自动化开发学习1-2
- Python自动化开发学习之三级菜单制作
- python自动化之djangoform表单验证
- Python开发入门与实战7-Django Form
- 一步步学习Python-django开发-添加后台管理
- python学习(5):web网站开发利器Django框架
- django学习之pythonbrew部署开发环境
- Python开发【Django】:ModelForm操作
- [转]Django 是一个 Python 下的 web 开发框架[学习资料]