二级菜单权限
该内容的介绍是基于 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/')
View Code
视图(缴费的增删改查)
#!/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/')
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("无权访问该网页!")
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
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;
}
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
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>
layout.html
login.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 %}
customer_list.html
{% 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 %}
customer_add.html
{% 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 %}
customer_edit
{% 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 %}
payment_list.html
{% 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 %}
payment_add.html
{% 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 %}
payment_edit.html
# 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)
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>