Django:二级菜单权限动态显示
二级菜单权限
该内容的介绍是基于 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')
视图(客户列表的增删改查)
import os import mimetypes from django.shortcuts import render, redirect from django.http import FileResponse from django.conf import settings # import xlrd from web import models from web.forms.customer import CustomerForm def customer_list(request): """ 客户列表 :return: """ data_list = models.Customer.objects.all() return render(request, 'customer_list.html', {'data_list': data_list}) def customer_add(request): """ 编辑客户 :return: """ if request.method == 'GET': form = CustomerForm() return render(request, 'customer_edit.html', {'form': form}) form = CustomerForm(data=request.POST) if form.is_valid(): form.save() return redirect('/customer/list/') return render(request, 'customer_edit.html', {'form': form}) def customer_edit(request, cid): """ 新增客户 :return: """ obj = models.Customer.objects.get(id=cid) if request.method == 'GET': form = CustomerForm(instance=obj) return render(request, 'customer_add.html', {'form': form}) form = CustomerForm(data=request.POST, instance=obj) if form.is_valid(): form.save() return redirect('/customer/list/') return render(request, 'customer_add.html', {'form': form}) def customer_del(request, cid): """ 删除客户 :param request: :param cid: :return: """ models.Customer.objects.filter(id=cid).delete() return redirect('/customer/list/')
视图(缴费的增删改查)
#!/usr/bin/env python # -*- coding:utf-8 -*- from django.shortcuts import render, redirect from web import models from web.forms.payment import PaymentForm, PaymentUserForm def payment_list(request): """ 付费列表 :return: """ data_list = models.Payment.objects.all() return render(request, 'payment_list.html', {'data_list': data_list}) def payment_add(request): """ 编辑付费记录 :return: """ if request.method == 'GET': form = PaymentForm() return render(request, 'payment_edit.html', {'form': form}) form = PaymentForm(data=request.POST) if form.is_valid(): form.save() return redirect('/payment/list/') return render(request, 'payment_edit.html', {'form': form}) def payment_edit(request, pid): """ 新增付费记录 :return: """ obj = models.Payment.objects.get(id=pid) if request.method == 'GET': form = PaymentForm(instance=obj) return render(request, 'payment_add.html', {'form': form}) form = PaymentForm(data=request.POST, instance=obj) if form.is_valid(): form.save() return redirect('/payment/list/') return render(request, 'payment_add.html', {'form': form}) def payment_del(request, pid): """ 删除付费记录 :param request: :param cid: :return: """ models.Payment.objects.filter(id=pid).delete() return redirect('/payment/list/')
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
.static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .static-menu a { text-decoration: none; padding: 8px 15px; border-bottom: 1px solid #ccc; color: #333; display: block; background: #efefef; background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa)); background: -ms-linear-gradient(bottom, #efefef, #fafafa); background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%); background: -o-linear-gradient(bottom, #efefef, #fafafa); filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')"; box-shadow: inset 0px 1px 1px white; } .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } .multi-menu .item { background-color: white; } .multi-menu .item > .title { padding: 10px 5px; border-bottom: 1px solid #dddddd; cursor: pointer; color: #333; display: block; background: #efefef; background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa)); background: -ms-linear-gradient(bottom, #efefef, #fafafa); background: -o-linear-gradient(bottom, #efefef, #fafafa); filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')"; box-shadow: inset 0 1px 1px white; } .multi-menu .item > .body { border-bottom: 1px solid #dddddd; } .multi-menu .item > .body a { display: block; padding: 5px 20px; text-decoration: none; border-left: 2px solid transparent; font-size: 13px; } .multi-menu .item > .body a:hover { border-left: 2px solid #2F72AB; } .multi-menu .item > .body a.active { border-left: 2px solid #2F72AB; }
# 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 有必要看看具体是怎么进行渲染的。
{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路飞学城</title> <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} "> <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'css/menu.css' %}"> <style> body { margin: 0; } .no-radius { border-radius: 0; } .no-margin { margin: 0; } .pg-body > .left-menu { background-color: #EAEDF1; position: absolute; left: 1px; top: 48px; bottom: 0; width: 220px; border: 1px solid #EAEDF1; overflow: auto; } .pg-body > .right-body { position: absolute; left: 225px; right: 0; top: 48px; bottom: 0; overflow: scroll; border: 1px solid #ddd; border-top: 0; font-size: 13px; min-width: 755px; } .navbar-right { float: right !important; margin-right: -15px; } .luffy-container { padding: 15px; } .left-menu .menu-body .static-menu { } </style> </head> <body> <div class="pg-header"> <div class="nav"> <div class="logo-area left"> <a href="#"> <img class="logo" src="{% static 'imgs/logo.svg' %}"> <span style="font-size: 18px;">路飞学城 </span> </a> </div> <div class="left-menu left"> <a class="menu-item">资产管理</a> <a class="menu-item">用户信息</a> <a class="menu-item">路飞管理</a> <div class="menu-item"> <span>使用说明</span> <i class="fa fa-caret-down" aria-hidden="true"></i> <div class="more-info"> <a href="#" class="more-item">管他什么菜单</a> <a href="#" class="more-item">实在是编不了</a> </div> </div> </div> <div class="right-menu right clearfix"> <div class="user-info right"> <a href="#" class="avatar"> <img class="img-circle" src="{% static 'imgs/default.png' %}"> </a> <div class="more-info"> <a href="#" class="more-item">个人信息</a> <a href="#" class="more-item">注销</a> </div> </div> <a class="user-menu right"> 消息 <i class="fa fa-commenting-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 通知 <i class="fa fa-envelope-o" aria-hidden="true"></i> <span class="badge bg-success">2</span> </a> <a class="user-menu right"> 任务 <i class="fa fa-bell-o" aria-hidden="true"></i> <span class="badge bg-danger">4</span> </a> </div> </div> </div> <div class="pg-body"> <div class="left-menu"> <div class="menu-body"> {% load rbac %} {% get_menu request %} </div> </div> <div class="right-body"> <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> {% block content %} {% endblock %} </div> </div> <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="/static/js/menu.js"></script> {% block js %} {% endblock %} </body> </html>
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <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> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>年龄</th> <th>邮箱</th> <th>公司</th> {% if "customer_edit"|has_permission:request %} <th>编辑</th> {% endif %} {% if "customer_del"|has_permission:request %} <th>删除</th> {% endif %} </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.name }}</td> <td>{{ row.age }}</td> <td>{{ row.email }}</td> <td>{{ row.company }}</td> {% if "customer_edit"|has_permission:request %} <td><a style="color: #333333;" href="/customer/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a></td> {% endif %} {% if "customer_del"|has_permission:request %} <td><a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a> </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <div style="margin: 5px 0;"> {% load rbac %} {% if "payment_add"|has_permission:request %} <a class="btn btn-success" href="/payment/add/"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加缴费记录 </a> {% endif %} </div> <table class="table table-bordered table-hover"> <thead> <tr> <th>ID</th> <th>客户姓名</th> <th>金额</th> <th>付费时间</th> {% if "payment_edit"|has_permission:request %} <th>编辑</th> {% endif %} {% if "payment_del"|has_permission:request %} <th>删除</th> {% endif %} </tr> </thead> <tbody> {% for row in data_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.customer.name }}</td> <td>{{ row.money }}</td> <td>{{ row.create_time|date:"Y-m-d H:i:s" }}</td> {% if "payment_edit"|has_permission:request %} <td> <a style="color: #333333;" href="/payment/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a> </td> {% endif %} {% if "payment_del"|has_permission:request %} <td><a style="color: #d9534f;" href="/payment/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a></td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <form class="form-horizontal clearfix" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group col-sm-6 clearfix"> <label class="col-sm-3 control-label">{{ field.label }}</label> <div class="col-sm-9"> {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group col-sm-12"> <div class="col-sm-6"> <div class="col-sm-offset-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </div> </div> </form> </div> {% endblock %}
# models.py customer表和 payment表
from django.db import models class Customer(models.Model): """ 客户表 """ name = models.CharField(verbose_name='姓名', max_length=32) age = models.CharField(verbose_name='年龄', max_length=32) email = models.EmailField(verbose_name='邮箱', max_length=32) company = models.CharField(verbose_name='公司', max_length=32) def __str__(self): return self.name class Payment(models.Model): """ 付费记录 """ customer = models.ForeignKey(verbose_name='关联客户', to='Customer',on_delete=models.CASCADE) money = models.IntegerField(verbose_name='付费金额') create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
总结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>