继上次CRM项目之后 我们发现了django自带admin的强大之处以及灵活性,但是admin在企业中也一样很难做到完全的对接,因此编写自己的后台管理就显得至关重要。
本次自定义admin项目将接着上次crm项目来写 :
Django CRM客户关系管理系统
创建Easy_admim 后台管理应用
- 创建app Python manage.py startapp easy_admin
- 编辑setting文件 添加app /static文件
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crm.apps.CrmConfig',
'easy_admin'
]
#...........
STATICFILES_DIRS=( os.path.join(BASE_DIR,'static'), )
- 前端准备
- bootsharp插件
- jquery插件
- 项目结构:
我们选着bootsharp官网的控制台实例作为easyadmin后台模板http://v3.bootcss.com/examples/dashboard/
- 编写路由系统
#easy_crm/urls.py from django.conf.urls import url,include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'easyadmin',include('easy_admin.urls')) ]
#easyadmin/urls.py
from django.conf.urls import url
from easy_admin import views
urlpatterns = [
url(r'^$', views.index,name="table_index"),
url(r'^/(w+)/(w+)/$', views.display_table_objs,name="table_objs"),
url(r'/(w+)/$',views.menus_url_jump,name="menus_url"),
]
- 前端代码
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="/static/css/admin_index.css"> <title>Dashboard Template for Bootstrap</title> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> {% block css %} {% endblock %} </head> <body> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <a class="navbar-brand" href="#">Brand</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav navbar-right"> <li><a href="#">{{ request.user }}</a></li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <div class="row"> <aside class="col-xs-2 sidebar"> <div class="list-group "> {% for role in request.user.userprofile.roles.all %} {% for meun in role.menus.all %} <a class="list-group-item" href={% url 'menus_url' meun.name %} >{{ meun }}</a> {% endfor %} {% endfor %} </div> </aside> <main class="col-xs-10 main"> {% block main %} {% endblock %} </main> </div> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> {% block js %} {% endblock %} </body> </html>
1 {% extends 'easyadmin/base.html' %} 2 {% block main %} 3 <h1>首页</h1> 4 {% for app_name,app_tables in table_list.items %} 5 <div class="panel panel-info"> 6 <div class="panel-heading"><h1 class="panel-title">{{ app_name }}</h1></div> 7 <div class="panel-body"> 8 <table class="table table-hover"> 9 <thead> 10 <tr><th>TB_NAME</th></tr> 11 </thead> 12 <tbody> 13 {% for table_name,admin in app_tables.items %} 14 <tr><td><a href={% url "table_objs" app_name table_name %}>{{ table_name }}</a></td><td>ADD<td>DEL</td></tr> 15 {% endfor %} 16 </tbody> 17 </table> 18 </div> 19 </div> 20 21 {% endfor %} 22 23 {% endblock %}
- 我们在index中继承base.html
{% extends 'easyadmin/base.html' %}
- views函数
def index(requests):
return render(requests,'easyadmin/index.html')
来吧 开始写功能了
目前完成主要的功能
- 顶部菜单
首先这里要说一下在模板渲染时 后端返回了一个request 而这个request对象依旧可以在模板中使用
type(requests)
<class 'django.core.handlers.wsgi.WSGIRequest'>
dir(requests)
['COOKIES', 'FILES', 'GET', 'META', 'POST', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_encoding', '_get_post', '_get_raw_host', '_get_scheme', '_initialize_handlers', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_post_parse_error', '_read_started', '_set_post', '_stream', '_upload_handlers', 'body', 'build_absolute_uri', 'close', 'content_params', 'content_type', 'csrf_processing_done', 'encoding', 'environ', 'get_full_path', 'get_host', 'get_port', 'get_raw_uri', 'get_signed_cookie', 'is_ajax', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'read', 'readline', 'readlines', 'resolver_match', 'scheme', 'session', 'upload_handlers', 'user', 'xreadlines']
requests.user也就返回了我们当前所登陆的用户
- 左侧菜单
左侧菜单实现的原理 首先根据当前登录的用户获取当前登录用户属于的角色 再根据角色获取用户所有的菜单 然后遍历出来
#views.py
def index(requests): return render(requests,'easyadmin/index.html') def menus_url_jump(requests,menu_url): return HttpResponse(menu_url)
- 主体内容
主体内容是admin的关键点有以下几个问题
- 如何只显示register注册的表?
- 如何自定义显示的字段?
- 在前端如何渲染?
我们可以定义一个字典,通过register注册的表加入到这个字典,再把这个字典传递到前端遍历{tablename:tab_obj}
那么我们又发现了一个问题,我们要如何把表归类到对应的app下呢? 很明显在前端做这个是比较麻烦的 因此我们队字典进行修改
{app_name:{tab_name:tab_obj}}
{app名字:{表一:表一对象,表二:表二对象}}
表写register函数
admin.site.register(models.CourseRecord) -->register(models.CourseRecprd)
通过表反射出app_name:
不知道是否对上次CRM中的meta方法是否有印象,在meta方法中封装了一些关于表的信息 verbose_name_plural。。等等 我们来看一下meta的防范有哪些
['FORWARD_PROPERTIES', 'REVERSE_PROPERTIES', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_expire_cache', '_forward_fields_map', '_get_fields', '_get_fields_cache', '_ordering_clash', '_populate_directed_relation_graph', '_prepare', '_property_names', '_relation_tree', 'abstract', 'add_field', 'add_manager', 'app_config', 'app_label', 'apps', 'auto_created', 'auto_field', 'base_manager', 'base_manager_name', 'can_migrate', 'concrete_fields', 'concrete_model', 'contribute_to_class', 'db_table', 'db_tablespace', 'default_apps', 'default_manager', 'default_manager_name', 'default_permissions', 'default_related_name', 'fields', 'fields_map', 'get_ancestor_link', 'get_base_chain', 'get_field', 'get_fields', 'get_latest_by', 'get_parent_list', 'get_path_from_parent', 'get_path_to_parent', 'has_auto_field', 'index_together', 'indexes', 'installed', 'label', 'label_lower', 'local_concrete_fields', 'local_fields', 'local_managers', 'local_many_to_many', 'managed', 'manager_inheritance_from_future', 'managers', 'managers_map', 'many_to_many', 'model', 'model_name', 'object_name', 'order_with_respect_to', 'ordering', 'original_attrs', 'parents', 'permissions', 'pk', 'private_fields', 'proxy', 'proxy_for_model', 'related_fkey_lookups', 'related_objects', 'required_db_features', 'required_db_vendor', 'select_on_save', 'setup_pk', 'setup_proxy', 'swappable', 'swapped', 'unique_together', 'verbose_name', 'verbose_name_plural', 'verbose_name_raw', 'virtual_fields']
app_name=models.tb._meta..app_label
创建自己的admin文件 easy_admin.py
from crm import models enabled_admins = {} class BaseAdmin(object): list_display = [] list_filters = [] list_per_page = 20 class CustomerAdmin(BaseAdmin): list_display = ['qq','name','source','consultant','consult_course','date','status'] list_filters = ['source','consultant','consult_course','status'] #model = models.Customer class CustomerFollowUpAdmin(BaseAdmin): list_display = ('customer','consultant','date') def register(model_class,admin_class=None): if model_class._meta.app_label not in enabled_admins: enabled_admins[model_class._meta.app_label] = {} #enabled_admins['crm'] = {} #admin_obj = admin_class() admin_class.model = model_class #绑定model 对象和admin 类 enabled_admins[model_class._meta.app_label][model_class._meta.model_name] = admin_class #enabled_admins['crm']['customerfollowup'] = CustomerFollowUpAdmin register(models.Customer,CustomerAdmin) register(models.CustomerFollowUp,CustomerFollowUpAdmin)
表的详细信息
通过admin_class 反射出app,table_name
我们首先在模板中是不允许带"_"的查询,因此不能直接查询的到_meta,我们知道django内置了许多标签和过滤器,同时也支持自定义标签 只需要在app下创建templatetags目录 然后创建tags.py 同时在模板中加载标签{% load tags %} 那么我们写一个通过admin_class 返回table_name的标签吧
from django import template register = template.Library() @register.simple_tag def render_app_name(admin_class): return admin_class.model._meta.verbose_name
1 def display_table_objs(requests,app_name,table_name): 2 admin_class = easy_admin.enabled_admins[app_name][table_name] 3 return render(requests,'easyadmin/display_table.html',{"admin_class":admin_class})
OK,表头就写好了,接下来就是难点,记得我们需求里面提到过,我们的内容中显示的是我们需要显示的字段,而不是所有的字段 也就是说只显示list_display中的内容,并且是可以自定义的,那么我们如何获取list_display中的内容并且返回一个queryset呢?
一个方法是直接在views中处理返回一个是定义一个tag,如果我们需要重复的使用这个方法,那么定义一个tag可以节省代码量!
@register.simple_tag def get_query_sets(admin_class): return admin_class.model.objects.all() @register.simple_tag def build_table_row(obj,admin_class): row_ele = "" for column in admin_class.list_display: field_obj = obj._meta.get_field(column) if field_obj.choices:#choices type 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") row_ele += "<td>%s</td>" % column_data return mark_safe(row_ele)
<table class="table"> <thead> <tr> {% for obj in admin_class.list_display %} <th>{{ obj }}</th> {% endfor %} </tr> </thead> <tbody> {% get_query_sets admin_class as query_sets %} {% for obj in query_sets %} <tr> {% build_table_row obj admin_class %} </tr> {% endfor %} </tbody>
过滤
:
过滤函数
def table_filter(request,admin_class): '''进行条件过滤并返回过滤后的数据''' filter_conditions = {} for k,v in request.GET.items(): if v: filter_conditions[k] =v return admin_class.model.objects.filter(**filter_conditions),filter_conditions
views函数
def display_table_objs(request,app_name,table_name): admin_class = easy_admin.enabled_admins[app_name][table_name] object_list,filter_condtions = table_filter(request, admin_class) return render(request,'easyadmin/display_table.html',{"admin_class":admin_class,"filter_condtions":filter_condtions,'object_list':object_list})
{% load tags %} <!DOCTYPE html> <html lang="cn"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="/static/css/admin_index.css"> <title>Dashboard Template for Bootstrap</title> <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> {% block css %} {% endblock %} </head> <body> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <a class="navbar-brand" href="#">Brand</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav navbar-right"> <li><a href="#">{{ request.user }}</a></li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <div class="row"> <main class="container"> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">{% render_app_name admin_class %}</h3> </div> <div class="panel-body"> {% if admin_class.list_filters %} <form class="" method="get"> {% for condtion in admin_class.list_filters %} <div class="col-lg-2"> <span>{{ condtion }}</span> {% render_filter_ele condtion admin_class filter_condtions %} </div> {% endfor %} <button type="SUBMIT" class="btn btn-success">检索</button> </form> </div> {% endif %} <table class="table"> <thead> <tr> {% for obj in admin_class.list_display %} <th>{{ obj }}</th> {% endfor %} </tr> </thead> <tbody> {% for obj in object_list %} <tr> {% build_table_row obj admin_class %} </tr> {% endfor %} </tbody> </table> </div> </div> </main> </div> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> {% block js %} {% endblock %} </body> </html>
效果:
在html中 我们丢弃了get_query_sets 因为在后端帮我们返回了queryset对象
分页
分页功能实现:http://www.cnblogs.com/tongchengbin/p/7697869.html
排序
排序涉及两个部分 一个是对从数据库中返回的object_list 另一个是前端标签的href
因为在考虑排序的同时还要考虑分页,那么先排序还是先分页呢?如果先分页,那么最后显示的数据第一页的数据依旧是最前面的数据 只是当前页的数据顺序变化,如果要达到整个数据的排序就要先排序在分页。如果没有排序我们就正常显示。另外出现的一个问题就是,默认按照id或者数据表的主键排序,而我们自定义排序的中也许没有主键或者id字段,当我们选择排序的时候最后无法回到初始的排序问题,因此可以增加一个排序还原连接,去掉排序关键字,在这里就不详细说明了。
排序函数
def easy_order(request,obj_list,admin_class): '''从request中获取排序规则 从obj中获取数据,admin_class中对验证排序有效性 返回排序后的数据 和排序规则 排序规则用于前端生成href ''' order_key = request.GET.get('o') if not order_key or order_key.strip('-') not in admin_class.list_display : return obj_list,None else: rule=0 if order_key[0]=='-' else 1 order_rule=[order_key.strip('-'),rule] return obj_list.order_by(order_key),order_rule
views函数
def display_table_objs(request,app_name,table_name): admin_class = easy_admin.enabled_admins[app_name][table_name] object_list,filter_condtions = table_filter(request, admin_class) #排序 order_rule 排序规则 [排序名字,排序顺序] object_list,order_rule=easy_order(request,object_list,admin_class) paginator = Paginator(object_list,admin_class.list_per_page) # Show 25 contacts per page page = request.GET.get('page') try: object_list=paginator.page(page) except PageNotAnInteger: object_list=paginator.page(1) except EmptyPage: object_list=paginator.page(paginator.num_pages) return render(request,'easyadmin/display_table.html',{"admin_class":admin_class,"filter_condtions":filter_condtions,'object_list':object_list,'order_rule':order_rule})
自定义标签
@register.simple_tag() def render_order_url(filter_condtions,order_rule,obj): #生产带过滤的排序url 避免排序后过滤功能失效 base_url="?" for k,v in filter_condtions.items(): base_url+="%s=%s&" %(k,v) if not order_rule or obj!=order_rule[0]: href=base_url+"o=%-s" % obj return href else: if order_rule[1]==0: href=base_url+"o=%s" % obj else: href=base_url+"o=-%s" % obj return href
前端代码:
{% for obj in admin_class.list_display %} <td><a href={% render_order_url filter_condtions order_rule obj %}><span>{{ obj }}</span></a></td> {% endfor %}
搜索功能
搜索功能主要是对object_list中查找,在写搜索前需要在Easy_admin 的baseAdmin基类中添加搜索字段 并且 如果需要使用搜索功能需要覆盖基类中的空列表list_search 讲需要搜索的字段添加进去
注意注意注意:如果包含外键不能直接写字段需要注明外键字段 双下划线连接 search_fileds = ('book__name', 'book__title', 'book__price', 'category')
def easy_search(request,obj_list,admin_class): '''从rquest中获取搜索关键字吗,admin_class中遍历需要搜索的字段''' search_key=request.GET.get('_q') con=Q() #定义搜索方式 ’或‘搜索 con.connector='OR' for column in admin_class.list_search: #contains 模糊搜索 con.children.append(("%s__contains"%column,search_key)) return obj_list.filter(con)
位了不影响过滤和搜索同时使用,我们将搜索输入框和过滤选择框放在同一个表单下,同时为了让用户知道自己搜索的内容,我们将search_key 返回给用户并显示出来。
<form class="" method="get"> <div class="row"> {% for condtion in admin_class.list_filters %} <div class="col-lg-2"> {% render_filter_ele condtion admin_class filter_condtions %} </div> {% endfor %} <button type="SUBMIT" class="btn btn-success">检索</button> </div> <hr> <div class="row"> <a></a> <div class="col-lg-4"><input type="search" value={{ search_text }} name="_q" class="form-control"></div> <button type="SUBMIT" class="btn btn-success">搜索</button> </div> </form>
跳转详情页
django 自带跳转详情页是通过list_display中第一个字段进行跳转的 跳转urll url(r'^(w+)/(w+)/(w+)/change/$',views.table_obj_change,name="menus_url"), 因为默认有id字段因此 我们把id作为(primary key 当然有的字段我们自定义了主键,那么我们就再cbaseadmin中定义一个属性吧 就叫pk_field 那么我们如何知道那个字段是主键了?百度好久没发现有案例,又想起了_meta,毕竟model中的信息都在里面,果然发现了有个pk属性,shell调试发现_meta.pk.name 返回的就是主键。开始写代码
def build_table_row(request,obj,admin_class): pk_field=admin_class.model._meta.pk.name obj_pk=getattr(obj,pk_field) row_ele = "" for index,column in enumerate(admin_class.list_display): field_obj = obj._meta.get_field(column) if field_obj.choices:#choices type 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: column_data = "<a href='{request_path}/{obj_pk}/change/'>{data}</a>".format(request_path=request.path, obj_pk=obj_pk, data=column_data) row_ele += "<td>%s</td>" % column_data return mark_safe(row_ele)
#主要该表就是在第一列加上了href 同时获取到主键字段,那么对应的后端取数据也应该是根据主键来取,而不是通过id字段,这样可以兼容后期所有的数据表结构。
编辑详情页
详细页的生成是自定义admin的重点,因为我们不可能针对每个表写一个前端的页面,那样也就不是自定义admin了,联想到django有没有自动生成html代码的模块?没错,就是modelfrom。那么问题又来了,虽然我们不需要一个一个写前端页面,但是我们还是需要一个一写modelfrom类,其实Python创建类的方式有很多,其中有一个自动创建类的方法type方法。关于类以及type方法可以参考:Python面向对象编程
froms.py
from django.forms import forms,ModelForm def create_model_form(request,admin_class): # 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 "" # return ModelForm.__new__(cls) class Meta: model=admin_class.model fields='__all__' attrs={'Meta':Meta} _model_form_class=type("DynamicModelForm",(ModelForm,),attrs) # setattr(_model_form_class, '__new__', __new__) return _model_form_class
views函数
instance的作用是填充前端的内容。
def table_obj_change(request,app_name,table_name,obj_pk): '''修改内容''' admin_class=easy_admin.enabled_admins[app_name][table_name] model_form_class = create_model_form(request, admin_class) #并不是所有的表都有id instance 填充数据 #pk_field=admin_class.model._meta.pk.name obj=admin_class.model.objects.get(pk=obj_pk) if request.method == "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) #object of type 'DynamicModelForm' has no len() 可以遍历但是没有len() 方法 return render(request,'easyadmin/table_obj_change.html',{"form_obj":form_obj})
html部分
<div class="container"> <form class="form-horizontal" role="form" method="post">{% csrf_token %} {% for field in form_obj %} <div class="form-group"> {% if field.field.required %} <label class="col-sm-2 control-label" style="font-weight: normal"><strong>{{ field.label }}</strong></label> {% else %} <label class="col-sm-2 control-label" style="font-weight: normal">{{ field.label }} </label> {% endif %} {{ field }} </div> {% endfor %} <div class="form-group"> <div class="col-sm-10 "> <button type="submit" class="btn btn-success pull-right">Save</button> </div> </div> </form> </div>
效果:
创建元素的话大同小异直接上代码吧 url 的话不用获取对象id,前端html都是后台生成的,样式可以自己修改。具体的可以看项目代码。
def table_obj_add(request,app_name,table_name): '''添加内容''' admin_class = easy_admin.enabled_admins[app_name][table_name] model_form_class = create_model_form(request, admin_class) # 并不是所有的表都有id instance 填充数据 if request.method == "POST": form_obj = model_form_class(request.POST)#添加数据 if form_obj.is_valid(): form_obj.save() else: form_obj = model_form_class() # object of type 'DynamicModelForm' has no len() 可以遍历但是没有len() 方法 return render(request, 'easyadmin/table_obj_change.html', {"form_obj": form_obj})
删除页
删除页和修改页url类似
url(r'^(w+)/(w+)/(d+)/delete/$', views.table_obj_delete,name="obj_delete"),之所以把删除也作为一个功能是因为django的删除页实在是太厉害了,在删除一个对象时,会列出关于这个对象的所有信息,并且不是简单的遍历下去,根据字段的类型,返回信息的数量也是不一样的,相比之前的代码也最为复杂。
def table_obj_delete(request,app_name,table_name,pk_obj): #删除对象 admin_class = easy_admin.enabled_admins[app_name][table_name] obj = admin_class.model.objects.get(pk=pk_obj) if request.method == "POST": obj.delete() return redirect(reverse('table_objs',args=(app_name,table_name))) return render(request, "easyadmin/table_obj_delete.html", {"obj": obj, "admin_class": admin_class, "app_name": app_name, "table_name": table_name })
前端
{% block main %} {% display_obj_related obj %} <form method="post">{% csrf_token %} <input type="submit" class="btn btn-danger" value="Yes,I'm sure"> <input type="hidden" value="yes" name="delete_confirm"> <input type="hidden" value=" name="selected_ids"> <input type="hidden" value="" name="action"> <a class="btn btn-info" href="{% url 'table_objs' app_name table_name %}">No,Take me back</a> </form> {% endblock %}
tags部分
def recursive_related_objs_lookup(objs): model_name = objs[0]._meta.model_name ul_ele = "<ul>" for obj in objs: li_ele = '''<li> %s: %s </li>'''%(obj._meta.verbose_name,obj.__str__().strip("<>")) ul_ele += li_ele for m2m_field in obj._meta.local_many_to_many: #把所有跟这个对象直接关联的m2m字段取出来了 sub_ul_ele = "<ul>" m2m_field_obj = getattr(obj,m2m_field.name) #getattr(customer, 'tags') for content in m2m_field_obj.select_related():# customer.tags.all() li_ele = '''<li> %s: %s </li>''' % (m2m_field.verbose_name, content.__str__().strip("<>")) sub_ul_ele +=li_ele sub_ul_ele += "</ul>" ul_ele += sub_ul_ele #最终跟最外层的ul相拼接 for related_obj in obj._meta.related_objects:#获取与customer表有关联的所有表 ForeignKey('customer') if 'ManyToManyRel' in related_obj.__repr__():#判断类型 if hasattr(obj, related_obj.get_accessor_name()): # hassattr(customer,'enrollment_set') accessor_obj = getattr(obj, related_obj.get_accessor_name()) print("-------ManyToManyRel",accessor_obj,related_obj.get_accessor_name()) # 上面accessor_obj 相当于 customer.enrollment_set if hasattr(accessor_obj, 'select_related'): # slect_related() == all() target_objs = accessor_obj.select_related() # .filter(**filter_coditions) # target_objs 相当于 customer.enrollment_set.all() sub_ul_ele ="<ul style='color:red'>" print(target_objs,'ta') for o in target_objs: li_ele = '''<li> %s: %s </li>''' % (o._meta.verbose_name, o.__str__().strip("<>")) sub_ul_ele += li_ele sub_ul_ele += "</ul>" ul_ele += sub_ul_ele elif hasattr(obj,related_obj.get_accessor_name()): # hassattr(customer,'enrollment_set') accessor_obj = getattr(obj,related_obj.get_accessor_name()) #上面accessor_obj 相当于 customer.enrollment_set if hasattr(accessor_obj,'select_related'): # slect_related() == all() target_objs = accessor_obj.select_related() #获取关于customer的queryset # target_objs 相当于 customer.enrollment_set.all() else: print("one to one i guess:",accessor_obj) target_objs = accessor_obj # print(target_objs,'target') if len(target_objs) >0: #print("