昨日内容回顾

1. 权限有几张表?2. 简述权限流程?3. 为什么要把权限放入session?4. 静态文件和模块文件5. 相关技术点- orm查询- 去空- 去重 - 中间件 - inclusion_tag - 引入静态文件{% load staticfiles %}{% static '....' %}

View Code

一、客户管理之动态"二级"菜单

下载github代码

https://github.com/987334176/luffy_permission/archive/v1.4.zip

对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。

增加菜单表

修改rbac目录下的models.py,增加菜单表

from django.db import modelsclass 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.titleclass 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.titleclass 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.titleclass 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

View Code

使用2个命令,生成表

python manage.py makemigrations
python manage.py migrate

修改rbac目录下的admin.py,注册菜单表

from django.contrib import admin
from rbac import modelsadmin.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)

View Code

登录到admin后台,v1.4.zip的用户名为xiao,密码为xiao1234

录入菜单数据

点击权限表,设置2个url为菜单

那么菜单结构应该是这个样子的

信息管理账单管理
客户管理客户列表

当权限表中的menu_id字段为空时,它不是菜单。否则就是二级菜单!

在菜单表的数据,都是一级菜单!

修改权限初始化

编辑rbac-->service-->init_permission.py

from django.conf import settingsdef init_permission(request, user):"""权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session:param request::param user::return:"""# 3. 获取用户信息和权限信息写入sessionpermission_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)

View Code

使用有权限的用户登录

这里不会跳转到后台页面,不要紧。看一下Pycharmk控制台输出:

{'permissions__menu__title': '客户管理', 'permissions__menu__icon': 'fa-clipboard', 'permissions__url': '/customer/list/', 'permissions__menu_id': 2, 'permissions__title': '客户列表'}
...

View Code

菜单结构

第一次构建

我们要的菜单结构,应该是这个样子的

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:# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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)

View Code

执行输出:

{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:# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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)

View Code

执行输出:

{1: {'title': '信息管理', 'children': [{'url': '/customer/list/', 'title': '客户列表'}, {'url': '/payment/list/', 'title': '账单列表'}], 'icon': 'fa-coffee'}}

修改权限初始化

编辑rbac-->service-->init_permission.py,将上面的构造字典的代码copy过来

from django.conf import settingsdef init_permission(request, user):"""权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session:param request::param user::return:"""# 3. 获取用户信息和权限信息写入sessionpermission_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']})# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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_listrequest.session[settings.MENU_SESSION_KEY] = menu_dict

View Code

因为权限结构没有变化,所以中间件不需要改动代码

修改自定义标签

因为菜单结构发生了变化,所以修改标签

修改 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}  # 变量传给模板

View Code

修改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>

View Code

重新登录

效果如下:

美化菜单

修改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>

View Code

修改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;
}

View Code

修改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>

View Code

重启django,刷新页面,效果如下:

二、点击菜单,展开二级菜单效果

上面的效果是把所有二级菜单展开了,但是如果菜单过多,用户需要拖动滚动条,体验不好!

next()

next() 获得匹配元素集合中每个元素紧邻的同胞元素。如果提供选择器,则取回匹配该选择器的下一个同胞元素。

toggleClass()

toggleClass()对设置或移除被选元素的一个或多个类进行切换。

该方法检查每个元素中指定的类。如果不存在则添加类,如果已设置则删除之。这就是所谓的切换效果

进入目录rbac-->static-->rbac,创建文件rbac.js

(function (jq) {jq('.multi-menu .title').click(function () {$(this).next().toggleClass('hide');});
})(jQuery);

View Code

修改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>

View Code

刷新页面,效果如下:

三、点击菜单,让其他菜单隐藏

上面菜单有一个问题,如果需要隐藏二级菜单时,需要手动点击一次,才会隐藏。

如果菜单过多,而且那个人需要每一个都点击了一遍。然后想隐藏其他一级菜单时,就比较累了!

那么能不能随便点击一个一级菜单时,只展开当前的二级菜单。其他所有一级菜单,一律隐藏!

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);

View Code

刷新页面,效果如下:

四、菜单字典有序

注意:这里使用的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))

View Code

执行输出:

[2, 3, 4]

它是以key来排序的,默认是升序。还可以做降序

dic = {3:'xxx',2:'xxx',4:'xxx',
}print(sorted(dic,reverse=True))

View Code

执行输出

[4, 3, 2]

它能对字典的key做排序,但是它不能返回一个有序字典!

