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:
                .......
原文地址:https://www.cnblogs.com/andy0816/p/13687364.html