Python入门自学进阶-Web框架——28DjangoAdmin项目应用-只读字段与后端表单验证
Posted kaoa000
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python入门自学进阶-Web框架——28DjangoAdmin项目应用-只读字段与后端表单验证相关的知识,希望对你有一定的参考价值。
有时候,记录的某些字段在生成后就不允许再修改了,这时前端只能显示,不能修改。这时,可在AdminClass中进行设置:readonly_fields=[字段名,字段名,。。。],前端格式就显示成只显示不能修改。实现如下:
在对应的input标签中增加disabled属性,就可以禁止修改,但问题是,这是在前端页面进行的修改,而我们的修改页面,即rec_change.html中,是直接 f 生成的标签,即使用的modelform标签,所以,要修改,只能在后端对modelform进行修改。
对创建动态modelform进行修改:
def create_model_form(req,admin_class):
# 动态生成ModelForm类,主要使用type函数
def __new__(cls, *args, **kwargs):
# 対生成ModelForm中的字段添加前端样式,即在前端自动生成标签时带上class属性
# cls.base_fields['qq'].widget.attrs['class'] = 'form-control'
print("22222222",cls,cls.base_fields) # cls是动态生成的Modelform,cls.base_fields是字段的列表
for field_name,field_obj in cls.base_fields.items():
print(field_name,"<---->",field_obj)
if field_name in admin_class.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
return ModelForm.__new__(cls)
class Meta:
model = admin_class.model
fields = "__all__"
print("111111111111")
model_form_class = type("DynamicModelForm",(ModelForm,),'Meta':Meta)
print("33333333")
setattr(model_form_class,'__new__',__new__) # 以这种方式给动态生成的类加Meta不好用
print('444444444444')
return model_form_class
打印的1111111、2222等是为了测试执行顺序,__new__中的cls就是动态生成的ModelForm类,其base_fields是ModelForm类对应的Model类中的字段字典:
这样在前端生成的标签,qq就不能修改了,但是在提交时,即点击保存按钮时,提示错误,qq字段不能为空,也就是标签被设置为disabled后,提交时其值不会被提交。
使用readonly属性,对于input标签可以做到就不能修改,也能提交,但是此属性对于下拉框不起作用,所以另想一个办法,在数据提交到后端后,对提交的数据进行修改:
修改前:
def rec_obj_change(req,app_name,table_name,id_num):
admin_class = mytestapp_admin.enable_admins[app_name][table_name]
model_form_class = myutils.create_model_form(req,admin_class)
obj = admin_class.model.objects.get(id=id_num)
if req.method == "POST":
print('第二次POST')
form_obj = model_form_class(req.POST,instance=obj)
# ModelForm参数为一个时,是新建一条记录,当有两个参数时,就是修改
# POST方法就是进行记录修改的,所以需要两个参数,第二个是instance=参数
if form_obj.is_valid():
form_obj.save()
else: # 这是GET请求,所以是新建一个ModelForm,进行显示
print('第一次GET')
form_obj = model_form_class(instance=obj)
return render(req,'mytestapp/rec_change.html','form_obj':form_obj,'model_name':admin_class.model.__name__,'admin_class':admin_class)
对上面的代码,在判断为POST方法后,尝试对req.POST修改,将obj对应的字段及其值再加入POST中,实测中,提示req.POST是不可修改的,所以,做一个拷贝:
if req.method == "POST":
print('第二次POST')
# for field in admin_class.readonly_fields:
# req.POST[field] = getattr(obj,field) # 不能修改POST,此方法不可行
post_data = req.POST.copy() # 对POST做一个拷贝,使用可变的副本进行修改
for field in admin_class.readonly_fields:
post_data[field] = getattr(obj,field)
form_obj = model_form_class(post_data,instance=obj)
这里有一个在添加post_data的readonly_fields字段时,这里如果将consultant字段加入readonly_fields中,在后端加入post_data的是post_data['consultant'] = getattr(obj,field),值是一个对象,(如这里添加的consultant是 <class 'plcrm.models.UserProfile'>类型),而从前端提交时,通过下拉框选择,传递的是一个字符串格式的数字,即UserProfile的id。
修改前端,在提交时,将disabled属性去掉,这样对应的字段值就能提交了。
在rec_change.html中,将form提交时执行的函数SelectedAll()进行如下修改:
function SelectedAll()
$("select[my_id='selectedalloption'] option").each(function ()
$(this).prop("selected",true)
);
$("form").find("[disabled]").removeAttr("disabled");
return true
主要是增加将disabled属性去掉的语句,这时再提交,后端视图函数打印POST如下:
@@@ <QueryDict: 'csrfmiddlewaretoken': ['59Ef3Jme9aQawB9jiZl9Y1CAHwHNkr1ybCy63izXnjJ6wjT519DH62rD16ZvfnpK'], 'name': ['老舍46667'], 'qq': ['2111111111'], 'qq_name': ['反反复复'], 'phone': ['13212345678'], 'source': ['5'], 'referral_from': ['1234554'], 'consult_course': ['1'], 'content': ['反反复复发发发发发发ppppp'], 'tags': ['1', '3', '5'], 'status': ['1'], 'consultant': ['3'], 'memo': ['巴巴爸爸不不不不不不不']>
此时,视图函数rec_obj_change就不需要修改了。修改功能完成。
前端通过修改成disabled的方法,是可以通过调试将其改回去的,其无法保证传递到后端的数据不变,所以,还需要在后端进行一次验证。
Form验证,使用自定义的clean方法,修改动态生成的ModelForm,添加自定义的clean方法:
def create_model_form(req,admin_class):
# 动态生成ModelForm类,主要使用type函数
def __new__(cls, *args, **kwargs):
# 対生成ModelForm中的字段添加前端样式,即在前端自动生成标签时带上class属性
# cls.base_fields['qq'].widget.attrs['class'] = 'form-control'
print("22222222",cls,cls.base_fields) # cls是动态生成的Modelform,cls.base_fields是字段的列表
for field_name,field_obj in cls.base_fields.items():
print(field_name,"<---->",field_obj)
if field_name in admin_class.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
return ModelForm.__new__(cls)
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',self)
class Meta:
model = admin_class.model
fields = "__all__"
model_form_class = type("DynamicModelForm",(ModelForm,),'Meta':Meta)
setattr(model_form_class,'__new__',__new__) # 以这种方式给动态生成的类加Meta不好用
setattr(model_form_class,'clean',default_clean)
return model_form_class
打印的结果:
可以看出,self就是页面中form表单内的内容。
将print('======运行自定义clean验证=======:',self)改成
print('======运行自定义clean验证=======:',admin_class.readonly_fields),打印出readonly字段,然后对该字段进行验证,即取前端传递过来的数据,与数据库中已有的数据进行比较,如果相同,说明前端确实没有修改,否则,就是被人为越过前端的disabled属性,修改了,此时要报错。
from django.forms import ValidationError
from django.utils.translation import ugettext as _
def create_model_form(req,admin_class):
# 动态生成ModelForm类,主要使用type函数
def __new__(cls, *args, **kwargs):
# 対生成ModelForm中的字段添加前端样式,即在前端自动生成标签时带上class属性
# cls.base_fields['qq'].widget.attrs['class'] = 'form-control'
print("22222222",cls,cls.base_fields) # cls是动态生成的Modelform,cls.base_fields是字段的列表
for field_name,field_obj in cls.base_fields.items():
print(field_name,"<---->",field_obj)
if field_name in admin_class.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
return ModelForm.__new__(cls)
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',admin_class.readonly_fields)
for field in admin_class.readonly_fields:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
if field_val_db != field_val_web:
raise ValidationError(
_('Field %(field)s is readonly,data should be %(val)s'),
code='invalid',
params='field':field,'val':field_val_db
)
class Meta:
model = admin_class.model
fields = "__all__"
model_form_class = type("DynamicModelForm",(ModelForm,),'Meta':Meta)
setattr(model_form_class,'__new__',__new__) # 以这种方式给动态生成的类加Meta不好用
setattr(model_form_class,'clean',default_clean)
return model_form_class
关键看上面的default_clean函数,对前端数据和后端数据进行校验,如果出错,提示:
我同时修改了qq和consultant,但是错误只显示了一个,说明在default_clean中,碰到第一个raise,程序就跳出,没有执行第二个字段的判断。
再次修改default_clean:
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',admin_class.readonly_fields)
error_list = []
for field in admin_class.readonly_fields:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
if field_val_db != field_val_web:
error_list.append(ValidationError(
_('Field %(field)s is readonly,data should be %(val)s'),
code='invalid',
params='field':field,'val':field_val_db
))
if error_list:
raise ValidationError(error_list)
此时在测试:
因为在动态生成的ModelForm中定义了自己的clean方法,就是上面的default_clean,这就阻断了原来用户可以重写clean方法来自定义自己的验证,如何让用户能够继续自定义自己的验证呢?可以在AdminClass中定义一个函数,用户可以覆写这个函数,在我们动态生成的ModelForm的clean方法中,最后调用一下用户自定义的函数就好了。
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',admin_class.readonly_fields)
error_list = []
for field in admin_class.readonly_fields:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
if field_val_db != field_val_web:
error_list.append(ValidationError(
_('Field %(field)s is readonly,data should be %(val)s'),
code='invalid',
params='field':field,'val':field_val_db
))
self.ValidationError = ValidationError
user_return = admin_class.user_form_validation(self)
if user_return:
error_list.append(user_return)
if error_list:
raise ValidationError(error_list)
# 在admin.py中
class BaseAdmin(object):
list_display = []
list_filter = []
list_per_page = 5
list_search = []
filter_horizontal = []
readonly_fields = []
actions = ['delete_action',]
def delete_action(self,req,model_objs):
print(self,req,model_objs)
def user_form_validation(self):
pass
# 给用户自定义留下接口
class CustomerAdmin(BaseAdmin):
list_display = ['qq','name','phone','source','consultant','referral_from','consult_course','tags','status']
list_per_page = 4
list_filter = ['qq','source','status','consult_course','tags']
list_search = ['qq','name']
filter_horizontal = ['tags']
readonly_fields = ['qq','consultant']
actions = ['delete_action',]
def delete_action(self,req,model_objs):
print("运行delete_action",self,req,model_objs)
# 跳转到rec_delete.html,借助已经实现的功能,调用rec_obj_delete(req,app_name,table_name,id_num):
# return render(req,'mytestapp/rec_delete.html',) #需要改造,匹配rec_obj_delete的参数
model_objs.delete()
print("删除执行完毕")
def user_form_validation(self): # 用户自定义验证
print('-----user validation:',self)
consult_content = self.cleaned_data.get("content",'')
if len(consult_content):
return self.ValidationError(('Field %(field)s 字段内容长度必须大于15'),
code='invalid',
params='field':"content"
)
前面的clean方法是对全部字段的验证,Django还提供单个字段的验证,即clean_filed的验证方法:
在AdminClass中增加一个单字段验证函数:
# 在CustomerAdmin中定义
def clean_name(self):
print('****单字段验证',self.cleaned_data['name'])
if not self.cleaned_data['name']:
self.add_error('name','名字字段内容不能为空')
# 在创建动态ModelForm类中,增加单字段验证:
def create_model_form(req,admin_class):
# 动态生成ModelForm类,主要使用type函数
def __new__(cls, *args, **kwargs):
# 対生成ModelForm中的字段添加前端样式,即在前端自动生成标签时带上class属性
# cls.base_fields['qq'].widget.attrs['class'] = 'form-control'
print("22222222",cls,cls.base_fields) # cls是动态生成的Modelform,cls.base_fields是字段的列表
for field_name,field_obj in cls.base_fields.items():
print(field_name,"<---->",field_obj)
if field_name in admin_class.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
if hasattr(admin_class,'clean_%s'%field_name): # 动态生成的类中增加单字段校验
field_validat_fuc = getattr(admin_class,'clean_%s'%field_name)
setattr(cls,'clean_%s'%field_name,field_validat_fuc)
return ModelForm.__new__(cls)
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',admin_class.readonly_fields)
error_list = []
for field in admin_class.readonly_fields:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
if field_val_db != field_val_web:
error_list.append(ValidationError(
_('Field %(field)s is readonly,data should be %(val)s'),
code='invalid',
params='field':field,'val':field_val_db
))
self.ValidationError = ValidationError
user_return = admin_class.user_form_validation(self)
if user_return:
error_list.append(user_return)
if error_list:
raise ValidationError(error_list)
class Meta:
model = admin_class.model
fields = "__all__"
model_form_class = type("DynamicModelForm",(ModelForm,),'Meta':Meta)
setattr(model_form_class,'__new__',__new__) # 以这种方式给动态生成的类加Meta不好用
setattr(model_form_class,'clean',default_clean)
return model_form_class
在前端,即rec_change.html中,在f后加上 f.errors ,这样在对应字段后显示错误信息,如下
上面的readonly设置只对ModelForm自动生成的标签起作用,对于我们自定义的标签,如tags标签,因为配置了 filter_horizontal = ['tags'] ,在页面是自定义的显示为左右两个下拉复选框,未选项和已选项分别显示在不同框中,这时设置了readonly,需要同时disabled两个框。同时要去掉对应的事件函数,这主要是前端操作。修改rec_change.html:
% if f.name in admin_class.filter_horizontal %
<div class="col-md-2" >
% get_m2m_obj_list admin_class f form_obj as m2m_obj_list %
% if f.name in admin_class.readonly_fields %
<select id="noselected_ f.name " multiple class="select-box" name=" f.name " disabled>
% for obj in m2m_obj_list %
<option value=" obj.id "> obj </option>
% endfor %
</select>
% else %
<select id="noselected_ f.name " multiple class="select-box" name=" f.name ">
% for obj in m2m_obj_list %
<option value=" obj.id " ondblclick="MoveToSelected(this,'selected_ f.name ','noselected_ f.name ')"> obj </option>
% endfor %
</select>
% endif %
</div>
<div class="col-md-1">
===》<br>
《===
</div>
<div class="col-md-2">
% if f.name in admin_class.readonly_fields %
<select my_id="selectedalloption" id="selected_ f.name " multiple class="select-box" name=" f.name " disabled>
% get_m2m_selected_list form_obj f as selected_list %
<!-- 上面是使用自定义标签获取已选择项数据-->
<!-- 下面是经过测试,使用f的initial也能获取到,不需要再定义标签 -->
% for obj in selected_list %
<option value=" obj.id " > obj </option>
% endfor %
</select>
% else %
<select my_id="selectedalloption" id="selected_ f.name " multiple class="select-box" name=" f.name ">
% get_m2m_selected_list form_obj f as selected_list %
<!-- 上面是使用自定义标签获取已选择项数据-->
<!-- 下面是经过测试,使用f的initial也能获取到,不需要再定义标签 -->
% for obj in selected_list %
<option value=" obj.id " ondblclick="MoveToSelected(this,'noselected_ f.name ','selected_ f.name ')"> obj </option>
% endfor %
</select>
% endif %
</div>
% else %
f <span> f.errors </span>
% endif %
再次测试:
tags无法操作了,但是,在提交保存时,出现了验证错误:
打印传递的参数:request.POST中如下
自定义验证中:
对应的取数语句:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
也就是从数据库中的取数,对于tags出现了错误,对于多对多的字段,需要判断是否存在select_related属性,存在,将这个属性值取出,因为是QuerySet,不能直接使用==判断,因为顺序不一样的话,也不相等,可以都转换为set:
def default_clean(self):
'''给所有的form添加一个默认的clean验证'''
print('======运行自定义clean验证=======:',admin_class.readonly_fields)
print('&&&self.instance',type(self.instance))
error_list = []
for field in admin_class.readonly_fields:
field_val_db = getattr(self.instance,field) # 从数据库中来的数据
if hasattr(field_val_db,'select_related'): # 如果是多对多字段,取select_related(),
field_val_db = set(field_val_db.select_related()) # 数据库值转换为set
field_val_web = set(self.cleaned_data.get(field)) # 前端数据也转换为set
else:
field_val_web = self.cleaned_data.get(field) # 从前端web页传递过来的数据
print("----field compare:",field,field_val_db,field_val_web)
if field_val_db != field_val_web:
error_list.append(ValidationError(
_('Field %(field)s is readonly,data should be %(val)s'),
code='invalid',
params='field':field,'val':field_val_db
))
self.ValidationError = ValidationError
user_return = admin_class.user_form_validation(self)
if user_return:
error_list.append(user_return)
if error_list:
raise ValidationError(error_list)
有一个问题:在单字段验证后,即clean_name(self)执行后,在用户自定义的验证中,这个字段的值就没有了,变成了None,导致最终保存后,name字段一直为空。
修改clean_name(self):
def clean_name(self):
print('****单字段验证',self.cleaned_data)
print('**单字段验证的参数self',self.cleaned_data['name'])
if not self.cleaned_data['name']:
self.add_error('name','名字字段内容不能为空')
print('***验证后:',self.cleaned_data)
return self.cleaned_data['name'] # 这一句非常重要
注意,最后一句加上了返回值,非常重要的一步。看下面的解析:
clean_<fieldname>()
方法是在表单子类上调用的——其中 <fieldname>
被替换为表单字段属性的名称。这个方法做任何特定属性的清理工作,与字段的类型无关。这个方法不传递任何参数。你需要在 self.cleaned_data
中查找字段的值,并且记住,此时它将是一个 Python 对象,而不是在表单中提交的原始字符串(它将在 cleaned_data
中,因为上面的一般字段 clean()
方法已经清理了一次数据)。
例如,如果你想验证一个叫 serialnumber
的 CharField
的内容是唯一的,clean_serialnumber()
就可以做这件事。你不需要一个特定的字段(它是一个 CharField
),但你需要一个特定字段的验证,可能的话,清理/规范数据。
这个方法的返回值会替换 cleaned_data
中的现有值,所以它必须是 cleaned_data
中的字段值(即使这个方法没有改变它)或一个新的干净值。
当没有最后一句时,默认返回值就是None。
添加新纪录时,就不需要进行readonly_fields判断了:
视图函数:
def rec_obj_add(req,app_name,table_name):
admin_class = mytestapp_admin.enable_admins[app_name][table_name]
admin_class.add_form = True # 为调用动态生成ModelForm,区分是修改还是增加的标志
model_form_class = myutils.create_model_form(req, admin_class)
if req.method == "POST":
print("添加的POST内容:",req.POST)
form_obj = model_form_class(req.POST)
# ModelForm参数为一个时,是新建一条记录,POST方法提交,是新建一条记录
if form_obj.is_valid():
form_obj.save()
return redirect(req.path.replace("/add/","/"))
else: # 这是GET请求,所以是新建一个空ModelForm
form_obj = model_form_class()
return render(req,"mytestapp/rec_add.html",'admin_class':admin_class,'model_name':admin_class.model.__name__,'form_obj':form_obj)
注意在调用生成动态ModelForm前,在admin_class中增加一个add_form属性,以此在create_model_form中判断是修改操作还是添加操作。
对create_model_form进行修改:主要是__new__()函数修改
def __new__(cls, *args, **kwargs):
# 対生成ModelForm中的字段添加前端样式,即在前端自动生成标签时带上class属性
# cls.base_fields['qq'].widget.attrs['class'] = 'form-control'
print("22222222") # cls是动态生成的Modelform,cls.base_fields是字段的列表
for field_name,field_obj in cls.base_fields.items():
# print(field_name,"<---->",field_obj)
if not hasattr(admin_class,"add_form"): # 判断是否是添加操作,如果不是,则进行readonly_fields字段加disabled属性操作
if field_name in admin_class.readonly_fields:
field_obj.widget.attrs['disabled'] = 'disabled'
if hasattr(admin_class,'clean_%s'%field_name): # 动态生成的类中增加单字段校验
field_validat_fuc = getattr(admin_class,'clean_%s'%field_name)
setattr(cls,'clean_%s'%field_name,field_validat_fuc)
print('222222222结束')
return ModelForm.__new__(cls)
对于空字段的单字段校验,如果添加时字段没有数据,为空,单字段校验应该提示错误信息,然后停留在添加页,但是实际测试中出现错误:
原因是单字段验证,对于POST中name=['']没有添加到cleaned_data中,修改一下:
def clean_name(self):
print('****单字段验证',self.cleaned_data)
print('**单字段验证的参数self',self.cleaned_data['name'])
if not self.cleaned_data['name']:
self.add_error('name','名字字段内容不能为空')
self.cleaned_data['name']=None # 如果为空,在cleaned_data中没有name这个属性,手工加上
print("进入单字段验证")
print('***验证后:',self.cleaned_data)
return self.cleaned_data['name'] # 这一句非常重要
以上是关于Python入门自学进阶-Web框架——28DjangoAdmin项目应用-只读字段与后端表单验证的主要内容,如果未能解决你的问题,请参考以下文章
Python入门自学进阶-Web框架——18FormModelForm
Python入门自学进阶-Web框架——18FormModelForm
Python入门自学进阶-Web框架——20Django其他相关知识2