OrderdDict

Python中的字典对象可以以"键:值"的方式存取数据。OrderedDict是它的一个子类,实现了对字典对象中元素的排序。

使用时,需要导入模块

from collections import OrderedDict

修改 字典排序.py

from collections import OrderedDictordered_dict = OrderedDict()dic = {3:'xxx',2:'xxx',4:'xxx',
}for key in sorted(dic):ordered_dict[key] = dic[key]print(ordered_dict)

View Code

执行输出:

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 OrderedDictregister = 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}  # 变量传给模板

View Code

刷新页面,效果如下:

五、客户管理之默认展开非菜单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 OrderedDictregister = 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}  # 变量传给模板

View Code

修改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>

View Code

刷新网页,效果如下:

六、点击二级菜单,让其他一级菜单隐藏

有些人,需要点击二级菜单时,让其他所有的一级菜单,全部隐藏!

在菜单特别多的情况下,比较有用!

修改rbac-->templatetags-->rbac.py,增加一个父级class

from django.template import Library
from django.conf import settings
import re
from collections import OrderedDictregister = 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}  # 变量传给模板

View Code

查看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>

View Code

刷新页面,效果如下:

上面功能,能不能让前端写?
不能。因为让前端做,功能不完善。

七、权限系统流程图

说明:

1. 用户第一个访问登录页面时,中间件中有一个白名单。允许通过,渲染登录页面!

用户输入用户名和密码提交,后台查询数据库,进行认证。

2.  认证通过后,在session中,生成菜单和权限字典。返回给用户,做重定向!

3.  浏览器访问客户列表,中间件对url做权限校验。根据当前url和session的权限字典进行匹配!

匹配不成功,提示用户没有权限。匹配成功后,进入视图,渲染模板!

4. 模板渲染时,执行inclusion_tag。拿到菜单信息,根据当前url,展开二级菜单。返回给用户!

模块功能

中间部分,做白名单和请求校验

视图部分,初始化session

模板部分,动态生成菜单

每次请求,都会动态生成菜单!

八、点击非菜单链接,展示所属二级菜单

上面的效果看起来挺好,但是有一个问题。当我点击一个非菜单链接时,比如添加缴费记录时,它是下面的效果

它并没有展示出二级菜单。那么用户就不知道,他是从哪个菜单点击进来的!

目前很多的后台网页,都存在问题的。怎么解决呢?

菜单结构

这个时候,菜单结构应该是这个样子

信息管理账单列表(可做菜单的权限)添加账单删除账单编辑账单
客户管理客户列表(可做菜单的权限)

需要二级菜单下的链接,做一个父级id。表示这个链接,属于哪个菜单!

这个关系,可以让运营人员管理!

表结构

修改rbac-->models.py,增加parent字段。

它做了自关联。就是自己关联自己,它的值,必须表的主键id。主要是为了表示父级关系!

from django.db import modelsclass 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.titleclass 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.titleclass 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.titleclass 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

View Code

执行2个命令,生成字段

python manage.py makemigrations
python manage.py migrate

录入数据

修改rbac-->admin.py

from django.contrib import admin
from rbac import modelsadmin.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)

View Code

重启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 settingsdef init_permission(request, user):"""权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session:param request::param user::return:"""# 3. 获取用户信息和权限信息写入sessionpermission_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'],})# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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_listrequest.session[settings.MENU_SESSION_KEY] = menu_dict

View Code

因为权限列表结构变动了,得需要修改中间件

修改rbac-->middleware-->rbac.py

from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from django.shortcuts import redirect,HttpResponse
import reclass RbacMiddleware(MiddlewareMixin):"""权限控制的中间件"""def process_request(self, request):"""权限控制:param request::return:"""# 1. 获取当前请求URLcurrent_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 = Falsefor item in permission_list:id = item.get('id')  # url的idpid = item.get('pid')  # url的pid# 获取urlreg = "^%s$" % item.get('url')if re.match(reg, current_url):flag = Trueif pid:  # 如果是有pid的url,比如添加客户# 当前菜单id取pidrequest.current_menu_id = pidelse:# 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级request.current_menu_id = idbreakif not flag:return HttpResponse('无权访问')

View Code

这样的做的目的,就是为了得到当前url的父id

修改rbac-->templatetags-->rbac.py

from django.template import Library
from django.conf import settings
import re
from collections import OrderedDictregister = 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}  # 变量传给模板

