阅读目录
-
二级菜单权限
二级菜单权限
该内容的介绍是基于 lufficypermission 项目来完成的。
介绍及需求
1、什么是二级菜单权限?
一级菜单权限:是指将当前登录用户所具有的权限,可以放到左侧菜单栏处的权限
二级菜单权限:当一级菜单权限非常多的时候,可以对其进行归类,例如客户列表属于信息管理,缴费列表属于财务管理,如下图:
2、需求
-
左侧菜单栏权限有分级显示,需要有地方存储一级菜单的内容,Menu表
-
点击一级菜单时,点击谁谁展开,(点击信息管理时,客户列表展开缴费列表收起;点击财务管理时,缴费列表展开客户列表收起)
-
当前二级菜单发送请求后,在菜单栏处仍然是展开当前的二级菜单
-
权限按钮控制,点击二级菜单时,内容区域没有的权限,其a标签按钮是隐藏的
-
变面包屑,路径导航列表,在内容区域的上面,实现路径,并且点击可以直接发送请求
具体的代码实现
1、视图函数(views.py)
登录成功后,将权限信息注入session
from django.shortcuts import render, HttpResponse, redirect, reverse
from rbac import models
from rbac.service.rbac import init_permission
# 登录
def login(request):
if request.method == 'POST':
username = request.POST.get('username')
pwd = request.POST.get('pwd')
user = models.User.objects.filter(name=username, password=pwd).first()
if not user:
err_msg = '用户名或密码错误'
return render(request, 'login.html', {'err_msg': err_msg})
request.session["user_id"]=user.pk
# 登录成功
# 将权限信息写入到session
init_permission(request,user) # user表示的是当前用户对象
return redirect(reverse('customer'))
return render(request, 'login.html')
视图(客户列表的增删改查)
View Code
视图(缴费的增删改查)
View Code
2、权限相关(都放入项目rbac中)
(1)模型类(models.py)
from django.db import models
class User(models.Model):
"""
用户表
"""
name = models.CharField(max_length=32, verbose_name='用户名')
password = models.CharField(max_length=32, verbose_name='密码')
roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True)
def __str__(self):
return self.name
class Role(models.Model):
"""
角色表
"""
name = models.CharField(max_length=32, verbose_name='角色名称')
permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True)
def __str__(self):
return self.name
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(max_length=32, verbose_name='标题')
url = models.CharField(max_length=32, verbose_name='权限')
name = models.CharField(max_length=32, verbose_name="url别名", default="") # 别名用于按钮控制,没有权限的按钮隐藏
menu = models.ForeignKey("Menu", on_delete=models.CASCADE, null=True) # 关联菜单表,通过menu是否有值,可以判断是否为菜单权限
pid = models.ForeignKey("self", on_delete=models.CASCADE, null=True, verbose_name="父权限") # 用于判断,点击添加或编辑菜单栏对应的二级菜单仍然展开, 设置了父权限和子权限,该字段是自关联, # 有pid值就是子权限,没有pid值就是菜单权限
class Meta:
verbose_name_plural = '权限表'
verbose_name = '权限表'
def __str__(self):
return self.title
class Menu(models.Model):
"""
菜单表, 用于存放一级菜单标题和图案
"""
title = models.CharField(max_length=32, verbose_name='菜单')
icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True)
(2)将权限注入session中的方法 init_permission(request,user)
rbac(app) / service / rbac.py
from rbac.models import Role
def init_permission(request,user_obj): #参数user为当前的登录用户对象
# 查看当前登录用户拥有的所有权限
print("user",user)
permissions = Role.objects.filter(user=user_obj).values("permissions__title", # title url用于菜单栏展示与发送请求
"permissions__url",
"permissions__name", # 别名用于按钮控制
"permissions__pk", # pk pid 用于判断添加或编辑时,对应的菜单权限扔展开,
"permissions__pid", # 存放父权限,表是自关联
"permissions__menu__title", # menu用于存一级菜单
"permissions__menu__icon",
"permissions__menu__pk").distinct()
permission_list = [] # 存放当前登录用户所有的权限,用于在中间件进行校验字典形式
permission_names = [] # 存放别名,用于按钮控制的时候,比较
permission_menu_dict = {} #存放的是菜单
for item in permissions:
# 构建所有权限列表用于中间件的校验,一条权限是一个字典
permission_list.append({
"url":item["permissions__url"],
"title":item["permissions__title"],
"id":item["permissions__pk"],
"pid":item["permissions__pid"]
})
# 构建别名列表,用于权限的按钮控制
permission_names.append(item["permissions__name"])
# 菜单权限
menu_pk = item["permissions__menu__pk"]
if menu_pk: #判断菜单权限是否存在
if menu_pk not in permission_menu_dict: # 一级菜单不存在时,需要创建
# 菜单权限的格式,其中若一级菜单下有多个二级菜单权限,那么children中会有多个字典
permission_menu_dict[menu_pk]={
"menu_title":item["permissions__menu__title"],
"menu_icon":item["permissions__menu__icon"],
"children":[
{
"title":item["permissions__title"],
"url":item["permissions__url"],
"pk":item["permissions__pk"] #存放当前的菜单权限的id值,(也是父权限的id)
}
]
}
else:
# 一级菜单存在时,直接将菜单权限添加到children中
permission_menu_dict[menu_pk]["children"].append({
"title":item["permissions__title"],
"url":item["permissions__url"], "pk":item["permissions_pk"]
})
# 将当前登录人的所有权限,别名,菜单字典注入session中
request.session["permission_list"]=permission_list
request.session["permission_names"]=permission_names
request.session["permission_menu_dict"]=permission_menu_dict
(3)中间件的校验
rbac(app) / service / middlewares.py
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,redirect
import re
from rbac.models import Permission
class PermissionMiddleWare(MiddlewareMixin):
def process_request(self,request):
current_path = request.path
# 设置白名单
for reg in ["/login/","/admin*/"]: # 设置/admin*/ 为了使用admin后台录入数据
ret = re.search(reg,current_path)
if ret:
return None
# 验证登录 登录验证的时候要根据登录认证的时候是什么方式,这里就用什么方式验证
user_id = request.session.get("user_id")
if not user_id:
return redirect("/login/")
# 1、校验权限 2、添加导航列表(做面包细) 3、添加show_id(用于判断当前url,与菜单权限的pk比较,菜单栏相应的二级菜单展开) # 菜单权限校验
permission_list = request.session.get("permission_list")
# 路径导航列表(面包细)将其存放到request内的添加属性breadcrumb(可以随便起名)中,用于在母版layout.html中渲染
request.breadcrumb=[
{
"title":"首页",
"url":"/"
}
]
for item in permission_list:
reg = "^%s$"%(item["url"])
ret = re.search(reg,current_path)
if ret: # 菜单权限校验成功
show_id = item["pid"] or item["id"] # 有item["pid"](子权限url的pid值)等于item["pid"],没有值则等于item["id"](父权限的pk)
request.show_id = show_id # 当前路径url的 pid ,show_id ,添加到request中临时属性中,便于后面进行比较
if item["pid"]:
# 请求为子权限时
p_permission = Permission.objects.filter(pk=item["pid"]).first() #子权限的父权限
# extend 是追加多个,需要放到列表中
request.breadcrumb.extend([
# 父权限字典
{
"title":p_permission.title,
"url":p_permission.url
},
# 子权限字典
{
"title":item["title"],
# "url":item["url"] #由于数据库中存放的是正则的字符串,不能用item["url"],可以直接使用当前路径
"url":request.path
}
])
else:
# 请求为菜单父权限时,直接加入到request.breadcrumb属性中
request.breadcrumb.append({
"title": item["title"],
"url": item["url"]
})
return None
return HttpResponse("无权访问该网页!")
(4)templatetags文件,用于自定义过滤器或自定义过滤标签,py文件必须放在 templatetags(文件名不可更改)文件中
rbac(app) / templatetags / rbac.py
from django import template
register = template.Library() # 命名必须为 register
from django.conf import settings
import re
# 1、将菜单权限字典 默认传给 templates/rbac/menu.html中
@register.inclusion_tag("rbac/menu.html") # 默认找 templates 包中的文件,因此路径中不用写 template
def get_menu(request):
permission_menu_dict = request.session.get("permission_menu_dict")
# 二级菜单,点击谁,谁出现,不点击的隐藏,先默认全部隐藏,
for val in permission_menu_dict.values():
for item in val["children"]:
val["class"]="hide"
# ret = re.search("^%s$"%(item["url"]),request.path) # 之前是只判断当前路径跟菜单权限一样展开,但是对应的添加和编辑时就不展开了,不符合需求
if request.show_id==item["pk"]: # 当前请求url的show_id 与菜单权限的pk一样时,那么该二级菜单权限是展开的
val["class"]=""
return {"permission_menu_dict":permission_menu_dict}
# 2、自定义过滤器,传值btn_url在别名列表中,那么返回True ,用于判断权限按钮隐藏是否隐藏
@register.filter
def has_permission(btn_url,request):
permission_names=request.session.get("permission_names")
return btn_url in permission_names
(5)templates
rbac(app) / templates / rbac / menu.html
<div class="multi-menu">
{% for item in permission_menu_dict.values %} # permission_menu_dict 是get_menu方法传值的,根据传的值来渲染标签,供模板调用
<div class="item">
<div class="title"><i class="{{ item.menu_icon }}"></i>{{ item.menu_title }}</div>
<div class="body {{ item.class }}" >
{% for foo in item.children %}
<a href="{{ foo.url }}">{{ foo.title }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
# rbac(app) / static / css / menu.css
View Code
rbac(app) / static / js / menu.js
通过点击一级菜单控制二级菜单的显示隐藏
$('.item .title').click(function () {
$(this).next().toggleClass('hide');
$(this).parent().siblings().children(".body").addClass("hide")
});
(6)模板调用(渲染的简单介绍)
♥♥♥♥♥ {% load rbac %} 会自动去找 templatetags 文件中的 rbac
a、layout.html 菜单栏中二级菜单的渲染
<div class="left-menu">
<div class="menu-body">
{% load rbac %}
{% get_menu request %}
</div>
</div>
b、customer_list.html 非菜单权限按钮的控制隐藏(以添加为例)
<div class="btn-group" style="margin: 5px 0"> {% load rbac %} {% if "customer_add"|has_permission:request %} <a class="btn btn-default" href="/customer/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} </div>
c、其他模板
layout.html 和 customer_list.html 有必要看看具体是怎么进行渲染的。
layout.html
login.html
customer_list.html
customer_add.html
customer_edit
payment_list.html
payment_add.html
payment_edit.html
models.py customer表和 payment表
View Code
总结1:二级菜单分级动态显示
一级菜单需要保存在Menu表中,保存名称与图标,在权限表中有 menu 字段(foreginekey)与之关联,其中属于菜单权限的,menu字段有值
思路:菜单权限的数据格式,便于后面渲染时取值
permission_menu_dict= { 1:{ "title":"信息管理", "icon":"", "children":[ { "title":"客户列表", "url":"", "pk":"", }, { "title":"我的私户", "url":"", "pk":"", }, ] }, 2:{ "title":"财务管理", "icon":"", "children":[ { "title":"缴费列表", "url":"", "pk":"", }, ] }, }
总结2:点击一级菜单控制二级菜单的显示隐藏
点击一级菜单时,点击谁谁展开,(点击信息管理时,客户列表展开缴费列表收起;点击财务管理时,缴费列表展开客户列表收起)
思路:可以通过DOM操作来完成,点击一级菜单时,next下一个标签是隐藏的,其他一级菜单的孩子都是hide
rbac(app) / static / js / menu.js (通过点击一级菜单控制二级菜单的显示隐藏)
$('.item .title').click(function () { $(this).next().toggleClass('hide'); $(this).parent().siblings().children(".body").addClass("hide") });
总结3:二级菜单、非菜单权限 发送请求时,相应的菜单权限仍然是展开的
当前二级菜单、非菜单权限发送请求后,在菜单栏处仍然是展开当前的二级菜单
思路:
-
在权限表中添加一个字段 pid (自关联),父权限也就是相应的菜单权限
-
分两种情况,当前请求url 为菜单权限时或为子权限时:
-
-
在中间件中定义一个 show_id 用来记录 子权限的 pid 或父权限的pk,因为在中间件中对当前登录用户所有权限进行校验,此时可以将权限的 pid 或 pk 取出来,所以在中间件中就添加 show_id
-
将 show_id 存储在 request.show_id 的临时属性中,因为 request是全局变量,可以供后面其他文件直接使用
-
-
在 templatetags 中的 rbac.py 文件中 get_menu 方法中比较 show_id 与相应菜单权限的 pk, 一样则 菜单权限是展开的
1、permission表新增一个pid字段,表示非菜单权限的父级菜单权限id,permission模型类如下:
class Permission(models.Model): """ 权限表 """ url = models.CharField(verbose_name='含正则的URL', max_length=32) title = models.CharField(verbose_name='标题', max_length=32) menu = models.ForeignKey(verbose_name='标题', to="Menu", on_delete=models.CASCADE, null=True) name = models.CharField(verbose_name='url别名', max_length=32, default="") pid = models.ForeignKey("self", on_delete=models.CASCADE, null=True, verbose_name='父权限') def __str__(self): return self.title
# 权限表结构
2、修改权限列表数据结构,注入session,setsession.py中代码如下:
详细见前面的代码。
3、# 中间件中 要做修改
for item in permission_list: reg = "^%s$"%(item["url"]) ret = re.search(reg,current_path) # 校验当前全向成功 if ret: show_id = item["pid"] or item["id"] # 有item["pid"](子权限url的pid值)等于item["pid"],没有值则等于item["id"](父权限的pk) request.show_id = show_id # 当前路径url的 pid ,show_id ,添加到request中临时属性中,便于后面进行比较
4、# templatetags / rbac.py
# 1、将菜单权限字典 默认传给 templates/rbac/menu.html中 @register.inclusion_tag("rbac/menu.html") # 默认找 templates 包中的文件,因此路径中不用写 template def get_menu(request): permission_menu_dict = request.session.get("permission_menu_dict") # 二级菜单,点击谁,谁出现,不点击的隐藏,先默认全部隐藏, for val in permission_menu_dict.values(): for item in val["children"]: val["class"]="hide" # 默认是隐藏的 # ret = re.search("^%s$"%(item["url"]),request.path) # 之前是只判断当前路径跟菜单权限一样展开,但是对应的添加和编辑时就不展开了,不符合需求 if request.show_id==item["pk"]: # 当前请求url的show_id 与菜单权限的pk一样时,那么该二级菜单权限是展开的 val["class"]="" return {"permission_menu_dict":permission_menu_dict}
总结4:动态显示按钮权限(非菜单权限)
权限按钮控制,点击二级菜单时,内容区域没有的权限,其a标签按钮是隐藏的
除了菜单权限,还有按钮权限,比如添加用户(账单),编辑用户(账单),删除用户(账单),这些不是菜单选项,而是以按钮的形式在页面中显示,但不是所有的用户都有所有的按钮权限,我们需要在用户不拥有这个按钮权限时就不要显示这个按钮,下面介绍一下思路和关键代码。
1、在permission表中增加一个字段name,permission模型类如下:
class Permission(models.Model): """ 权限表 """ url = models.CharField(verbose_name='含正则的URL', max_length=32) title = models.CharField(verbose_name='标题', max_length=32) menu = models.ForeignKey(verbose_name='标题', to="Menu", on_delete=models.CASCADE, null=True) name = models.CharField(verbose_name='url别名', max_length=32, default="") def __str__(self): return self.title
注意:permission表中新增name字段别名与urls.py中的别名没有关系,当然也可以起一样的名字,心里明白他们其实并无关系即可。
2、将权限别名列表注入session,setsession.py中代码如下:
详细代码见前面 # rbac(app) / service / rbac.py 中关于permission_names
def initial_session(user_obj, request): """ 将当前登录人的所有权限url列表和 自己构建的所有菜单权限字典和 权限表name字段列表注入session :param user_obj: 当前登录用户对象 :param request: 请求对象HttpRequest """ # 查询当前登录人的所有权限列表 ret = Role.objects.filter(user=user_obj).values('permissions__url', 'permissions__title', 'permissions__name', 'permissions__menu__title', 'permissions__menu__icon', 'permissions__menu__id').distinct() permission_list = [] permission_names = [] permission_menu_dict = {} for item in ret: # 获取用户权限列表用于中间件中权限校验 permission_list.append(item['permissions__url']) # 获取权限表name字段用于动态显示权限按钮 permission_names.append(item['permissions__name']) menu_pk = item['permissions__menu__id'] if menu_pk: if menu_pk not in permission_menu_dict: permission_menu_dict[menu_pk] = { "menu_title": item["permissions__menu__title"], "menu_icon": item["permissions__menu__icon"], "children": [ { "title": item["permissions__title"], "url": item["permissions__url"], } ], } else: permission_menu_dict[menu_pk]["children"].append({ "title": item["permissions__title"], "url": item["permissions__url"], }) print('权限列表', permission_list) print('菜单权限', permission_menu_dict) # 将当前登录人的权限列表注入session中 request.session['permission_list'] = permission_list # 将权限表name字段列表注入session中 request.session['permission_names'] = permission_names # 将当前登录人的菜单权限字典注入session中 request.session['permission_menu_dict'] = permission_menu_dict
3、自定义过滤器 # templatetags / rbac.py
判断当前的请求路径是否在按钮权限字典中,
@register.filter def has_permission(btn_url, request): permission_names = request.session.get("permission_names") return btn_url in permission_names # 在则返回True
4、使用过滤器,模板(如customer_list.html)中部分权限按钮代码如下:
<div class="btn-group"> {% load my_tags %} {% if "customer_add"|has_permission:request %} <a class="btn btn-default" href="/customer/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} </div>
总结5:路径导航栏(面包屑)
面包屑,路径导航列表,在内容区域的上面,实现路径,并且点击可以直接发送请求,
如图:
1、中间件的时候,将路径列表存储在request.breadcrumb中
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import redirect, HttpResponse import re class PermissionMiddleWare(MiddlewareMixin): def process_request(self, request): # 设置白名单放行 for reg in ["/login/", "/admin/*"]: ret = re.search(reg, request.path) if ret: return None # 检验是否登录 user_id = request.session.get('user_id') if not user_id: return redirect('/login/') # 检验权限 permission_list = request.session.get('permission_list') # 路径导航列表 request.breadcrumb = [ { "title": "首页", "url": "/" }, ] for item in permission_list: reg = '^%s$' % item["url"] ret = re.search(reg, request.path) if ret: show_id = item["pid"] or item["id"] request.show_id = show_id # 给request对象添加一个属性 # 确定面包屑列表 if item["pid"]: ppermission = Permission.objects.filter(pk=item["pid"]) .first() request.breadcrumb.extend( [{ # 父权限字典 "title": ppermission.title, "url": ppermission.url }, { # 子权限字典 "title": item["title"], "url": request.path }] ) else: request.breadcrumb.append( { "title": item["title"], "url": item["url"] } ) return None return HttpResponse('无权访问')
2、在母版中渲染 面包屑
母版中的 html 文件
<div class="content-wrapper"> <!-- 添加面包屑 --> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {% for item in request.breadcrumb %} <li><a href="{{ item.url }}">{{ item.title }}</a></li> {% endfor %} </ol> </div> <br> {% block content %} <!-- 其他子模板中调用的时候,代码区域 --> {% endblock %} </div>