• python 全栈开发,Day109(客户管理之动态"二级"菜单)


    昨日内容回顾

    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 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
    View Code

    使用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)
    View Code

    登录到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)
    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:
        # 获取菜单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)
    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:
        # 获取菜单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)
    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 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
    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 {
         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;
                 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- 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 {
                 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;
                 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- 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 {
                 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 OrderedDict
    
    ordered_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 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}  # 变量传给模板
    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 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}  # 变量传给模板
    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 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}  # 变量传给模板
    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 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
    View Code

    执行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)
    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 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
    View Code

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

    修改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('无权访问')
    View Code

    这样的做的目的,就是为了得到当前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}  # 变量传给模板
    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 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('无权访问')
    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;
                 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- 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 {
                 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取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']},
        ])
    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 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
    View Code

    修改 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('无权访问')
    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;
                 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- 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 {
                 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 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}
    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;
                 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- 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 {
                 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 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'),
    ]
    View Code

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

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

    权限表结构

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

    使用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)
    View Code

    登录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
    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 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('无权访问')
    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 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
    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 用来做反向生成
    有的公司叫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后面的条件
    View Code

    总结

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

    完整代码,参考github

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

    作业

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

  • 相关阅读:
    java获取指定日期的前一天和后一天
    Swagger注解-@ApiModel 和 @ApiModelProperty
    Timer中schedule()的用法
    Spring Boot 增加jar包
    报Result Maps collection does not contain value for java.lang.Long错误解决方法
    服务注册组件eureka
    浅谈redission以及Redis分布式锁探索入门
    用Redis实现分布式锁以及redission使用
    JVM逃逸分析对性能的影响
    【HTML打印】HTML直接调用window下的打印机并执行打印任务(简单打印任务生成)
  • 原文地址:https://www.cnblogs.com/xiao987334176/p/9519208.html
Copyright © 2020-2023  润新知