View Code

这个时候,菜单结构如下:

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 reclass RbacMiddleware(MiddlewareMixin):"""权限控制的中间件"""def process_request(self, request):"""权限控制:param request::return:"""# 1. 获取当前请求URLcurrent_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 = Falsefor item in permission_list:id = item.get('id')  # url的idpid = item.get('pid')  # url的pid# 获取urlreg = "^%s$" % item.get('url')if re.match(reg, current_url):flag = Trueif pid:  # 如果是有pid的url,比如添加客户# 当前菜单id取pidrequest.current_menu_id = pidelse:# 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级request.current_menu_id = idbreakif not flag:return HttpResponse('无权访问')

View Code

修改 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>

View Code

刷新页面,就可以看到导航路径有3个

动态导航

首先确定的是,首页肯定是有的。其他的是动态的!

修改 rbac-->middleware-->rbac.py,修改导航列表,只保留首页。看下面一段代码

# 导航列表
request.breadcrumb_list = [{'title': '首页', 'url': '/'},]if pid:  # 如果是有pid的url,比如添加客户# 当前菜单id取pidrequest.current_menu_id = pid# 追加url菜单
    request.breadcrumb_list.extend([{'title': 'xx', 'url': '/'},{'title': item['title'], 'url': item['url']},])
else:# 否则就是二级菜单。因为一级菜单无法点击,这里只能是二级request.current_menu_id = idrequest.breadcrumb_list.extend([{'title': item['title'], 'url': item['url']},])

View Code

这里的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 settingsdef init_permission(request, user):"""权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session:param request::param user::return:"""# 3. 获取用户信息和权限信息写入sessionpermission_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'],}# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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_dictrequest.session[settings.MENU_SESSION_KEY] = menu_dict

View Code

修改 rbac-->middleware-->rbac.py

from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from django.shortcuts import redirect,HttpResponse
import reclass RbacMiddleware(MiddlewareMixin):"""权限控制的中间件"""def process_request(self, request):"""权限控制:param request::return:"""# 1. 获取当前请求URLcurrent_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 = Falsefor item in permission_dict.values():id = item.get('id')  # url的idpid = item.get('pid')  # url的pid# 获取urlreg = "^%s$" % item.get('url')if re.match(reg, current_url):flag = Trueif pid:  # 如果是有pid的url,比如添加客户# 当前菜单id取pidrequest.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 = idrequest.breadcrumb_list.extend([# 二级菜单{'title': item['title'], 'url': item['url']},])breakif not flag:return HttpResponse('无权访问')

View Code

注意:这一行代码

{'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>

View Code

退出,重新登录,效果如下:

那么问题来了,导航路径在layout里面。它是动态生成的,应该在inclusion_tag里面。

修改 rbac-->templatetags-->rbac.py,再定义一个标签

from django.template import Library
from django.conf import settings
import re
from collections import OrderedDictregister = 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}

View Code

进入目录 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>

View Code

修改 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>

View Code

重新登录,效果同上!

十、客户管理之 权限粒度控制按钮级别

不同用户登录系统时候,根据权限不同来控制是否限制指定按钮,如:

没有权限的用户

有权限的用户:

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 accounturlpatterns = [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'),
]

View Code

后续可以通过别名做判断,在不在个人权利列表里面。

这个别名,应该写在数据库里面。

权限表结构

修改 rbac-->models.py,权限表增加字段name,它是唯一的!

from django.db import modelsclass 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.titleclass 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.titleclass 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.titleclass 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

View Code

使用2个命令生成表字段

python manage.py makemigrations
python manage.py migrate

录入数据

修改 rbac-->admin.py

from django.contrib import admin
from rbac import modelsadmin.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)

View Code

登录admin后台,修改数据

获取别名

修改 rbac-->service-->init_permission.py,ORM增加相应字段,权限字段,增加pname

from django.conf import settingsdef init_permission(request, user):"""权限和菜单信息初始化,以后使用时,需要在登陆成功后调用该方法将权限和菜单信息放入session:param request::param user::return:"""# 3. 获取用户信息和权限信息写入sessionpermission_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别名为keypermission_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'],}# 获取菜单idmenu_id = row.get('permissions__menu_id')# 如果菜单id为空,跳过此次循环if not menu_id:continue# 判断菜单id不在字典里面时,避免一级菜单重复if menu_id not in menu_dict:# 以菜单id为keymenu_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_dictrequest.session[settings.MENU_SESSION_KEY] = menu_dict

