Django学习之Forms组件部分源码学习及钩子使用
前面学习了django form表单的一些基本功能使用,本次主要学习form钩子的使用以及form的源码。首先实现一个基本的from表单使用。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.shortcuts import render,HttpResponse from app01 import models from django import forms from django.core.exceptions import ValidationError class UserForm(forms.Form): username=forms.CharField(label='用户名',min_length=6) email=forms.EmailField(label='邮箱') def fmindex(request): if request.method=="GET": obj=UserForm() return render(request, 'fm.html', {'obj': obj}) elif request.method=="POST": obj=UserForm(request.POST) if obj.is_valid(): print(obj.cleaned_data) # 保存全部通过验证的表单数据 return render(request, 'fm.html', {'obj': obj}) else: print(obj.errors) return render(request, 'fm.html', {'obj': obj})
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/fm/" method="POST"> <!--as_p表示把form表单在前端页面渲染成p标签的形式展示,此外还有as_ul,as_table等--> {{ obj.as_p }} <input type="submit" value="提交" /> </form> </body> </html>
form组件流程及部分源码
form组件最核心的方法是is_valid( ),最重要的源码也是is_valid(),钩子函数也在is_valid( )中。首先is_valid( )是校验数据的部分,将数据放入is_valid( )开始校验。因此如果不执行is_valid,是不能执行后面的cleaned_data或者errors,也就是说他是循环每个字段分别去校验,而cleaned_data和errors本质上就是两个字典,用来存放正确的数据和错误的数据。
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
is_valid就是只有一个return,前面的self.is_bound返回的一定是True,那么is_valid最后返回True还是False取决于errors到底是空字典还是有键值的,而当errors为空字典,说明没有任何错误,那么not 空就是True,如果errors里面有错误的键值,那么就返回False。
self.errors源码
@property def errors(self): "Returns an ErrorDict for the data provided for the form" if self._errors is None: self.full_clean() # 因为self._errors默认为None,所以继续从这进入 return self._errors
可以看到,self.errors中执行了full_clean()方法,继续查看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()
可以看到full_clean()中先定义了self._errors和self.cleaned_data两个空字典,分别存放用户输入验证成功和失败的数据。然后分别执行self._clean_fields()、self._clean_form()、self._post_clean()三个方法。接下来我们分别看一下这个3个方法中分别做了哪些操作。
_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[name] = value except ValidationError as e: self.add_error(name, e)
可以看到_clean_fields方法中依次循环用户输入的信息并进行规则验证,其中name对应的是字段字符串,field对应的是字段对象(也是规则对象)。注意到其中有个hasattr方法,是当用户定义的form类中有clean_字段名的一个方法时(如:用户在form类中定义了clean_username方法,该方法中用户自定义自己想实现的功能),在程序循环到这个字段且通过前面第一层的规则校验后时会去执行clean_字段名(clean_username)这个方法。注意此时我们看到了form组件中的第一个钩子(clean_字段名),这个钩子一个局部钩子,从这里可以看到,该钩子需要一个返回值,后面会详细介绍钩子相关使用。_clean_fields方法中还会捕获异常。我们继续看_clean_form方法源码
_clean_form源码
def _clean_form(self): try: 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
可以看到到_clean_form中调用了clean方法,并把返回值赋值给cleaned_data, _clean_form方法中也有异常捕获。clean方法是form组件中的第2个钩子并且是全局钩子。我接着看clean方法源码
clean()
def clean(self): """ Hook for doing any extra form-wide cleaning after Field.clean() has been called on every field. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ return self.cleaned_data
我们可以看到clean方法中仅仅是把cleaned_data作为返回值返回,这里返回的cleaned_data是什么值用户通过obj.cleaned_data获取到的就是什么值。clean该方法中用户可以自定义实现自己的功能,自己重写该方法时有异常需要抛出,便于_clean_form中捕获。
最后我们看下full_clean方法中执行的最后一个_post_clean方法主要做了什么操作
_post_clean源码
def _post_clean(self): """ An internal hook for performing additional cleaning after form cleaning is complete. Used for model validation in model forms. """ pass
我们可以看到_post_clean中什么也没做,可以让用户自己定义,_post_clean也是一个钩子,_post_clean不允许抛出异常。可以self.add_error("__all__", ValidationError('用户名或邮箱错误...'))添加错误信息。
至此梳理一下整体的流程,
1、首先用户调用is_valid()进行验证,
2、执行full_clean方法(依次执行_clean_fields、_clean_form、_post_clean方法),
3、_clean_fields方法中依次循环对用户定义的form类中每个字段进行正则验证,字段验证通过后,判断用户定义的form类中是否有clean_字段名方法,如果有执行改方法,
4、然后执行_clean_form方法(该方法中调用了clean方法,用户可以在clean方法中定义自己想要实现功能)
5、最后执行_post_clean方法(_post_clean中没任何操作,用户可以定义自己想要实现功能)
通过前面对form源码的分析可以看出,用户要实现一些自定义的功能,可以通过钩子来实现,下面就学习一些几个钩子的使用
局部钩子(clean_字段名)
在自定义的from类中定义一个方法clean_字段名,注意:名字必须为clean_%s。其原理是,当字段正则校验成功后,会在用户定义的form类中查找是否有以clean_开头的函数名,如果有,就调用该函数,运行我们自定义的函数,如果满足条件就返回当前被校验字段的内容(源码中有value = getattr(self, 'clean_%s' % name)())。否则手动触发ValidationError错误,源码中会捕获并将值返回。
#自定义UserForm类,继承Form class UserForm(forms.Form): username=forms.CharField(label='用户名',min_length=2) email=forms.EmailField(label='邮箱')
#可以对每个字段进行其他的校验 def clean_username(self): # self:当前form对象 value = self.cleaned_data['username']#通过cleaned_data获得对应字段的'干净数据' # 可以去数据库中判断用户是否存在或者其他操作 if value == 'root': # 正常,把username返回到clean_data,将name写入clean_data字典中 return value #返回的值即为cleaned_data中username对应的值,例如返回aaaa,可以看到username对应的值就是aaaa,{'username': 'root', 'email': '1129614034@qq.com'} else: # 失败,抛异常,将异常信息以 {'username':value} 写入errors字典中 raise ValidationError('数据库中不存在改用户') def fmindex(request): if request.method=="GET": obj=UserForm() return render(request, 'fm.html', {'obj': obj}) elif request.method=="POST": obj=UserForm(request.POST) if obj.is_valid():#校验,is_valid如果是true表示校验成功(满足UserForm里的条件),反之,校验失败 print(obj.cleaned_data) # 保存全部通过验证的表单数据 return render(request, 'fm.html', {'obj': obj}) else: print(obj.errors) return render(request, 'fm.html', {'obj': obj})
验证失败错误信息 <ul class="errorlist"><li>username<ul class="errorlist"><li>数据库中不存在该用户</li></ul></li></ul>
验证成功获取到正确信息
{'username': 'root', 'email': '11@qq.com'}
全局钩子
_post_clean和clean都是全局钩子,两者实现功能差不多。但是_post_clean不允许抛出异常,否则会导致整个程序异常,要通过self.add_error("__all__", ValidationError('用户名或邮箱错误...'))添加错误信息。clean可以抛出异常,程序可以捕获。
# 重写clean方法 def clean(self): # 程序能走到该函数,说明前面正则校验和每个字段的单独校验已经通过了,所以可以从cleaned_data中取出密码和确认密码 v1 = self.cleaned_data['username'] v2 = self.cleaned_data['email'] #对所有校验的字段进行其他判断,如果满足条件,就将cleaned_data返回,由于正确信息都已经保存在cleaned_data中,可以直接写pass if v1 == "root" and v2 == "root@live.com": pass else: #如果不满足就手动触发ValidationError错误。 raise ValidationError('用户名或邮箱错误!!!') #重写_post_clean方法 # def _post_clean(self): # v1 = self.cleaned_data['username'] # v2 = self.cleaned_data['email'] # if v1 == "root" and v2 == "root@live.com": # pass # else: # self.add_error("__all__", ValidationError('用户名或邮箱错误...')) def fmindex(request): if request.method=="GET": obj=UserForm() return render(request, 'fm.html', {'obj': obj}) elif request.method=="POST": obj=UserForm(request.POST) if obj.is_valid():#校验,is_valid如果是true表示校验成功(满足UserForm里的条件),反之,校验失败 print(obj.cleaned_data) # 保存全部通过验证的表单数据 return render(request, 'fm.html', {'obj': obj}) else: print(obj.errors) return render(request, 'fm.html', {'obj': obj})
补充:
form组件在使用选择标签时,需要注意choices的选项可以从数据库中获取,但是由于是静态字段 ***获取的值无法实时更新***,那么有2中方法可以实现
from django.shortcuts import render,HttpResponse from app01 import models from django import forms from django.forms import models as models_fields from django.core.exceptions import ValidationError #先写一个类,继承Form class UserForm(forms.Form): username=forms.CharField(label='用户名',min_length=2) email=forms.EmailField(label='邮箱') user_type1 = forms.ChoiceField(choices=models.UserType.objects.values_list('id', 'name')) #利用ModelForm,使用此方法models中要增加 """def __str__(self): return self.name """ user_type2 = models_fields.ModelChoiceField(queryset=models.UserType.objects.all(), empty_label='请选择用户类型', to_field_name="id", limit_choices_to={'id': 1}) def __init__(self, *args, **kwargs): super(UserForm, self).__init__(*args, **kwargs) self.fields['user_type1'].widget.choices = models.UserType.objects.all().values_list('id', 'name')