12-crm项目-kingadmin,只读字段的配置和后端校验
现实需求中,会有在编辑的时候有部分字段是只读的,不能修改的,这个功能如何实现?
1,首先是在基类中,增加一个只读字段的配置,
class BaseAdmin(object): list_display = [] list_filters = [] search_fields = [] list_per_page = 20 ordering = None filter_horizontal = [] readonly_fields = [] ----只读字段
2,在编辑页面,在展示字段的时候判断字段是否在这个只读字段里面里面,
如果在的话就给这个字段做处理
处理逻辑:
2.1 在实现的时候有disabled的属性来控制,但是会有问题,这样提交数据的时候form表单不提交这个字段的数据了
怎么办:
2.1.1 使用js在提交时候把disabled去掉?
function SelectAllChosenData() { $("select[tag='chosen_list'] option").each(function () { $(this).prop("selected",true); }) //remove all disabled attrs $("form").find("[disabled]").removeAttr("disabled") ; return true; }
2.1.2 改成p标签?不行,p标签不会在form表单中提交,
2.1.3 直接从数据库中取这个值?
2.2 不能使用只读属性:因为这个下拉框还能下拉下来,并且还能修改选中的值,这是不行的
<input type="text" name="country" value="China" readonly="readonly" />
3,前端验证完了后端也要验证,否则前端改了后端不校验还是没有用,
别人通过前端F12改掉,就可以提交了,
所以要有后端表单的验证,
这个验证要放到那个动态model里面,加一个clean方法,所有的form都会默认加一个clean方法,
逻辑:
第一步:就是修改表单的时候判断是否数据库有这一个数据,如果有的话就是修改表单,
第二步:如果是修改表单了,拿到这个数据,看这个字段是否只读字段, 如果是只读字段就拿到数据库的数据,
第三步:拿到了数据库的数据,还要拿到页面传递的字段,然后对比这两个字段是否一致,
优化:
第一个:可能有多个字段,就可能会有多个异常,所以先把所有的异常放到一个列表中,然后统一返回,
第二个:这种写法,不能允许用户自己做自定义了,写了之后就别覆盖了,所以在最后调用用户自定义的方法就可以了,
如何对单个字段做验证:
在注册的表的时候,每一个都有一个对应的admin类,
在这里做对这个表的,单个字段的验证,
def create_model_form(request,admin_class): '''动态生成MODEL FORM''' def __new__(cls, *args, **kwargs): # super(CustomerForm, self).__new__(*args, **kwargs) #print("base fields",cls.base_fields) for field_name,field_obj in cls.base_fields.items(): #print(field_name,dir(field_obj)) field_obj.widget.attrs['class'] = 'form-control' # field_obj.widget.attrs['maxlength'] = getattr(field_obj,'max_length' ) if hasattr(field_obj,'max_length') \ # else "" if not hasattr(admin_class,"is_add_form"): #代表这是添加form,不需要disabled if field_name in admin_class.readonly_fields: field_obj.widget.attrs['disabled'] = 'disabled' if hasattr(admin_class,"clean_%s" % field_name): ------这是为了兼容自定义的admin类里面有没有对单个字段的校验,会自动运行这个方法 field_clean_func = getattr(admin_class,"clean_%s" %field_name) setattr(cls,"clean_%s"%field_name, field_clean_func) return ModelForm.__new__(cls) def default_clean(self): -----------------这就是加的表单验证 '''给所有的form默认加一个clean验证''' print("---running default clean",admin_class) print("---running default clean",admin_class.readonly_fields) print("---obj instance",self.instance.id) error_list = [] if self.instance.id: # 这是个修改的表单 ---------这是在修改表单的时候能获取到这个数据,然后就可以去判断字段是否是来自数据里的数据了, for field in admin_class.readonly_fields: field_val = getattr(self.instance,field) #val in db if hasattr(field_val,"select_related"): #m2m m2m_objs = getattr(field_val,"select_related")().select_related() m2m_vals = [i[0] for i in m2m_objs.values_list('id')] set_m2m_vals = set(m2m_vals) set_m2m_vals_from_frontend = set([i.id for i in self.cleaned_data.get(field)]) print("m2m",m2m_vals,set_m2m_vals_from_frontend) if set_m2m_vals != set_m2m_vals_from_frontend: # error_list.append(ValidationError( # _('Field %(field)s is readonly'), # code='invalid', # params={'field': field}, # )) self.add_error(field,"readonly field") continue field_val_from_frontend = self.cleaned_data.get(field) #print("cleaned data:",self.cleaned_data) print("--field compare:",field, field_val,field_val_from_frontend) if field_val != field_val_from_frontend: -------如果不一致就要报错出来异常, error_list.append( ValidationError( _('Field %(field)s is readonly,data should be %(val)s'), code='invalid', params={'field': field,'val':field_val}, )) #readonly_table check if admin_class.readonly_table: raise ValidationError( _('Table is readonly,cannot be modified or added'), code='invalid' ) #invoke user's cutomized form validation self.ValidationError = ValidationError response = admin_class.default_form_validation(self)----------这就是用户自定义了验证类,也要执行, if response: error_list.append(response) if error_list: raise ValidationError(error_list) class Meta: model = admin_class.model fields = "__all__" exclude = admin_class.modelform_exclude_fields attrs = {'Meta':Meta} _model_form_class = type("DynamicModelForm",(ModelForm,),attrs) setattr(_model_form_class,'__new__',__new__) setattr(_model_form_class,'clean',default_clean) -------------这就是加成一个属性了, print("model form",_model_form_class.Meta.model ) return _model_form_class
在基类里面写上一个入口,用户可以自定义验证方法,
class BaseAdmin(object): list_display = [] list_filters = [] search_fields = [] list_per_page = 20 ordering = None filter_horizontal = [] readonly_fields = [] actions = ["delete_selected_objs",] readonly_table = False modelform_exclude_fields = [] def delete_selected_objs(self,request,querysets): app_name = self.model._meta.app_label table_name = self.model._meta.model_name print("--->delete_selected_objs",self,request,querysets) if self.readonly_table: errors = {"readonly_table": "This table is readonly ,cannot be deleted or modified!" } else: errors = {} if request.POST.get("delete_confirm") == "yes": if not self.readonly_table: querysets.delete() return redirect("/king_admin/%s/%s/" % (app_name,table_name)) selected_ids = ','.join([str(i.id) for i in querysets]) return render(request,"king_admin/table_obj_delete.html",{"objs":querysets, "admin_class":self, "app_name": app_name, "table_name": table_name, "selected_ids":selected_ids, "action":request._admin_action, "errors":errors }) def default_form_validation(self): '''用户可以在此进行自定义的表单验证,相当于django form的clean方法''' pass
举一个例子可以用户自定义验证方法;
class CustomerAdmin(BaseAdmin): list_display = ["id",'qq','name','source','consultant','consult_course','date','status','enroll'] list_filters = ['source','consultant','consult_course','status','date'] search_fields = ['qq','name',"consultant__name"] filter_horizontal = ('tags',) #model = models.Customer list_per_page = 2 ordering = "qq" readonly_fields = ["qq","consultant","tags"] #readonly_table = True #modelform_exclude_fields = [] actions = ["delete_selected_objs","test"] def test(self,request,querysets): print("in test",) test.display_name = "测试动作" def enroll(self): print("enroll",self.instance) if self.instance.status ==0: link_name = "报名新课程" else: link_name = "报名" return '''<a href="/crm/customer/%s/enrollment/" > %s</a>''' %(self.instance.id,link_name) enroll.display_name = "报名链接" def default_form_validation(self): #print("-----customer validation ",self) #print("----instance:",self.instance) consult_content =self.cleaned_data.get("content",'') if len(consult_content) <15: return self.ValidationError( ('Field %(field)s 咨询内容记录不能少于15个字符'), code='invalid', params={'field': "content",}, ) def clean_name(self): -------------这就是对单个字段的验证!!!!! print("name clean validation:", self.cleaned_data["name"]) if not self.cleaned_data["name"]: self.add_error('name', "cannot be null")
针对多对多的字段,需要在前端做一个特殊的处理:
{% if field.name in admin_class.filter_horizontal %} <div class="col-md-5"> {% get_m2m_obj_list admin_class field form_obj as m2m_obj_list%} <select id="id_{{ field.name }}_from" multiple class="filter-select-box" > {% if field.name in admin_class.readonly_fields and not admin_class.is_add_form %}----判断是否是添加页面 {% for obj in m2m_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% else %} {% for obj in m2m_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_to','id_{{ field.name }}_from')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% endif %} </select> </div> <div class="col-md-1"> 箭头 </div> <div class="col-md-5" > {% get_m2m_selected_obj_list form_obj field as selected_obj_list %} <select tag="chosen_list" id="id_{{ field.name }}_to" name="{{ field.name }}" multiple class="filter-select-box"> {% if field.name in admin_class.readonly_fields and not admin_class.is_add_form %} {% for obj in selected_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% else %} {% for obj in selected_obj_list %} <option value="{{ obj.id }}" ondblclick="MoveElementTo(this,'id_{{ field.name }}_from','id_{{ field.name }}_to')" >{{ obj }}</option> {% endfor %} {% endif %} </select> {# {% print_obj_methods form_obj %}#} </div> <span style="color: red">{{ field.errors.as_text }}</span> {% else %} {{ field }} <span style="color: grey">{{ field.help_text }}</span> <span style="color: red">{{ field.errors.as_text }}</span> {% endif %}
还需要后端做一个处理:
if self.instance.id: # 这是个修改的表单 for field in admin_class.readonly_fields: field_val = getattr(self.instance,field) #val in db if hasattr(field_val,"select_related"): #m2m ------通过判断是否有这个方法来判断是一个多对多的情况, m2m_objs = getattr(field_val,"select_related")().select_related() m2m_vals = [i[0] for i in m2m_objs.values_list('id')] -----这是把数据库中的id全部都拿到, set_m2m_vals = set(m2m_vals) ---这是用集合进行一次排序和去重, set_m2m_vals_from_frontend = set([i.id for i in self.cleaned_data.get(field)]) print("m2m",m2m_vals,set_m2m_vals_from_frontend) if set_m2m_vals != set_m2m_vals_from_frontend: -----这里就是判断前端的传过来的值和数据库的值是否一致, # error_list.append(ValidationError( # _('Field %(field)s is readonly'), # code='invalid', # params={'field': field}, # )) self.add_error(field,"readonly field") continue
实现创建的时候不做readonly的校验,
看前端页面,
{% if field.name in admin_class.readonly_fields and not admin_class.is_add_form %}----判断是否是添加页面 {% for obj in m2m_obj_list %} <option value="{{ obj.id }}" disabled>{{ obj }}</option> {% endfor %} {% else %} {% for obj in m2m_obj_list %} <option ondblclick="MoveElementTo(this,'id_{{ field.name }}_to','id_{{ field.name }}_from')" value="{{ obj.id }}">{{ obj }}</option> {% endfor %} {% endif %}
看后端的校验:
两个地方:
1是new方法里面
2是clean方法里面,
def __new__(cls, *args, **kwargs): # super(CustomerForm, self).__new__(*args, **kwargs) #print("base fields",cls.base_fields) for field_name,field_obj in cls.base_fields.items(): #print(field_name,dir(field_obj)) field_obj.widget.attrs['class'] = 'form-control' # field_obj.widget.attrs['maxlength'] = getattr(field_obj,'max_length' ) if hasattr(field_obj,'max_length') \ # else "" if not hasattr(admin_class,"is_add_form"): #代表这是添加form,不需要disabled if field_name in admin_class.readonly_fields: field_obj.widget.attrs['disabled'] = 'disabled'
这是clean方法:
if self.instance.id: # 这是个修改的表单 for field in admin_class.readonly_fields: .......