View Code

修改 rbac-->middleware-->rbac.py,增加pname

from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from django.shortcuts import redirect,HttpResponse
import reclass RbacMiddleware(MiddlewareMixin):"""权限控制的中间件"""def process_request(self, request):"""权限控制:param request::return:"""# 1. 获取当前请求URLcurrent_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 = Falsefor item in permission_dict.values():id = item.get('id')  # url的idpid = item.get('pid')  # url的pidpname = item.get('pname')  # url的别名# 获取urlreg = "^%s$" % item.get('url')if re.match(reg, current_url):flag = Trueif pid:  # 如果是有pid的url,比如添加客户# 当前菜单id取pidrequest.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 = idrequest.breadcrumb_list.extend([# 二级菜单{'title': item['title'], 'url': item['url']},])breakif not flag:return HttpResponse('无权访问')

View Code

重新登录一次,效果同上!

权限字典结构

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 %}

View Code

但是这样不好,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 OrderedDictregister = 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

View Code

修改 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 %}

View Code

注意:自定义过滤器,最大只有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 %}

View Code

测试按钮是否显示

刷新页面,效果如下:

让一个有权限的用户登录

按钮还在

那么其他页面,也需要修改

修改 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 %}

View Code

进入admin后台,为秘书,添加权限

测试无权限的用户登录,效果如下:

回顾上面的一些内容

流程是不变的中间件-->白名单
权限初始化,数据库有6张表。
菜单,权限,角色,3个关系表
表里面有哪些字段
最重要的权限表
id,name,title,menu
name 用来做反向生成
有的公司叫codepid  作用:让添加客户端,默认展示相关的子菜单
meum_id:作用:因为要做二级菜单获取相关的权限信息。session放了2个东西。菜单和权限信息
它都是字典
permission_dict 以别名做为key
menu_dict 一级菜单id作为Key中间件,请求信息做校验
成功之后,pid对应的菜单,默认展开
还是一个就是导航条,自动生成
最重要的功能,权限验证
还有一个白名单requetst多了2个值,current_menu_id,breadcrumb_list
在模板里面做了一些事情,动态生成菜单,粒度控制在按钮级别
公共应用都是inclusion_tag和filter
只有filter作为if后面的条件

View Code

总结

1. 如何实现的权限系统?粒度控制到按钮级别的权限控制- 用户登陆成功之后,将权限和菜单信息放入session- 每次请求时,在中间件中做权限校验- inclusion_tag实现的动态菜单
2. 如何实现控制到按钮的呢?用户登陆时,用户所拥有的权限 别名==django 路由name 构造成一个字典;在页面中写了一个 django模板的filter来进行判断是否显示;3. 为什么要在中间件中做校验呢?所有请求在到达视图函数之前,必须经过中间件,所以在中间件中对请求做处理比较简单;4. 模板中的特殊方法:inclusion_tag、simpletag、filter5. 权限中使用了几张表?    六张,必须要说出来6. 表中的字段?(背表)7. 写流程(思维导读)8. 如何实现粒度到数据行?答:添加一条更细粒度的表,做条件用;9. 修改权限之后,如想应用最新权限- 我们:需要重新登陆。- 不用重新登陆,如何完成?更新涉及的所有用户的session信息10. 最重要 *****- 了解权限系统的流程和实现(一行一行过,根据表结构自己写)    不要抄- 权限组件的应用

View Code

完整代码,参考github

https://github.com/987334176/luffy_permission/archive/v1.5.zip

作业

了解权限系统的流程和实现(一行一行过,根据表结构自己写,不要抄代码)

转载于:https://www.cnblogs.com/xiao987334176/p/9519208.html

