9-crm项目-kingadmin,修改和添加页面---动态modelform生成和filter_horizontal的实现
修改功能实现
思路:
1,模仿admin,在每一个数据的第一列,做一个超链接,点击进入修改页面
2,增加一个修改页面
3,进入页面之后增加一个views来返回数据,
4,进入页面之后把字段都展示出来,然后可以修改,一个model,做一个modelform然后在前端修改,这是规则
如果我们自己写admin,就要每一个表一个modelform,但是明显不是这样的,因为我们使用admin 的时候并没有单独每一个表写modelform,
最可能的就是写了一个modelform模板,然后你传进哪一个model进来,我就生成这样的一个modelform,
modelform是一个类啊,类怎么动态生成,可以通过type生成类,
前面讲了modelform的内容,这个地方要回顾一下,
5,下一步把修改的记录传递进来,
#########
(1)给第一列添加一个a标签
@register.simple_tag def build_table_row(request, obj,admin_class): row_ele = "" for index,column in enumerate(admin_class.list_display): try: field_obj = obj._meta.get_field(column) if field_obj.choices:#choices type,处理status这样的数字,展示为代表的汉字, column_data = getattr(obj,"get_%s_display" % column)() else: column_data = getattr(obj,column) if type(column_data).__name__ == 'datetime': column_data = column_data.strftime("%Y-%m-%d %H:%M:%S") if index == 0: #add a tag, 可以跳转到修改页 column_data = "<a href='{request_path}{obj_id}/change/'>{data}</a>".format(request_path=request.path, obj_id=obj.id, data=column_data) except FieldDoesNotExist as e : if hasattr(admin_class,column): column_func = getattr(admin_class,column) admin_class.instance = obj admin_class.request = request column_data = column_func() row_ele += "<td>%s</td>" % column_data return mark_safe(row_ele)
(2)kingadmin/urls.py
urlpatterns = [ url(r'^$', views.index,name="table_index"), url(r'^(\w+)/(\w+)/$', views.display_table_objs,name="table_objs"), url(r'^(\w+)/(\w+)/(\d+)/change/$', views.table_obj_change,name="table_obj_change"), url(r'^(\w+)/(\w+)/(\d+)/change/password/$', views.password_reset,name="password_reset"), url(r'^(\w+)/(\w+)/(\d+)/delete/$', views.table_obj_delete,name="obj_delete"), url(r'^(\w+)/(\w+)/add/$', views.table_obj_add,name="table_obj_add"), ]
(3)kingamdin/views.py
@login_required # @permission.check_permission def table_obj_change(request,app_name,table_name,obj_id): admin_class = king_admin.enabled_admins[app_name][table_name] model_form_class = create_model_form(request,admin_class) ------这个方法就是返回了对应的modelform, obj = admin_class.model.objects.get(id=obj_id) if request.method == "POST": ------>处理提交按钮的问题 print("change form",request.POST) form_obj = model_form_class(request.POST,instance=obj) #更新 if form_obj.is_valid(): form_obj.save() ------->如果校验通过直接保存 else: form_obj = model_form_class(instance=obj) ---->这是把修改的数据填充进来, return render(request,"king_admin/table_obj_change.html",{"form_obj":form_obj, "admin_class":admin_class, "app_name":app_name, "table_name":table_name})
(4)table_obj_change.html
{% extends 'king_admin/table_index.html' %} {% load tags %} {% block extra-css-resources %} <style type="text/css"> .filter-select-box{ height: 250px!important; width: 100%; border-radius: 3px; } </style> {% endblock %} {% block container %} change table <form class="form-horizontal" role="form" method="post" onsubmit="return SelectAllChosenData()">{% csrf_token %} <span style="color: red">{{ form_obj.errors }}</span> ----->统一把错误的提示信息放到这个地方 {% for field in form_obj %} <div class="form-group"> ----->这个使用bootstrap的样式,让页面更加的好看, <label class="col-sm-2 control-label" style="font-weight: normal"> {% if field.field.required %} ----->这是判断这个字段是否是必填的,如果是必填的,要加粗这个字段名字,否则不加粗, <b>{{ field.label }}</b> {% else %} {{ field.label }} {% endif %} </label> <div class="col-sm-6"> {# <input type="email" class="form-control" id="inputEmail3" placeholder="Email">#}
-----------------------------filter_horizontal ------------------------
{% if field.name in admin_class.filter_horizontal %} ------>这是另一个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 %} ----这是加了一个js的ondbclick事件,这样双击就可以移动到右边了 {% 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 %}
---------------------------------------------- </div> </div> {% endfor %} {% if not admin_class.readonly_table %} <div class="form-group"> {% block obj_delete %} <div class="col-sm-2"> <a class="btn btn-danger" href="{% url 'obj_delete' app_name table_name form_obj.instance.id %}">Delete</a> </div> {% endblock %} <div class="col-sm-10 "> <button type="submit" class="btn btn-success pull-right">Save</button> </div> </div> {% endif %} </form> <script> function MoveElementTo(ele,target_id,new_target_id) { var opt_ele = "<option value='" + $(ele).val() + "' ondblclick=MoveElementTo(this,'" + new_target_id +"','"+ target_id +"')>" + $(ele).text() + "</option>"; // $(ele).off().dblclick($(ele), parent_id) $("#" +target_id).append(opt_ele); $(ele).remove(); } function SelectAllChosenData() { $("select[tag='chosen_list'] option").each(function () { $(this).prop("selected",true); }) //remove all disabled attrs $("form").find("[disabled]").removeAttr("disabled") ; return true; } </script> {% endblock %}
这里面有一个细节就是在编辑页面,能够水平操作字段的一个功能
1.1 tag
@register.simple_tag def get_m2m_obj_list(admin_class,field,form_obj): '''返回m2m所有待选数据''' #表结构对象的某个字段 field_obj = getattr(admin_class.model,field.name) all_obj_list = field_obj.rel.to.objects.all() -----rel方法,这是根据一个字段到这个类查找到tag的所有的数据, #单条数据的对象中的某个字段 if form_obj.instance.id: obj_instance_field = getattr(form_obj.instance,field.name) selected_obj_list = obj_instance_field.all() else:#代表这是在创建新的一条记录 return all_obj_list standby_obj_list = []----备选的, for obj in all_obj_list: if obj not in selected_obj_list: standby_obj_list.append(obj) return standby_obj_list -----这是把已经选中的数据从备选列表中去除, @register.simple_tag def get_m2m_selected_obj_list(form_obj,field): --------这是数据库已经选中的标签,展示出来, '''返回已选择的m2m数据''' if form_obj.instance.id : field_obj = getattr(form_obj.instance,field.name) ----instance这个方法干什么的? return field_obj.all()
1.2 怎么把已经选择的数据移动到右侧呢?
使用js,定义了两个元素,select,要做的就是移动元素,从一个元素移动到另一个元素,
js的基本语法,
function MoveElementTo(ele,target_id,new_target_id) { var opt_ele = "<option value='" + $(ele).val() + "' ondblclick=MoveElementTo(this,'" + new_target_id +"','"+ target_id +"')>" + $(ele).text() + "</option>"; // $(ele).off().dblclick($(ele), parent_id) $("#" +target_id).append(opt_ele); $(ele).remove(); }
1.3 怎么实现一个保存右侧的内容,
对form 表单增加一个onsubmit属性,
onsubmit: 当表单提交时自动执行的javascript事件,一般在需要进行提交验证时使用.
增加一个js动作,把右侧的选中
function SelectAllChosenData() { $("select[tag='chosen_list'] option").each(function () { $(this).prop("selected",true); }) //remove all disabled attrs $("form").find("[disabled]").removeAttr("disabled") ; return true;
(5)动态modelform生成
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): 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) ----->这就是通过type,在动态生成类, 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
type动态生成类:
def hello(self):
self.name = 10
print("hello world")
t = type("hello",(),{"a":1,"hello":hello})
print(t)
T = t()
print(T.a)
T.hello()
print(T.name)
<class '__main__.hello'>
1
hello world
10
所以type是可以实现动态创建类的,其实python中一切都是对象,类也是对象;只不过是一种特殊的对象,是type的对象
添加页面
1,在列表页面,添加一个add入口
<div class="panel-heading"> <h3 class="panel-title">{% get_model_name admin_class %}----->这是获取表名, {% if not admin_class.readonly_table %} <a href="{{ request.path }}add/" class="pull-right">Add</a> {% endif %} </h3> </div>
2,添加url
urlpatterns = [ url(r'^$', views.index,name="table_index"), url(r'^(\w+)/(\w+)/$', views.display_table_objs,name="table_objs"), url(r'^(\w+)/(\w+)/(\d+)/change/$', views.table_obj_change,name="table_obj_change"), url(r'^(\w+)/(\w+)/(\d+)/change/password/$', views.password_reset,name="password_reset"), url(r'^(\w+)/(\w+)/(\d+)/delete/$', views.table_obj_delete,name="obj_delete"), url(r'^(\w+)/(\w+)/add/$', views.table_obj_add,name="table_obj_add"), ]
3,添加一个views,
@login_required def table_obj_add(request,app_name,table_name): admin_class = king_admin.enabled_admins[app_name][table_name] admin_class.is_add_form = True model_form_class = create_model_form(request,admin_class) if request.method == "POST": form_obj = model_form_class(request.POST) # if form_obj.is_valid(): form_obj.save() return redirect(request.path.replace("/add/","/")) ----->这是添加的时候保存之后,调整到列表页面, else: form_obj = model_form_class() return render(request, "king_admin/table_obj_add.html", {"form_obj": form_obj, "admin_class": admin_class})
4,添加一个html页面
{% extends "king_admin/table_obj_change.html" %} {% block obj_delete %}{% endblock %}
5,复用一个动态的modelform
6,添加之后,总是最新的数据在前面,
def table_filter(request,admin_class): '''进行条件过滤并返回过滤后的数据''' filter_conditions = {} keywords = ['page','o','_q'] for k,v in request.GET.items(): if k in keywords:#保留的分页关键字 and 排序关键字 continue if v: filter_conditions[k] =v print("filter coditions",filter_conditions) return admin_class.model.objects.filter(**filter_conditions).\ order_by("-%s" % admin_class.ordering if admin_class.ordering else "-id"),\ filter_conditions
##########################
技术改变命运