python 全栈开发,Day109(客户管理之动态"二级"菜单)
昨日内容回顾
1. 权限有几张表? 2. 简述权限流程? 3. 为什么要把权限放入session? 4. 静态文件和模块文件 5. 相关技术点 - orm查询 - 去空 - 去重 - 中间件 - inclusion_tag - 引入静态文件 {% load staticfiles %} {% static '....' %}
一、客户管理之动态"二级"菜单
下载github代码
https://github.com/987334176/luffy_permission/archive/v1.4.zip
对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。
增加菜单表
修改rbac目录下的models.py,增加菜单表
from django.db import models class Menu(models.Model): """ 菜单 """ title = models.CharField(verbose_name='菜单', max_length=32,unique=True) icon = models.CharField(verbose_name='图标', max_length=32) 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表示非菜单') 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
使用2个命令,生成表
python manage.py makemigrations
python manage.py migrate
修改rbac目录下的admin.py,注册菜单表
from django.contrib import admin from rbac import models admin.site.register(models.Menu) class PermissionAdmin(admin.ModelAdmin): list_display = ['title','url'] # 显示的字段 list_editable = ['url'] # 允许编辑 admin.site.register(models.Permission,PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.UserInfo)
登录到admin后台,v1.4.zip的用户名为xiao,密码为xiao1234
录入菜单数据
点击权限表,设置2个url为菜单
那么菜单结构应该是这个样子的
信息管理
账单管理
客户管理
客户列表
当权限表中的menu_id字段为空时,它不是菜单。否则就是二级菜单!
在菜单表的数据,都是一级菜单!
修改权限初始化
编辑rbac-->service-->init_permission.py
from django.conf import settings def init_permission(request, user): """ 权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session :param request: :param user: :return: """ # 3. 获取用户信息和权限信息写入session permission_queryset = user.roles.filter(permissions__url__isnull=False).values('permissions__url', 'permissions__title', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() for item in permission_queryset: print(item)
使用有权限的用户登录
这里不会跳转到后台页面,不要紧。看一下Pycharmk控制台输出:
{'permissions__menu__title': '客户管理', 'permissions__menu__icon': 'fa-clipboard', 'permissions__url': '/customer/list/', 'permissions__menu_id': 2, 'permissions__title': '客户列表'} ...
菜单结构
第一次构建
我们要的菜单结构,应该是这个样子的
menu_dict = { 1:{ title:'信息管理', icon:'fa-coffee', children:[ {title:'客户列表',url:'/customer/list/'}, {title:'客户列表',url:'/customer/list/'}, ] } }
注意:上面的1指的是一级菜单的id,也就是菜单表的主键id
children表示子菜单,也就是二级菜单!
新建一个文件 生成菜单结构.py。文件位置随意,它是一个临时文件而已
data = [ {'permissions__menu_id': 1, 'permissions__url': '/customer/list/', 'permissions__title': '客户列表', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-coffee'}, {'permissions__menu_id': None, 'permissions__url': '/customer/add/', 'permissions__title': '添加客户', 'permissions__menu__title': None, 'permissions__menu__icon': None}, {'permissions__menu_id': 1, 'permissions__url': '/payment/list/', 'permissions__title': '账单列表', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-coffee'}, ] menu_dict={} # 菜单字典 for row in data: # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title':row['permissions__menu__title'], # 一级菜单的图标 'icon':row['permissions__menu__icon'], # 二级菜单 'children':[ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'title':row['permissions__title'],'url':row['permissions__url']} ] } print(menu_dict)
执行输出:
{1: {'icon': 'fa-coffee', 'title': '信息管理', 'children': [{'title': '客户列表', 'url': '/customer/list/'}]}}
第二次构建
第二次构建时,如果一级菜单还有子菜单,就继续添加
data = [ {'permissions__menu_id': 1, 'permissions__url': '/customer/list/', 'permissions__title': '客户列表', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-coffee'}, {'permissions__menu_id': None, 'permissions__url': '/customer/add/', 'permissions__title': '添加客户', 'permissions__menu__title': None, 'permissions__menu__icon': None}, {'permissions__menu_id': 1, 'permissions__url': '/payment/list/', 'permissions__title': '账单列表', 'permissions__menu__title': '信息管理', 'permissions__menu__icon': 'fa-coffee'}, ] menu_dict={} # 菜单字典 for row in data: # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title':row['permissions__menu__title'], # 一级菜单的图标 'icon':row['permissions__menu__icon'], # 二级菜单 'children':[ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'title':row['permissions__title'],'url':row['permissions__url']} ] } else: # 如果一级菜单还有二级菜单,就继续添加 menu_dict[menu_id]['children'].append({'title': row['permissions__title'], 'url': row['permissions__url']}) print(menu_dict)
执行输出:
{1: {'title': '信息管理', 'children': [{'url': '/customer/list/', 'title': '客户列表'}, {'url': '/payment/list/', 'title': '账单列表'}], 'icon': 'fa-coffee'}}
修改权限初始化
编辑rbac-->service-->init_permission.py,将上面的构造字典的代码copy过来
from django.conf import settings def init_permission(request, user): """ 权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session :param request: :param user: :return: """ # 3. 获取用户信息和权限信息写入session permission_queryset = user.roles.filter(permissions__url__isnull=False).values('permissions__url', 'permissions__title', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() menu_dict = {} # 菜单字典,它是能成为菜单的权限,用于做菜单显示 permission_list = [] # 权限列表,所有权限,用于做权限校验 for row in permission_queryset: permission_list.append({'permissions__url': row['permissions__url']}) # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title': row['permissions__menu__title'], # 一级菜单的图标 'icon': row['permissions__menu__icon'], # 二级菜单 'children': [ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'title': row['permissions__title'], 'url': row['permissions__url']} ] } else: # 如果一级菜单还有二级菜单,就继续添加 menu_dict[menu_id]['children'].append({'title': row['permissions__title'], 'url': row['permissions__url']}) request.session[settings.PERMISSION_SESSION_KEY] = permission_list request.session[settings.MENU_SESSION_KEY] = menu_dict
因为权限结构没有变化,所以中间件不需要改动代码
修改自定义标签
因为菜单结构发生了变化,所以修改标签
修改 rbac-->templatetags-->rbac.py
from django.template import Library from django.conf import settings import re register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) return {'menu_dict':menu_dict} # 变量传给模板
修改rbac-->templates-->rbac-->menu.html
注意:这里使用了2层for循环。一层是一级菜单,一层是二级菜单
<div class="static-menu"> {% for item in menu_dict.values %} <div class="item"> <div class="header">{{ item.title }}</div> <div class="body"> {% for node in item.children %} <a href="{{ node.url }}">{{ node.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
重新登录
效果如下:
美化菜单
修改rbac-->templates-->rbac-->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 href="{{ per.url }}">{{ per.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
修改rbac-->static-->rbac-->rbac.css,增加二级菜单样式
.static-menu { } .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; }
修改web-->templates-->layout.html,引用rbac.css
{% load staticfiles %} {% load rbac %} <!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 'rbac/rbac.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 { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .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; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </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"> {% menu request %} </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> <li><a href="#">首页</a></li> <li class="active">客户管理</li> </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> {% block js %} {% endblock %} </body> </html>
重启django,刷新页面,效果如下:
二、点击菜单,展开二级菜单效果
上面的效果是把所有二级菜单展开了,但是如果菜单过多,用户需要拖动滚动条,体验不好!
next()
next() 获得匹配元素集合中每个元素紧邻的同胞元素。如果提供选择器,则取回匹配该选择器的下一个同胞元素。
toggleClass()
toggleClass()对设置或移除被选元素的一个或多个类进行切换。
该方法检查每个元素中指定的类。如果不存在则添加类,如果已设置则删除之。这就是所谓的切换效果
进入目录rbac-->static-->rbac,创建文件rbac.js
(function (jq) { jq('.multi-menu .title').click(function () { $(this).next().toggleClass('hide'); }); })(jQuery);
修改web-->templates-->layout.html,引用rbac.js
{% load staticfiles %} {% load rbac %} <!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 'rbac/rbac.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 { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .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; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </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"> {% menu request %} </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> <li><a href="#">首页</a></li> <li class="active">客户管理</li> </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 'rbac/rbac.js' %} "></script> {% block js %} {% endblock %} </body> </html>
刷新页面,效果如下:
三、点击菜单,让其他菜单隐藏
上面菜单有一个问题,如果需要隐藏二级菜单时,需要手动点击一次,才会隐藏。
如果菜单过多,而且那个人需要每一个都点击了一遍。然后想隐藏其他一级菜单时,就比较累了!
那么能不能随便点击一个一级菜单时,只展开当前的二级菜单。其他所有一级菜单,一律隐藏!
removeClass()
removeClass() 方法从被选元素移除一个或多个类。
注释:如果没有规定参数,则该方法将从被选元素中删除所有类。
parent()
parent() 获得当前匹配元素集合中每个元素的父元素,使用选择器进行筛选是可选的。
siblings()
siblings() 获得匹配集合中每个元素的同胞,通过选择器进行筛选是可选的。
find()
find() 方法获得当前元素集合中每个元素的后代,通过选择器、jQuery 对象或元素来筛选。
addClass()
addClass() 方法向被选元素添加一个或多个类。
该方法不会移除已存在的 class 属性,仅仅添加一个或多个 class 属性。
提示:如需添加多个类,请使用空格分隔类名。
修改rbac-->static-->rbac-->rbac.js
(function (jq) { jq('.multi-menu .title').click(function () { // $(this).next().toggleClass('hide'); $(this).next().removeClass('hide'); $(this).parent().siblings().find('.body').addClass('hide'); }); })(jQuery);
刷新页面,效果如下:
四、菜单字典有序
注意:这里使用的Python版本是3.5.4,字典是无序的。在Python3.6中,字典是有序的。它有默认的排序规则!
所以,即使是同一个用户登录,它每次登录时。展示的菜单时不一样的!影响用户体验!
sorted()
sorted() 函数对所有可迭代的对象进行排序操作
sort 与 sorted 区别
sort 是应用在 list 上的方法,sorted 可以对所有可迭代的对象进行排序操作。
list 的 sort 方法返回的是对已经存在的列表进行操作,而内建函数 sorted 方法返回的是一个新的 list,而不是在原来的基础上进行的操作。
举例:
新建一个文件 字典排序.py,存放位置随意,它是一个临时文件
dic = { 3:'xxx', 2:'xxx', 4:'xxx', } print(sorted(dic))
执行输出:
[2, 3, 4]
它是以key来排序的,默认是升序。还可以做降序
dic = { 3:'xxx', 2:'xxx', 4:'xxx', } print(sorted(dic,reverse=True))
执行输出
[4, 3, 2]
它能对字典的key做排序,但是它不能返回一个有序字典!
OrderdDict
Python中的字典对象可以以"键:值"的方式存取数据。OrderedDict是它的一个子类,实现了对字典对象中元素的排序。
使用时,需要导入模块
from collections import OrderedDict
修改 字典排序.py
from collections import OrderedDict ordered_dict = OrderedDict() dic = { 3:'xxx', 2:'xxx', 4:'xxx', } for key in sorted(dic): ordered_dict[key] = dic[key] print(ordered_dict)
执行输出:
OrderedDict([(2, 'xxx'), (3, 'xxx'), (4, 'xxx')])
它返回的是一个有序字典对象,那么只要对它做for循环,每次返回的顺序是一致的!
修改rbac-->templatetags-->rbac.py
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): ordered_dict[key] = menu_dict[key] return {'menu_dict':ordered_dict} # 变量传给模板
刷新页面,效果如下:
五、客户管理之默认展开非菜单URL
由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:
- 删除
- 修改
- ...
比如说:当我点击 信息管理-->账单列表时,让它默认选中
看到没有?账单列表左边,有一个蓝色的竖杠,它就是选中状态!
那么如何做到这个效果呢?
思路
通过当前url和二级菜单的url做匹配,如果匹配成功,就增加一个class为active
实现
修改rbac-->templatetags-->rbac.py
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): # 对字典的key做排序,并添加到有序字典对象中 ordered_dict[key] = menu_dict[key] # 循环二级菜单 for node in menu_dict[key]['children']: # 正则表达式,为url添加^和$ reg = "^%s$" %node['url'] # 如果当前url和二级菜单url匹配 if re.match(reg,request.path_info): # 增加一个class为action。这个是用来给前端展示的! node['class'] = 'active' return {'menu_dict':ordered_dict} # 变量传给模板
修改rbac-->templates-->rbac-->menu.html,子菜单中增加一个class
<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 href="{{ per.url }}" class="{{ per.class }}">{{ per.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
刷新网页,效果如下:
六、点击二级菜单,让其他一级菜单隐藏
有些人,需要点击二级菜单时,让其他所有的一级菜单,全部隐藏!
在菜单特别多的情况下,比较有用!
修改rbac-->templatetags-->rbac.py,增加一个父级class
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): # 对字典的key做排序,并添加到有序字典对象中 ordered_dict[key] = menu_dict[key] # 默认所有的一级菜单隐藏 menu_dict[key]['class'] = 'hide' # 循环二级菜单 for node in menu_dict[key]['children']: # 正则表达式,为url添加^和$ reg = "^%s$" %node['url'] # 如果当前url和二级菜单url匹配 if re.match(reg,request.path_info): # 增加一个class为action。这个是用来给前端展示的! node['class'] = 'active' # 点击二级菜单时,让当前所在的一级菜单展示 # 因为上面,把所有的一级菜单给隐藏了.这里设置为空,表示显示 menu_dict[key]['class'] = '' return {'menu_dict':ordered_dict} # 变量传给模板
查看rbac-->templates-->rbac-->menu.html,确保有这一行代码。它是一级菜单的class
<div class="body {{ item.class }}">
完整代码如下:
<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 href="{{ per.url }}" class="{{ per.class }}">{{ per.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
刷新页面,效果如下:
上面功能,能不能让前端写?
不能。因为让前端做,功能不完善。
七、权限系统流程图
说明:
1. 用户第一个访问登录页面时,中间件中有一个白名单。允许通过,渲染登录页面!
用户输入用户名和密码提交,后台查询数据库,进行认证。
2. 认证通过后,在session中,生成菜单和权限字典。返回给用户,做重定向!
3. 浏览器访问客户列表,中间件对url做权限校验。根据当前url和session的权限字典进行匹配!
匹配不成功,提示用户没有权限。匹配成功后,进入视图,渲染模板!
4. 模板渲染时,执行inclusion_tag。拿到菜单信息,根据当前url,展开二级菜单。返回给用户!
模块功能
中间部分,做白名单和请求校验
视图部分,初始化session
模板部分,动态生成菜单
每次请求,都会动态生成菜单!
八、点击非菜单链接,展示所属二级菜单
上面的效果看起来挺好,但是有一个问题。当我点击一个非菜单链接时,比如添加缴费记录时,它是下面的效果
它并没有展示出二级菜单。那么用户就不知道,他是从哪个菜单点击进来的!
目前很多的后台网页,都存在问题的。怎么解决呢?
菜单结构
这个时候,菜单结构应该是这个样子
信息管理
账单列表(可做菜单的权限)
添加账单
删除账单
编辑账单
客户管理
客户列表(可做菜单的权限)
需要二级菜单下的链接,做一个父级id。表示这个链接,属于哪个菜单!
这个关系,可以让运营人员管理!
表结构
修改rbac-->models.py,增加parent字段。
它做了自关联。就是自己关联自己,它的值,必须表的主键id。主要是为了表示父级关系!
from django.db import models class Menu(models.Model): """ 菜单 """ title = models.CharField(verbose_name='菜单', max_length=32,unique=True) icon = models.CharField(verbose_name='图标', max_length=32) 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) parent = models.ForeignKey(verbose_name='父权限', to='Permission', null=True, blank=True) menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单') 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
执行2个命令,生成字段
python manage.py makemigrations
python manage.py migrate
录入数据
修改rbac-->admin.py
from django.contrib import admin from rbac import models admin.site.register(models.Menu) class PermissionAdmin(admin.ModelAdmin): list_display = ['title','url','parent'] # 显示的字段 list_editable = ['url','parent'] # 允许编辑 admin.site.register(models.Permission,PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.UserInfo)
重启django项目,退出admin后台,重新登录。
点击权限表,进行相关设置!
注意:账单列表和客户列表是二级菜单,不能设置父权限!
父权限和菜单时二选一的,不能同时设置!
权限列表结构
permission_list = { {'id': 1, 'url': '/customer/list/', 'pid': None}, {'id': 2, 'url': '/customer/add/', 'pid': 1}, {'id': 3, 'url': '/customer/edit/', 'pid': 1}, }
注意:非菜单,才有pid。否则pid为None
代码实现
修改rbac-->service-->init_permission.py,增加id和pid
from django.conf import settings def init_permission(request, user): """ 权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session :param request: :param user: :return: """ # 3. 获取用户信息和权限信息写入session permission_queryset = user.roles.filter(permissions__url__isnull=False).values('permissions__id', 'permissions__url', 'permissions__title', 'permissions__parent_id', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() menu_dict = {} # 菜单字典,它是能成为菜单的权限,用于做菜单显示 permission_list = [] # 权限列表,所有权限,用于做权限校验 for row in permission_queryset: permission_list.append({ # 权限id 'id': row['permissions__id'], # url 'url': row['permissions__url'], # 权限父id 'pid': row['permissions__parent_id'], }) # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title': row['permissions__menu__title'], # 一级菜单的图标 'icon': row['permissions__menu__icon'], # 二级菜单 'children': [ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']} ] } else: # 如果一级菜单还有二级菜单,就继续添加 menu_dict[menu_id]['children'].append({'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']}) request.session[settings.PERMISSION_SESSION_KEY] = permission_list request.session[settings.MENU_SESSION_KEY] = menu_dict
因为权限列表结构变动了,得需要修改中间件
修改rbac-->middleware-->rbac.py
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import redirect,HttpResponse import re class RbacMiddleware(MiddlewareMixin): """ 权限控制的中间件 """ def process_request(self, request): """ 权限控制 :param request: :return: """ # 1. 获取当前请求URL current_url = request.path_info # print(current_url) # 1.5 白名单处理 for reg in settings.VALID_URL: if re.match(reg,current_url): return None # 2. 获取当前用户session中所有的权限 permission_list = request.session.get(settings.PERMISSION_SESSION_KEY) if not permission_list: return redirect('/login/') # 3. 进行权限校验 flag = False for item in permission_list: id = item.get('id') # url的id pid = item.get('pid') # url的pid # 获取url reg = "^%s$" % item.get('url') if re.match(reg, current_url): flag = True if pid: # 如果是有pid的url,比如添加客户 # 当前菜单id取pid request.current_menu_id = pid else: # 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级 request.current_menu_id = id break if not flag: return HttpResponse('无权访问')
这样的做的目的,就是为了得到当前url的父id
修改rbac-->templatetags-->rbac.py
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): # 对字典的key做排序,并添加到有序字典对象中 ordered_dict[key] = menu_dict[key] # 默认所有的一级菜单隐藏 menu_dict[key]['class'] = 'hide' # 循环二级菜单 for node in menu_dict[key]['children']: # 正则表达式,为url添加^和$ reg = "^%s$" %node['url'] # 判断当前url的菜单id等于二级菜单id # 因为权限表的url能成为菜单的都是二级菜单 if request.current_menu_id == node['id']: # 增加选中样式,给前端展示 node['class'] = 'active' # 点击二级菜单时,让当前所在的一级菜单展示 # 因为上面,把所有的一级菜单给隐藏了.这里设置为空,表示显示 menu_dict[key]['class'] = '' return {'menu_dict':ordered_dict} # 变量传给模板
这个时候,菜单结构如下:
1就是一级菜单的id
menu_dict = { 1:{ 'title':'信息管理', 'icon':'fa-coffee', 'class':'' 'children':{ {'id':1,'title':'客户列表','url':'/customer/list/','class':'active'} } } }
退出账号,重新登录。效果如下:
点击二级菜单时,页面会刷新。但是它会展开当时的一级菜单下的二级菜单!
原理
当用户登录之后,会生成当前用户的菜单字典和权限列表。
在中间件里面,根据当前url去查找current_menu_id(菜单id)。如果pid不为空,取id,否则取id。
并在request中增加属性current_menu_id
rbac-->templatetags-->rbac.py 这个是动态生成菜单的。
根据request.current_menu_id和菜单字典中children里面的id进行匹配,如果匹配,则添加class为active(展开),否则不展开!
九、客户管理之访问路径导航
路径导航
看一下Bootstrap官网,找到v3的文档-->组件-->路径导航
https://v3.bootcss.com/components/#breadcrumbs
看这里的路径导航,固定死了。应该动态变动才对!
要回退的时候,点击一下,就可以完成。
导航列表
固定导航列表(仅做调试)
修改 rbac-->middleware-->rbac.py,增加导航列表。这个可以放到session,看需求了!
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import redirect,HttpResponse import re class RbacMiddleware(MiddlewareMixin): """ 权限控制的中间件 """ def process_request(self, request): """ 权限控制 :param request: :return: """ # 1. 获取当前请求URL current_url = request.path_info # print(current_url) # 1.5 白名单处理 for reg in settings.VALID_URL: if re.match(reg,current_url): return None # 2. 获取当前用户session中所有的权限 permission_list = request.session.get(settings.PERMISSION_SESSION_KEY) if not permission_list: return redirect('/login/') # 3. 路径导航列表,首页是必须有的 request.breadcrumb_list = [ {'title': '首页', 'url': '/'}, {'title': '客户列表', 'url': '/customer/list/'}, {'title': '添加客户', 'url': '/customer/add/'}, ] # 4. 进行权限校验 flag = False for item in permission_list: id = item.get('id') # url的id pid = item.get('pid') # url的pid # 获取url reg = "^%s$" % item.get('url') if re.match(reg, current_url): flag = True if pid: # 如果是有pid的url,比如添加客户 # 当前菜单id取pid request.current_menu_id = pid else: # 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级 request.current_menu_id = id break if not flag: return HttpResponse('无权访问')
修改 web-->templates-->layout.html,for循环导航列表
{% load staticfiles %} {% load rbac %} <!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 'rbac/rbac.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 { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .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; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </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"> {% menu request %} </div> </div> <div class="right-body"> <div> <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {# <li><a href="#">首页</a></li>#} {# <li class="active">客户管理</li>#} {% for item in request.breadcrumb_list %} <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 'rbac/rbac.js' %} "></script> {% block js %} {% endblock %} </body> </html>
刷新页面,就可以看到导航路径有3个
动态导航
首先确定的是,首页肯定是有的。其他的是动态的!
修改 rbac-->middleware-->rbac.py,修改导航列表,只保留首页。看下面一段代码
# 导航列表 request.breadcrumb_list = [ {'title': '首页', 'url': '/'}, ] if pid: # 如果是有pid的url,比如添加客户 # 当前菜单id取pid request.current_menu_id = pid # 追加url菜单 request.breadcrumb_list.extend([ {'title': 'xx', 'url': '/'}, {'title': item['title'], 'url': item['url']}, ]) else: # 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级 request.current_menu_id = id request.breadcrumb_list.extend([ {'title': item['title'], 'url': item['url']}, ])
这里的xx应该是父级菜单的名称。但是添加父级标题有问题。因为权限列表,它是一个列表。由于它在for循环中,会产生很多的重复的数据。还有一个问题,得通过pid得到二级菜单!终上所述,权限列表,必须改造成字典
权限字典结构
permission_dict = { 1:{'id': 1, 'url': '/customer/list/', 'title':'客户列表','pid': None}, 2:{'id': 2, 'url': '/customer/add/', 'title':'添加客户','pid': 1}, 3:{'id': 3, 'url': '/customer/edit/', 'title':'编辑客户', 'pid': 1}, }
修改 rbac-->service-->init_permission.py
from django.conf import settings def init_permission(request, user): """ 权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session :param request: :param user: :return: """ # 3. 获取用户信息和权限信息写入session permission_queryset = user.roles.filter(permissions__url__isnull=False).values('permissions__id', 'permissions__url', 'permissions__title', 'permissions__parent_id', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() menu_dict = {} # 菜单字典,它是能成为菜单的权限,用于做菜单显示 permission_dict = {} # 权限列表,所有权限,用于做权限校验 for row in permission_queryset: permission_dict[row['permissions__id']] = { # 权限id 'id': row['permissions__id'], # url 'url': row['permissions__url'], 'title': row['permissions__title'], # 权限父id 'pid': row['permissions__parent_id'], } # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title': row['permissions__menu__title'], # 一级菜单的图标 'icon': row['permissions__menu__icon'], # 二级菜单 'children': [ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']} ] } else: # 如果一级菜单还有二级菜单,就继续添加 menu_dict[menu_id]['children'].append({'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']}) request.session[settings.PERMISSION_SESSION_KEY] = permission_dict request.session[settings.MENU_SESSION_KEY] = menu_dict
修改 rbac-->middleware-->rbac.py
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import redirect,HttpResponse import re class RbacMiddleware(MiddlewareMixin): """ 权限控制的中间件 """ def process_request(self, request): """ 权限控制 :param request: :return: """ # 1. 获取当前请求URL current_url = request.path_info # print(current_url) # 1.5 白名单处理 for reg in settings.VALID_URL: if re.match(reg,current_url): return None # 2. 获取当前用户session中所有的权限 permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) if not permission_dict: return redirect('/login/') # 3. 路径导航列表,首页是必须有的 request.breadcrumb_list = [ {'title': '首页', 'url': '/'}, ] # 4. 进行权限校验 flag = False for item in permission_dict.values(): id = item.get('id') # url的id pid = item.get('pid') # url的pid # 获取url reg = "^%s$" % item.get('url') if re.match(reg, current_url): flag = True if pid: # 如果是有pid的url,比如添加客户 # 当前菜单id取pid request.current_menu_id = pid # 追加url菜单 request.breadcrumb_list.extend([ # 二级菜单和二级菜单下的非菜单url {'title': permission_dict[str(pid)]['title'], 'url': permission_dict[str(pid)]['url']}, {'title': item['title'], 'url': item['url']}, ]) else: # 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级 request.current_menu_id = id request.breadcrumb_list.extend([ # 二级菜单 {'title': item['title'], 'url': item['url']}, ]) break if not flag: return HttpResponse('无权访问')
注意:这一行代码
{'title': permission_dict[str(pid)]['title'], 'url': permission_dict[str(pid)]['url']}
对int做json序列化之后,再反序列得到的类型是str。所以这里必须转换为str,否则报错
最后一个不能点
修改 web-->templates-->layout.html,for循环导航列表,如果是最后一个,去掉a标签
{% load staticfiles %} {% load rbac %} <!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 'rbac/rbac.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 { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .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; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </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"> {% 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_list %} {# 判断最后一个路径#} {% if forloop.last %} {#不让点击#} <li>{{ item.title }}</li> {% else %} <li><a href="{{ item.url }}">{{ item.title }}</a></li> {% endif %} {% 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 'rbac/rbac.js' %} "></script> {% block js %} {% endblock %} </body> </html>
退出,重新登录,效果如下:
那么问题来了,导航路径在layout里面。它是动态生成的,应该在inclusion_tag里面。
修改 rbac-->templatetags-->rbac.py,再定义一个标签
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): # 对字典的key做排序,并添加到有序字典对象中 ordered_dict[key] = menu_dict[key] # 默认所有的一级菜单隐藏 menu_dict[key]['class'] = 'hide' # 循环二级菜单 for node in menu_dict[key]['children']: # 正则表达式,为url添加^和$ reg = "^%s$" %node['url'] # 判断当前url的菜单id等于二级菜单id # 因为权限表的url能成为菜单的都是二级菜单 if request.current_menu_id == node['id']: # 增加选中样式,给前端展示 node['class'] = 'active' # 点击二级菜单时,让当前所在的一级菜单展示 # 因为上面,把所有的一级菜单给隐藏了.这里设置为空,表示显示 menu_dict[key]['class'] = '' return {'menu_dict':ordered_dict} # 变量传给模板 @register.inclusion_tag('rbac/breadcrumb.html') def breadcrumb(request): """ 路径导航 :param request: :return: """ return {'breadcrumb_list':request.breadcrumb_list}
进入目录 rbac-->templates-->rbac,创建文件breadcrumb.html
<ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;"> {% for item in breadcrumb_list %} {% if forloop.last %} <li class="active">{{ item.title }}</li> {% else %} <li><a href="{{ item.url }}">{{ item.title }}</a></li> {% endif %} {% endfor %} </ol>
修改 web-->templates-->layout.html,使用标签
{% load staticfiles %} {% load rbac %} <!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 'rbac/rbac.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 { } .left-menu .menu-body .static-menu .icon-wrap { width: 20px; display: inline-block; text-align: center; } .left-menu .menu-body .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; } .left-menu .menu-body .static-menu a:hover { color: #2F72AB; border-left: 2px solid #2F72AB; } .left-menu .menu-body .static-menu a.active { color: #2F72AB; border-left: 2px solid #2F72AB; } </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"> {% menu request %} </div> </div> <div class="right-body"> <div> {% breadcrumb request %} </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 'rbac/rbac.js' %} "></script> {% block js %} {% endblock %} </body> </html>
重新登录,效果同上!
十、客户管理之 权限粒度控制按钮级别
不同用户登录系统时候,根据权限不同来控制是否限制指定按钮,如:
没有权限的用户
有权限的用户:
url别名
要想做到粒度控制按钮级别,需要为每一个url定义一个别名
修改 web-->urls.py,增加别名
from django.conf.urls import url from web.views import customer from web.views import payment from web.views import account urlpatterns = [ url(r'^login/$', account.login), url(r'^customer/list/$', customer.customer_list, name='customer_list'), url(r'^customer/add/$', customer.customer_add, name='customer_add'), url(r'^customer/edit/(?P<cid>\d+)/$', customer.customer_edit, name='customer_edit'), url(r'^customer/del/(?P<cid>\d+)/$', customer.customer_del, name='customer_del'), url(r'^customer/import/$', customer.customer_import, name='customer_import'), url(r'^customer/tpl/$', customer.customer_tpl, name='customer_tpl'), url(r'^payment/list/$', payment.payment_list, name='payment_list'), url(r'^payment/add/$', payment.payment_add, name='payment_add'), url(r'^payment/edit/(?P<pid>\d+)/$', payment.payment_edit, name='payment_edit'), url(r'^payment/del/(?P<pid>\d+)/$', payment.payment_del, name='payment_del'), ]
后续可以通过别名做判断,在不在个人权利列表里面。
这个别名,应该写在数据库里面。
权限表结构
修改 rbac-->models.py,权限表增加字段name,它是唯一的!
from django.db import models class Menu(models.Model): """ 菜单 """ title = models.CharField(verbose_name='菜单', max_length=32,unique=True) icon = models.CharField(verbose_name='图标', max_length=32) 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) name = models.CharField(verbose_name='URL别名', max_length=32, null=True, blank=True,unique=True) parent = models.ForeignKey(verbose_name='父权限', to='Permission', null=True, blank=True) menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单') 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
使用2个命令生成表字段
python manage.py makemigrations
python manage.py migrate
录入数据
修改 rbac-->admin.py
from django.contrib import admin from rbac import models admin.site.register(models.Menu) class PermissionAdmin(admin.ModelAdmin): list_display = ['title','url','parent','name'] # 显示的字段 list_editable = ['url','parent','name'] # 允许编辑 admin.site.register(models.Permission,PermissionAdmin) admin.site.register(models.Role) admin.site.register(models.UserInfo)
登录admin后台,修改数据
获取别名
修改 rbac-->service-->init_permission.py,ORM增加相应字段,权限字段,增加pname
from django.conf import settings def init_permission(request, user): """ 权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session :param request: :param user: :return: """ # 3. 获取用户信息和权限信息写入session permission_queryset = user.roles.filter(permissions__url__isnull=False).values('permissions__id', 'permissions__url', 'permissions__title', 'permissions__name', 'permissions__parent_id', 'permissions__parent__name', 'permissions__menu_id', 'permissions__menu__title', 'permissions__menu__icon', ).distinct() menu_dict = {} # 菜单字典,它是能成为菜单的权限,用于做菜单显示 permission_dict = {} # 权限列表,所有权限,用于做权限校验 for row in permission_queryset: # 以url别名为key permission_dict[row['permissions__name']] = { # 权限id 'id': row['permissions__id'], # url 'url': row['permissions__url'], 'title': row['permissions__title'], # 权限父id 'pid': row['permissions__parent_id'], # 父id的name 'pname': row['permissions__parent__name'], } # 获取菜单id menu_id = row.get('permissions__menu_id') # 如果菜单id为空,跳过此次循环 if not menu_id: continue # 判断菜单id不在字典里面时,避免一级菜单重复 if menu_id not in menu_dict: # 以菜单id为key menu_dict[menu_id] = { # value部分就是title,用来展示一级菜单 'title': row['permissions__menu__title'], # 一级菜单的图标 'icon': row['permissions__menu__icon'], # 二级菜单 'children': [ # 二级菜单标题和url。注意:一级标题是不能点击的,所以它没有url # 二级菜单是可以点击的,但是它没有图标 {'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']} ] } else: # 如果一级菜单还有二级菜单,就继续添加 menu_dict[menu_id]['children'].append({'id':row['permissions__id'],'title': row['permissions__title'], 'url': row['permissions__url']}) request.session[settings.PERMISSION_SESSION_KEY] = permission_dict request.session[settings.MENU_SESSION_KEY] = menu_dict
修改 rbac-->middleware-->rbac.py,增加pname
from django.utils.deprecation import MiddlewareMixin from django.conf import settings from django.shortcuts import redirect,HttpResponse import re class RbacMiddleware(MiddlewareMixin): """ 权限控制的中间件 """ def process_request(self, request): """ 权限控制 :param request: :return: """ # 1. 获取当前请求URL current_url = request.path_info # print(current_url) # 1.5 白名单处理 for reg in settings.VALID_URL: if re.match(reg,current_url): return None # 2. 获取当前用户session中所有的权限 permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) if not permission_dict: return redirect('/login/') # 3. 路径导航列表,首页是必须有的 request.breadcrumb_list = [ {'title': '首页', 'url': '/'}, ] # 4. 进行权限校验 flag = False for item in permission_dict.values(): id = item.get('id') # url的id pid = item.get('pid') # url的pid pname = item.get('pname') # url的别名 # 获取url reg = "^%s$" % item.get('url') if re.match(reg, current_url): flag = True if pid: # 如果是有pid的url,比如添加客户 # 当前菜单id取pid request.current_menu_id = pid # 追加url菜单 request.breadcrumb_list.extend([ # 二级菜单和二级菜单下的非菜单url {'title': permission_dict[pname]['title'], 'url': permission_dict[pname]['url']}, {'title': item['title'], 'url': item['url']}, ]) else: # 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级 request.current_menu_id = id request.breadcrumb_list.extend([ # 二级菜单 {'title': item['title'], 'url': item['url']}, ]) break if not flag: return HttpResponse('无权访问')
重新登录一次,效果同上!
权限字典结构
key都是url别名
permission_dict = { 'customer_list':{'id': 1, 'url': '/customer/list/', 'title':'客户列表','pid': None}, 'customer_add':{'id': 2, 'url': '/customer/add/', 'title':'添加客户','pid': 1}, 'customer_edit':{'id': 3, 'url': '/customer/edit/', 'title':'编辑客户', 'pid': 1}, }
那么就可以通过别名判断
if 'customer_add' in permission_dict: print('有权限') else: print('无权限')
模板权限判断
修改 web-->templates-->customer_list.html,做if判断,通过url别名反向生成url
{% extends 'layout.html' %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% if 'customer_add' in request.session.permission_list %} <a class="btn btn-default" href="{% url 'customer_add' %}"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} {% if 'customer_import' in request.session.permission_list %} <a class="btn btn-default" href="{% url 'customer_import' %}"> <i class="fa fa-file-excel-o" 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> <th>选项</th> </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> <td> <a style="color: #333333;" href="/customer/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a> | <a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
但是这样不好,permission_list是放在settting.py里面的。
如果有人修改了settings.py配置里面的permission_list,那么前端页面也得更改!
自定义过滤器
通过自定义过滤器来获取settings.py里面的配置
修改 rbac-->templatetags-->rbac.py
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag('rbac/menu.html') def menu(request): """ 生成菜单 :param request: :return: """ # 获取session中的菜单列表 menu_dict = request.session.get(settings.MENU_SESSION_KEY) ordered_dict = OrderedDict() # 实例化 for key in sorted(menu_dict): # 对字典的key做排序,并添加到有序字典对象中 ordered_dict[key] = menu_dict[key] # 默认所有的一级菜单隐藏 menu_dict[key]['class'] = 'hide' # 循环二级菜单 for node in menu_dict[key]['children']: # 正则表达式,为url添加^和$ reg = "^%s$" %node['url'] # 判断当前url的菜单id等于二级菜单id # 因为权限表的url能成为菜单的都是二级菜单 if request.current_menu_id == node['id']: # 增加选中样式,给前端展示 node['class'] = 'active' # 点击二级菜单时,让当前所在的一级菜单展示 # 因为上面,把所有的一级菜单给隐藏了.这里设置为空,表示显示 menu_dict[key]['class'] = '' return {'menu_dict':ordered_dict} # 变量传给模板 @register.inclusion_tag('rbac/breadcrumb.html') def breadcrumb(request): """ 路径导航 :param request: :return: """ return {'breadcrumb_list':request.breadcrumb_list} @register.filter def has_permission(request,name): """ 权限判断 :param request: :param name: url别名 :return: 如果别名在权限字典里,返回True。否则返回None """ permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY) if name in permission_dict: return True
修改 web-->templates-->customer_list.html,使用自定义过滤器判断。要导入rbac!
注意:在模板里面,只有过滤器才可以做if判断
这就是,为什么要自定义过滤器的原因
{% extends 'layout.html' %} {% load rbac %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% if request|has_permission:"customer_add" %} <a class="btn btn-default" href="{% url 'customer_add' %}"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} {% if request|has_permission:"customer_import" %} <a class="btn btn-default" href="{% url 'customer_import' %}"> <i class="fa fa-file-excel-o" 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> <th>选项</th> </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> <td> <a style="color: #333333;" href="/customer/edit/{{ row.id }}/"> <i class="fa fa-edit" aria-hidden="true"></i></a> | <a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a> </td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
注意:自定义过滤器,最大只有2个参数
看下面的代码
{% if request|has_permission:"customer_add" %}
request是第一个参数,customer_add是二个参数!
使用无权限的用户登录
效果如下:
虽然上面的按钮没有了,但是表格的按钮,还存在。继续做if判断!
修改 web-->templates-->customer_list.html,判断表格
{% extends 'layout.html' %} {% load rbac %} {% block content %} <div class="luffy-container"> <div class="btn-group" style="margin: 5px 0"> {% if request|has_permission:"customer_add" %} <a class="btn btn-default" href="{% url 'customer_add' %}"> <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户 </a> {% endif %} {% if request|has_permission:"customer_import" %} <a class="btn btn-default" href="{% url 'customer_import' %}"> <i class="fa fa-file-excel-o" 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 request|has_permission:"customer_edit" or request|has_permission:"customer_del" %} <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 request|has_permission:"customer_edit" or request|has_permission:"customer_del" %} <td> {% if request|has_permission:"customer_edit" %} <a style="color: #333333;" href="{% url 'customer_edit' cid=row.id %}"> <i class="fa fa-edit" aria-hidden="true"></i></a> {% endif %} {% if request|has_permission:"customer_del" %} <a style="color: #d9534f;" href="{% url 'customer_del' cid=row.id %}"><i class="fa fa-trash-o"></i></a> {% endif %} </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
测试按钮是否显示
刷新页面,效果如下:
让一个有权限的用户登录
按钮还在
那么其他页面,也需要修改
修改 web-->templates-->payment_list.html
{% extends 'layout.html' %} {% load rbac %} {% block content %} <div class="luffy-container"> <div style="margin: 5px 0;"> {% if request|has_permission:"payment_add" %} <a class="btn btn-success" href="{% url '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 request|has_permission:"payment_edit" or request|has_permission:"payment_del" %} <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 request|has_permission:"payment_edit" or request|has_permission:"payment_del" %} <td> {% if request|has_permission:"payment_edit" %} <a style="color: #333333;" href="{% url 'payment_edit' cid=row.id %}"> <i class="fa fa-edit" aria-hidden="true"></i></a> {% endif %} {% if request|has_permission:"payment_del" %} <a style="color: #d9534f;" href="{% url 'payment_del' cid=row.id %}"><i class="fa fa-trash-o"></i></a> {% endif %} </td> {% endif %} </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
进入admin后台,为秘书,添加权限
测试无权限的用户登录,效果如下:
回顾上面的一些内容
流程是不变的 中间件-->白名单 权限初始化,数据库有6张表。 菜单,权限,角色,3个关系表 表里面有哪些字段 最重要的权限表 id,name,title,menu name 用来做反向生成 有的公司叫code pid 作用:让添加客户端,默认展示相关的子菜单 meum_id:作用:因为要做二级菜单 获取相关的权限信息。session放了2个东西。菜单和权限信息 它都是字典 permission_dict 以别名做为key menu_dict 一级菜单id作为Key 中间件,请求信息做校验 成功之后,pid对应的菜单,默认展开 还是一个就是导航条,自动生成 最重要的功能,权限验证 还有一个白名单 requetst多了2个值,current_menu_id,breadcrumb_list 在模板里面做了一些事情,动态生成菜单,粒度控制在按钮级别 公共应用都是inclusion_tag和filter 只有filter作为if后面的条件
总结
1. 如何实现的权限系统? 粒度控制到按钮级别的权限控制 - 用户登陆成功之后,将权限和菜单信息放入session - 每次请求时,在中间件中做权限校验 - inclusion_tag实现的动态菜单 2. 如何实现控制到按钮的呢? 用户登陆时,用户所拥有的权限 别名==django 路由name 构造成一个字典; 在页面中写了一个 django模板的filter来进行判断是否显示; 3. 为什么要在中间件中做校验呢? 所有请求在到达视图函数之前,必须经过中间件,所以在中间件中对请求做处理比较简单; 4. 模板中的特殊方法:inclusion_tag、simpletag、filter 5. 权限中使用了几张表? 六张,必须要说出来 6. 表中的字段?(背表) 7. 写流程(思维导读) 8. 如何实现粒度到数据行? 答:添加一条更细粒度的表,做条件用; 9. 修改权限之后,如想应用最新权限 - 我们:需要重新登陆。 - 不用重新登陆,如何完成?更新涉及的所有用户的session信息 10. 最重要 ***** - 了解权限系统的流程和实现(一行一行过,根据表结构自己写) 不要抄 - 权限组件的应用
完整代码,参考github
https://github.com/987334176/luffy_permission/archive/v1.5.zip
作业
了解权限系统的流程和实现(一行一行过,根据表结构自己写,不要抄代码)