2 动态菜单
1 一级菜单
用户登录后,我们不应该单单只让用户拿到权限信息。应该也可以拿到动态菜单
那么如何显示一级菜单?实现思路如下:
我们需要在数据库permission表中,添加一个字段,表示该权限是否可以成为菜单。
用户第一次发送请求时,我们不单单只把权限信息放入session,还要把用户权限中可以成为菜单的放入另一个session中。
用户第二次发送请求,经过中间件,读取权限信息并进行校验,成功返回页面。在页面中的菜单,需要在session中读取。
第一步:在数据表中添加相应字段
models.py
from django.db import models
class Permission(models.Model):
"""权限表"""
title = models.CharField(verbose_name="标题", max_length=32)
url = models.CharField(verbose_name="含正则的URL", max_length=128)
icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标')
is_menu = models.BooleanField(verbose_name='是否是菜单', default=False)
def __str__(self):
return self.title
class Role(models.Model):
"""角色表"""
title = models.CharField(verbose_name="角色名称", max_length=32)
permissions = models.ManyToManyField(verbose_name="拥有的所有权限", to="Permission", blank=True)
def __str__(self):
return self.title
class UserInfo(models.Model):
"""用户表"""
name = models.CharField(verbose_name="用户名", max_length=32)
password = models.CharField(verbose_name="密码", max_length=64)
email = models.CharField(verbose_name="邮箱", max_length=32)
roles = models.ManyToManyField(verbose_name="拥有的所有角色", to="Role", blank=True)
def __str__(self):
return self.name
PS:
fontawesome图标库:http://www.fontawesome.com.cn/faicons/
第一步:获取菜单信息并存储session
settings.py
PERMISSION_SESSION_KEY = "crm_permission_url_list"
MENU_SESSION_KEY = "crm_permission_menu_list"
VALID_URL_LIST = ["/login/", "/admin/.*", ]
account.py
from django.shortcuts import render, HttpResponse, redirect
from rbac.service.init_permission import init_permission
from rbac import models
def login(request):
if request.method == "GET":
return render(request, "login.html")
user = request.POST.get("user")
pwd = request.POST.get("pwd")
current_user = models.UserInfo.objects.filter(name=user, password=pwd).first()
if not current_user:
return render(request, "login.html", {"msg": "用户名密码错误"})
# 权限信息的初始化
init_permission(current_user, request)
return redirect("/customer/list/")
init_permission.py
from django.conf import settings
def init_permission(current_user, request):
"""
用户权限信息的初始化
:param current_user:当前用户对象
:param request:当前用户的请求数据
:return:
"""
# 根据当前用户信息获取此用户拥有的所有权限
permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
"permissions__id",
"permissions__title",
"permissions__is_menu",
"permissions__icon",
"permissions__url",).distinct()
# 获取权限+菜单信息,写入session
menu_list = []
permissions_list = []
for item in permission_queryset:
permissions_list.append(item["permissions__url"])
if item["permissions__is_menu"]:
temp = {
"title": item["permissions__title"],
"icon": item["permissions__icon"],
"url": item["permissions__url"],
}
menu_list.append(temp)
request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
request.session[settings.MENU_SESSION_KEY] = menu_list
第三步:模板中显示菜单信息(session)
layout.html
{% load rbac %}
<div class="static-menu">
{% static_menu request %}
</div>
rbac.py
from django.template import Library
from django.conf import settings
register = Library()
@register.inclusion_tag("rbac/static_menu.html")
def static_menu(request):
"""创建一级菜单"""
menu_list = request.session[settings.MENU_SESSION_KEY]
return {"menu_list": menu_list, "path": request.path_info}
static_menu.html
{% for item in menu_list %}
<a href="{{ item.url }}" class="{% if path == item.url %}active{% endif %}">
<span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}</a>
{% endfor %}
一级菜单示例代码下载
2 二级菜单
对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。
1 第一步:session中存储的菜单信息结构
{
1: {
"title": "信息管理",
"icon": "x1",
"children": [
{"title": "客户列表", "url": "/customer/list/"}
{"title": "账单列表", "url": "/account/list/"}
]
},
2: {
"title": "用户管理",
"icon": "x2",
"children": [
{"title": "个人资料", "url": "/userinfo/list/"}
]
},
}
2 第一步:数据表中的修改
models.py
from django.db import models
class Menu(models.Model):
title = models.CharField(verbose_name="一级菜单名称", max_length=32)
icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)
def __str__(self):
return self.title
class Permission(models.Model):
"""权限表"""
title = models.CharField(verbose_name="标题", max_length=32)
url = models.CharField(verbose_name="含正则的URL", max_length=128)
menu = models.ForeignKey(verbose_name="所属菜单", to="Menu", null=True, blank=True, help_text="null表示不是菜单,非null表示是二级菜单", on_delete=models.CASCADE)
def __str__(self):
return self.title
class Role(models.Model):
"""角色表"""
title = models.CharField(verbose_name="角色名称", max_length=32)
permissions = models.ManyToManyField(verbose_name="拥有的所有权限", to="Permission", blank=True)
def __str__(self):
return self.title
class UserInfo(models.Model):
"""用户表"""
name = models.CharField(verbose_name="用户名", max_length=32)
password = models.CharField(verbose_name="密码", max_length=64)
email = models.CharField(verbose_name="邮箱", max_length=32)
roles = models.ManyToManyField(verbose_name="拥有的所有角色", to="Role", blank=True)
def __str__(self):
return self.name
3 第三步:页面显示二级菜单
layout.html
{% load static %}
{% load rbac %}
<div class="static-menu">
{% multi_menu request %}
</div>
rbac.py
import re
from django.template import Library
from django.conf import settings
from collections import OrderedDict
register = Library()
@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
"""
创建二级菜单
:return:
"""
menu_dict = request.session[settings.MENU_SESSION_KEY]
# 对字典的key进行排序
key_list = sorted(menu_dict)
# 空的有序字典
ordered_dict = OrderedDict()
for key in key_list:
val = menu_dict[key]
val['class'] = 'hide'
for per in val['children']:
regex = "^%s$" % (per['url'],)
if re.match(regex, request.path_info):
per['class'] = 'active'
val['class'] = ''
ordered_dict[key] = val
return {'menu_dict': ordered_dict}
static_menu.html
<div class="multi-menu">
{% for item in menu_dict.values %}
<div class="item">
<div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
<div class="body {{ item.class }}">
{% for per in item.children %}
<a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
二级菜单示例代码下载
3 默认展开非菜单URL
由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:
- 删除
- 修改
...
点击非菜单的权限时,默认选中或默认展开。
当点击某个不能成为菜单的权限时,指定一个可以成为菜单的权限,让其默认选中及展开
第一步:修改models表结构
models.py
from django.db import models
class Menu(models.Model):
"""
菜单表
"""
title = models.CharField(verbose_name='一级菜单名称', max_length=32)
icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True)
def __str__(self):
return self.title
class Permission(models.Model):
"""
权限表
"""
title = models.CharField(verbose_name='标题', max_length=32)
url = models.CharField(verbose_name='含正则的URL', max_length=128)
menu = models.ForeignKey(verbose_name='所属菜单', to='Menu', null=True, blank=True, help_text='null表示不是菜单;非null表示是二级菜单',
on_delete=models.CASCADE)
pid = models.ForeignKey(verbose_name="关联的权限", to="Permission", null=True, blank=True, related_name="parents",
help_text="对于非菜单权限需要选择一个可以成为菜单的权限,用于做默认展开和选中菜单",
on_delete=models.CASCADE)
def __str__(self):
return self.title
class Role(models.Model):
"""
角色
"""
title = models.CharField(verbose_name='角色名称', max_length=32)
permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)
def __str__(self):
return self.title
class UserInfo(models.Model):
"""
用户表
"""
name = models.CharField(verbose_name='用户名', max_length=32)
password = models.CharField(verbose_name='密码', max_length=64)
email = models.CharField(verbose_name='邮箱', max_length=32)
roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)
def __str__(self):
return self.name
思路梳理:
登录,做权限和菜单的初始化:
- 获取菜单信息
{
1: {
"title": "信息管理",
"icon": "x1",
"children": [
{"id": 1, "title": "客户列表", "url": "/customer/list/"} # 可以做菜单权限的id
]
},
2: {
"title": "用户管理",
"icon": "x2",
"children": [
{"id": 7, "title": "账单列表", "url": "/account/list/"} # 可以做菜单权限的id
]
},
}
- 获取权限信息
[
{"id": 1, "url":"/customer/lsit/", "pid": null}, # 可以做菜单
{"id": 2, "url":"/customer/add/", "pid": 1}, # 不可以做菜单
]
再次来访问
- 中间件进行权限的校验(根据权限信息)
获取ID或PID (应该被选中的可以做菜单的权限ID)
模板中使用inclusion_tag生成动态菜单(根据菜单信息进行动态生成)
第二步:登录,做权限和菜单的初始化
init_permission.py
from django.conf import settings
def init_permission(current_user, request):
"""
用户权限信息的初始化
:param current_user:当前用户对象
:param request:当前用户的请求数据
:return:
"""
# 根据当前用户信息获取此用户拥有的所有权限
permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
"permissions__id",
"permissions__title",
"permissions__url",
"permissions__pid_id",
"permissions__menu__id",
"permissions__menu__title",
"permissions__menu__icon", ).distinct()
# 获取权限+菜单信息,写入session
menu_dict = {}
permissions_list = []
for item in permission_queryset:
permissions_list.append(
{"id": item["permissions__id"], "url": item["permissions__url"], "pid": item['permissions__pid_id']})
menu_id = item["permissions__menu__id"]
if not menu_id:
continue
node = {"id": item["permissions__id"], "title": item["permissions__title"], "url": item["permissions__url"]}
if menu_id in menu_dict:
menu_dict[menu_id]["children"].append(node)
else:
menu_dict[menu_id] = {
"title": item["permissions__menu__title"],
"icon": item["permissions__menu__icon"],
"children": [node, ]
}
request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
request.session[settings.MENU_SESSION_KEY] = menu_dict
第三步:中间件进行权限的校验,模板中使用inclusion_tag生成动态菜单
middlewares/rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
"""
用户权限信息校验
"""
def process_request(self, request):
"""
当用户请求刚进入时候出发执行
:param request:
:return:
"""
"""
1. 获取当前用户请求的URL
2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
3. 权限信息匹配
"""
current_url = request.path_info
for valid_url in settings.VALID_URL_LIST:
if re.match(valid_url, current_url):
# 白名单中的URL无需权限验证即可访问
return None
permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
if not permission_list:
return HttpResponse('未获取到用户权限信息,请登录!')
flag = False
for item in permission_list:
reg = "^%s$" % item["url"]
if re.match(reg, current_url):
flag = True
request.current_selected_permission = item["pid"] or item["id"]
break
if not flag:
return HttpResponse('无权访问')
templatetags/rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.template import Library
from django.conf import settings
from collections import OrderedDict
register = Library()
@register.inclusion_tag('rbac/static_menu.html')
def static_menu(request):
"""
创建一级菜单
:return:
"""
menu_list = request.session[settings.MENU_SESSION_KEY]
return {'menu_list': menu_list, "path": request.path_info}
@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
"""
创建二级菜单
:return:
"""
menu_dict = request.session[settings.MENU_SESSION_KEY]
# 对字典的key进行排序
key_list = sorted(menu_dict)
# 空的有序字典
ordered_dict = OrderedDict()
for key in key_list:
val = menu_dict[key]
val['class'] = 'hide'
for per in val['children']:
if per["id"] == request.current_selected_permission:
per['class'] = 'active'
val['class'] = ''
ordered_dict[key] = val
return {'menu_dict': ordered_dict}
layout.html
{% load static %}
{% load rbac %}
<div class="static-menu">
{% multi_menu request %}
</div>
非菜单默认选中代码下载
4 动态菜单之路径导航
init_permission.py
from django.conf import settings
def init_permission(current_user, request):
"""
用户权限信息的初始化
:param current_user:当前用户对象
:param request:当前用户的请求数据
:return:
"""
# 根据当前用户信息获取此用户拥有的所有权限
permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
"permissions__id",
"permissions__title",
"permissions__url",
"permissions__pid__id",
"permissions__pid__title",
"permissions__pid__url",
"permissions__menu__id",
"permissions__menu__title",
"permissions__menu__icon", ).distinct()
# 获取权限+菜单信息,写入session
menu_dict = {}
permissions_list = []
for item in permission_queryset:
permissions_list.append(
{
"id": item["permissions__id"],
"title": item["permissions__title"],
"url": item["permissions__url"],
"pid": item['permissions__pid__id'],
"p_title": item["permissions__pid__title"],
"p_url": item["permissions__pid__url"]
})
menu_id = item["permissions__menu__id"]
if not menu_id:
continue
node = {"id": item["permissions__id"], "title": item["permissions__title"], "url": item["permissions__url"]}
if menu_id in menu_dict:
menu_dict[menu_id]["children"].append(node)
else:
menu_dict[menu_id] = {
"title": item["permissions__menu__title"],
"icon": item["permissions__menu__icon"],
"children": [node, ]
}
request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
request.session[settings.MENU_SESSION_KEY] = menu_dict
rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
"""
用户权限信息校验
"""
def process_request(self, request):
"""
当用户请求刚进入时候出发执行
:param request:
:return:
"""
"""
1. 获取当前用户请求的URL
2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
3. 权限信息匹配
"""
current_url = request.path_info
for valid_url in settings.VALID_URL_LIST:
if re.match(valid_url, current_url):
# 白名单中的URL无需权限验证即可访问
return None
permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
if not permission_list:
return HttpResponse('未获取到用户权限信息,请登录!')
flag = False
url_record = [
{"title": "首页", "url": "#"}
]
for item in permission_list:
reg = "^%s$" % item["url"]
if re.match(reg, current_url):
flag = True
request.current_selected_permission = item["pid"] or item["id"]
if not item["pid"]:
url_record.extend([{"title": item["title"], "url": item["url"]}])
else:
url_record.extend([
{"title": item["p_title"], "url": item["p_url"]},
{"title": item["title"], "url": item["url"]},
])
request.url_record = url_record
break
if not flag:
return HttpResponse('无权访问')
layout.html
<div class="right-body">
<div>
<ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
{% for item in request.url_record %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ol>
</div>
{% block content %} {% endblock %}
</div>
路径导航代码下载