权限菜单展示
1.展示的效果
实现该效果,涉及到的核心点(突破点)
1. 权限分类:
(1)菜单权限 (主要用来展示的权限(url)如查看权限 url, 如上图中的每个权限都归类为菜单权限,主要用来构建权限列表)。
(2)按钮权限(主要用来 添加,删除,修改 的权限url )。
(3) 实现 权限分类的方法。
在权限model类 Permission ,设置一个 type 字段 type = model.Charfield(max_length=32,choices=(("menu","菜单权限"),(“button”,"按钮权限")),default="menu"))
后端通过该type 字段判断哪些权限需要展示。
2. 权限的树形结构。构建 权限分层,就是权限归类。如 信息管理 权限下有--- 客户管理权限,订单管理权限,客户管理权限下有查看客户权限,订单管理权限下有查看订单。
(1)自关联。
在权限model类Permission 中设置一个字段自关联字段,类似一对多 的情况,就是一个父权限可以多个子权限, 该字段用于记录每个权限的父权限id
#自关联,解决 层级关系问题,谁是谁的子权限 ,树形结构
parent = models.ForeignKey("Permission",on_delete=models.CASCADE,null=True,blank=True) #null = True 该权限的父权限可以为空, blank=True 添加该权限时可以不填内容。
(2)权限列表树形结构数据的构建
需要通过菜单权限列表,构建出以下类型的数据结构,如何实现。
default_data = [
{
"text": '信息管理',
"href": '',
"tags": ['2'],
"nodes": [
{
"text": '客户管理',
"href": '',
"tags": ['1'],
"nodes": [
{
"text": '查看客户',
"href": '/stark/app1/customer/',
"tags": ['0']
},
]
},
{
"text": '订单管理',
"href": '',
"tags": ['1'],
"nodes": [
{
"text": '查看订单',
"href": '/stark/app1/order/',
"tags": ['0']
},
]
},
]
},
{
"text": '权限管理',
"href": '',
"tags": ['0']
},
]
实现逻辑:
1. 用户登录成功后,获取该用户的权限,并构建权限列表
def permission_init(request,user_obj):
queryset = user_obj.roles.all().values(
"permissions__title", # 跨表
"permissions__url",
"permissions__type",
"permissions__parent",
"permissions__id",
).distinct()
# [{'permissions__title': '查看客户', 'permissions__url': '/stark/app1/customer/'},.....]
# 准备一个权限列表,用来装用户所有的权限
permissions_list = []
for per in queryset:
permissions_list.append(
{
"url": per.get("permissions__url"),
"title": per.get("permissions__title"),
"type": per.get("permissions__type"),
"pid": per.get("permissions__parent"),
"id": per.get("permissions__id"),
}
)
# 将权限列表注入session ,用户下次登录可以获取到该权限列表
request.session["permission_list"] = permissions_list
2.获取用户的权限列表
(1)先经过,权限类型过滤,判断是菜单权限还是按钮权限,过滤出菜单权限,用于展示到页面。
(2)通过 菜单权限列表,构建 树形结构数据(前端需要展示的树形结构数据)
(3)权限列表路径展开问题的解决
难点在于如何给当前选择的路径的所有父权限,设置属性。state :{“expanded”:True}
from django import template
import json
register = template.Library()
@register.inclusion_tag("rbac/menu.html")
def get_menu(request):
permission_list = request.session.get("permission_list") #获取用户的权限列表
per_list = []
permission_dic ={}
#循环重构字典 , 每一个权限的id 为键,新字典为value ,构成一个新的字典
for per_dic in permission_list:
new_per_dic = {}
#剔除非菜单权限
if per_dic.get("type") == "button":
continue
#对菜单权限,重构数据结构,以id 值为键,新数据结构为value值
new_per_dic["text"] = per_dic["title"]
new_per_dic["href"] = per_dic["url"] or ""
new_per_dic["pid"] = per_dic["pid"] or ""
new_per_dic["nodes"] = []
#new_per_dic["state"] = {"expanded":True} #此属性 设置节点展开 ,bootstrap_view 插件中的语法state :expanded为True 时节点时展开的
permission_dic[per_dic.get("id")] = new_per_dic
#重构出的字典大致结构如下:
# permission_dic={
# 1:{"text":"信息管理","href":"","nodes":[],"pid":None},
# 2:{"text":"权限管理","href":"","nodes":[],"pid":None},
# 3:{"text":"客户管理","href":"","nodes":[],"pid":1},
# 4:{"text":"订单管理","href":"","nodes":[],"pid":1},
# 6:{"text":"查看客户","href":"","nodes":[],"pid":3},
# 7:{"text":"查看订单","href":"","nodes":[],"pid":4},
# }
# 路径展开
current_path = request.path
#过滤出一级权限,也就是 父pid 为None 的权限 ,加入已经准备好的数据列表
for id,dic in permission_dic.items():
#找到根节点,也就是一级权限
if not dic["pid"]:
per_list.append(dic)
else:
permission_dic[dic["pid"]]["nodes"].append(dic)
#路径展开逻辑
if dic["href"]==current_path: #找到当前的路径
pid =dic["pid"] #找到当前路径的父级 pid
#通过while 循环不断的往上找父及诶单,并将父节点设置 相关的属性
while pid:
permission_dic[pid]["state"] = {"expanded":True}
pid = permission_dic[pid]["pid"]
#为什么需要用json格式的,因为python代码 None,Ture 在js 语法中不能识别,所以需要转化成json字符串给插件
return {"default_data": json.dumps(per_list)}
inclusion_tag("filename.html")
1. 该自定义标签,返回必须是一个字典形式,一般该方法需要带参数reuqest , 是通过render 方法进行渲染的。在全局可以应用,该标签。
2. 应用场景,权限菜单栏,在增,删,改,查页面都需要通过动态数据渲染出菜单权限栏时,用此中方法可以解决代码重复问题。
3. 实现 效果: 给调用该方法的页面返回一个html 前端标签代码。相当于 通过该标签方法,得到后端数据,返回字典格式的数据,该数据
在 filename.html 渲染后,将该filename.html后的效果返回给前端页面调用该方法的位置。
该方法写在 上图rbac.py 中:
from django import template import json register = template.Library() @register.inclusion_tag("rbac/menu.html") def get_menu(request): permission_list = request.session.get("permission_list") per_list = [] permission_dic ={} #循环重构字典 , 每一个权限的id 为键,新字典为value ,构成一个新的字典 for per_dic in permission_list: new_per_dic = {} #剔除非菜单权限 if per_dic.get("type") == "button": continue #对菜单权限,重构数据结构,以id 值为键,新数据结构为value值 new_per_dic["text"] = per_dic["title"] new_per_dic["href"] = per_dic["url"] or "" new_per_dic["pid"] = per_dic["pid"] or "" new_per_dic["nodes"] = [] #new_per_dic["state"] = {"expanded":True} #此属性 设置节点展开 ,bootstrap_view 插件中的语法state :expanded为True 时节点时展开的 permission_dic[per_dic.get("id")] = new_per_dic #重构出的字典大致结构如下: # permission_dic={ # 1:{"text":"信息管理","href":"","nodes":[],"pid":None}, # 2:{"text":"权限管理","href":"","nodes":[],"pid":None}, # 3:{"text":"客户管理","href":"","nodes":[],"pid":1}, # 4:{"text":"订单管理","href":"","nodes":[],"pid":1}, # 6:{"text":"查看客户","href":"","nodes":[],"pid":3}, # 7:{"text":"查看订单","href":"","nodes":[],"pid":4}, # } # 路径展开 current_path = request.path #过滤出一级权限,也就是 父pid 为None 的权限 ,加入已经准备好的数据列表 for id,dic in permission_dic.items(): #找到根节点,也就是一级权限 if not dic["pid"]: per_list.append(dic) else: permission_dic[dic["pid"]]["nodes"].append(dic) #路径展开逻辑 if dic["href"]==current_path: #找到当前的路径 pid =dic["pid"] #找到当前路径的父级 pid #通过while 循环不断的往上找父及诶单,并将父节点设置 相关的属性 while pid: permission_dic[pid]["state"] = {"expanded":True} pid = permission_dic[pid]["pid"] #为什么需要用json格式的,因为python代码 None,Ture 在js 语法中不能识别,所以需要转化成json字符串给插件 return {"default_data": json.dumps(per_list)}
menu.html:(该标签html ,参数文件)
<div id="treeview" class="small"> </div> <script src="/static/bootstrap-treeview/js/bootstrap-treeview.js"></script> <script type="text/javascript"> // API文档参数列表: https://www.cnblogs.com/tangzeqi/p/8021637.html $(function () { var options = { data:{{ permission_tree_list|safe }}, //data属性是必须的,是一个对象数组 Array of Objects. color: "", //所有节点使用的默认前景色,这个颜色会被节点数据上的backColor属性覆盖. String backColor: "#22282e", //所有节点使用的默认背景色,这个颜色会被节点数据上的backColor属性覆盖. String borderColor: "#000000", //边框颜色。如果不想要可见的边框,则可以设置showBorder为false。 String nodeIcon: "glyphicon glyphicon-stop", //所有节点的默认图标 checkedIcon: "glyphicon glyphicon-check", //节点被选中时显示的图标 String collapseIcon: "glyphicon glyphicon-minus", //节点被折叠时显示的图标 String expandIcon: "glyphicon glyphicon-plus", //节点展开时显示的图标 String emptyIcon: "glyphicon", //当节点没有子节点的时候显示的图标 String enableLinks: false, //是否将节点文本呈现为超链接。前提是在每个节点基础上,必须在数据结构中提供href值。 Boolean highlightSearchResults: true, //是否高亮显示被选中的节点 Boolean levels: 2, //设置整棵树的层级数 Integer multiSelect: false, //是否可以同时选择多个节点 Boolean onhoverColor: "#F5F5F5", //光标停在节点上激活的默认背景色 String selectedIcon: "glyphicon glyphicon-stop", //节点被选中时显示的图标 String searchResultBackColor: "", //当节点被选中时的背景色 searchResultColor: "", //当节点被选中时的前景色 selectedBackColor: "", //当节点被选中时的背景色 selectedColor: "#FFFFFF", //当节点被选中时的前景色 showBorder: true, //是否在节点周围显示边框 showCheckbox: false, //是否在节点上显示复选框 showIcon: true, //是否显示节点图标 showTags: false, //是否显示每个节点右侧的标记。前提是这个标记必须在每个节点基础上提供数据结构中的值。 uncheckedIcon: "glyphicon glyphicon-unchecked", //未选中的复选框时显示的图标,可以与showCheckbox一起使用 }; $('#treeview').treeview({ color: "white", onhoverColor:"#394555", selectedColor:"white", showBorder: false, expandIcon: 'glyphicon glyphicon-chevron-right', collapseIcon: 'glyphicon glyphicon-chevron-down', nodeIcon: 'glyphicon glyphicon-bookmark', enableLinks: true, levels: 1, showIcon: false, selectedBackColor: "", backColor: "#22282e", highlightSearchResults: true, data: {{ permission_tree_list|safe }}, }); $('#treeview').on('nodeSelected', function (event, data) { console.log(data); }) }); </script>
base.html :(调用该标签的文件)
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> {% block title %} <title>Starter Template for Bootstrap</title> {% endblock title %} <link href="/static/bootstrap/css/bootstrap.css" rel="stylesheet"> <link rel="stylesheet" href="/static/css/site.css"> <script src="/static/js/jquery-3.3.1.js"></script> </head> <body> <div class="container-fluid site-wrapper"> <!-- site-navbar --> <div class="site-navbar navbar-fixed-top"> <span class="site-navbar-title">CRM后台管理</span> </div> <!-- /.site-navbar --> <div class="row"> <!-- site-menu --> <div class="site-menu"> <p class="site-menu-title">主菜单</p> {% block side_bar %} {% load rbac %} {% get_menu request %} {% endblock side_bar %} </div> <!-- /.site-menu --> <!-- site-body --> <div class="site-body"> <div class="site-content-wrap"> {% block content %} {% endblock content %} </div> </div> <!-- site-body --> </div> </div><!-- /.container-fluid --> <script src="/static/bootstrap/js/bootstrap.js"></script> <script src="/static/js/site.js"></script> {% block script %} {% endblock script %} </body> </html>