python 全栈开发,Day109(客户管理之动态二级菜单)相关推荐

  1. 收藏!最详细的Python全栈开发指南 看完这篇你还不会Python全栈开发 你来打我!!!

    Python Web全栈开发入门实战教程教程    大家好,我叫亓官劼(qí guān jié ),这个<Python Web全栈开发入门实战教程教程>是一个零基础的实战教程,手把手带你开 ...

  2. python利器手机版-将安卓手机打造成你的python全栈开发利器

    超神利器- 相信多数安卓用户都使用过Qpython这款移动端的Python编辑器吧?之前我也研究过一阵子这个工具,但因为一次简单的爬虫让我对它失望之极.Qpython不支持lxml这个模块,然而pyt ...

  3. termux pythonlxml安装_将安卓手机打造成你的python全栈开发利器

    原标题:将安卓手机打造成你的python全栈开发利器 超神利器 相信多数安卓用户都使用过Qpython这款移动端的Python编辑器吧?之前我也研究过一阵子这个工具,但因为一次简单的爬虫让我对它失望之 ...

  4. python全栈开发中级班全程笔记(第二模块、第四章)(常用模块导入)

    python全栈开发笔记第二模块 第四章 :常用模块(第二部分)     一.os 模块的 详解 1.os.getcwd()    :得到当前工作目录,即当前python解释器所在目录路径 impor ...

  5. 将安卓手机打造成你的python全栈开发利器

    超神利器 很多人学习python,不知道从何学起. 很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手. 很多已经做案例的人,却不知道如何去学习更加高深的知识. 那么针对这三类人,我 ...

  6. python全栈开发下载_python全栈开发神器 - 『精品软件区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn...

    将安卓手机打造成你的python全栈开发利器 超神利器- 相信多数安卓用户都使用过Qpython这款移动端的Python编辑器吧?之前我也研究过一阵子这个工具,但因为一次简单的爬虫让我对它失望之极.Q ...

  7. 路飞学城python全栈开发_[Python] 老男孩路飞学城Python全栈开发重点班 骑士计划最新100G...

    简介 老男孩&路飞学城Python全栈开发重点班课程,作为Python全栈教学系列的重头戏,投入了全新的课程研发和教学精力,也是Python骑士计划的核心教学,由ALEX老师开班镇守,一线技术 ...

  8. python 全栈开发,Day128(创建二维码,扫码,创建玩具的基本属性)

    python 全栈开发,Day128(创建二维码,扫码,创建玩具的基本属性) 昨日内容回顾 1.app播放音乐plus.audio.createPlayer(文件路径/URL)player.play( ...

  9. python 全栈开发,Day86(上传文件,上传头像,CBV,python读写Excel,虚拟环境virtualenv)

    python 全栈开发,Day86(上传文件,上传头像,CBV,python读写Excel,虚拟环境virtualenv) 一.上传文件 上传一个图片 使用input type="file& ...

  10. python 全栈开发,Day106(结算中心(详细),立即支付)

    python 全栈开发,Day106(结算中心(详细),立即支付) 昨日内容回顾 1. 为什么要开发路飞学城?提供在线教育的学成率:特色:学,看视频,单独录制增加趣味性.练,练习题改,改学生代码管,管 ...

最新文章

  1. 这7个开源技术,支撑起整个互联网时代
  2. [C++学习笔记]C++常见问题大全(二)
  3. 折叠玻璃体球囊手术介绍
  4. python随机生成字母和数字的混合字符串_用python生成数字、字母和特殊字符混合的字符串...
  5. 怎么查看和获取SQL Server实例名
  6. linux dns 问题吗,Linux下DNS的问题
  7. Android开发学习之ImageView手势拖拽、缩放、旋转
  8. 大数据集合求交集_还记得学生时代数学老师教的“集合”吗?
  9. 解决同一条sql在pl/sql工具中执行很快,在程序中却很慢
  10. HackerRank SQL练习题答案大全
  11. [Unity]CutScene工具Cinema Suite Rotion 角度不能负数方向旋转的bug修正。
  12. Mixly-数位计及1602屏亮度显示
  13. 如何通过修改注册表关闭、开启windows10内置Windows Defender安全中心
  14. html5 自动触发事件,HTML5视频触发事件触发一次
  15. 使用GDI/GDI+绘制到D3D9缓冲区的方法
  16. OpenJudgeNOI4978 宠物小精灵之收服
  17. 打开Windows系统某设置的方法有哪些?
  18. unordered_set使用介绍
  19. 西门子PID调节仿真程序
  20. hdu 50722014鞍山现场赛C题(容斥原理+同色三角形)

热门文章

  1. 如何在一场面试中展现你对Python的coding能力?
  2. 当R用户用ggplot2 package时,经常问的10个问题
  3. 一个文科小白的数据分析师之路
  4. Android 各API版本代码常量
  5. PHP大批量插入数据库的3种方法和速度对比
  6. 站立会议07(第二次冲刺)
  7. iOS开发之使用Runtime给Model类赋值
  8. F2工作流引擎之 概述(一)
  9. 68、secureCRT,vim中输入中文
  10. decorator 装饰