二级菜单权限

该内容的介绍是基于 lufficypermission 项目来完成的。

介绍及需求

1、什么是二级菜单权限?

  一级菜单权限:是指将当前登录用户所具有的权限,可以放到左侧菜单栏处的权限

  二级菜单权限:当一级菜单权限非常多的时候,可以对其进行归类,例如客户列表属于信息管理,缴费列表属于财务管理,如下图:

         

android 二级菜单框架 二级菜单栏_django

2、需求

  • 左侧菜单栏权限有分级显示,需要有地方存储一级菜单的内容,Menu表
  • 点击一级菜单时,点击谁谁展开,(点击信息管理时,客户列表展开缴费列表收起;点击财务管理时,缴费列表展开客户列表收起)
  • 当前二级菜单发送请求后,在菜单栏处仍然是展开当前的二级菜单
  • 权限按钮控制,点击二级菜单时,内容区域没有的权限,其a标签按钮是隐藏的
  • 变面包屑,路径导航列表,在内容区域的上面,实现路径,并且点击可以直接发送请求

具体的代码实现

1、视图函数(views.py)

  # 登录成功后,将权限信息注入session

from django.shortcuts import render, HttpResponse, redirect, reverse
from rbac import models
from rbac.service.rbac import init_permission

# 登录
def login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        pwd = request.POST.get('pwd')
        
        user = models.User.objects.filter(name=username, password=pwd).first()
        
        if not user:
            err_msg = '用户名或密码错误'
            return render(request, 'login.html', {'err_msg': err_msg})

        request.session["user_id"]=user.pk
        
        # 登录成功
        # 将权限信息写入到session
        init_permission(request,user)  # user表示的是当前用户对象
        
        return redirect(reverse('customer'))
    
    return render(request, 'login.html')

  视图(客户列表的增删改查)

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

import os
import mimetypes
from django.shortcuts import render, redirect
from django.http import FileResponse
from django.conf import settings
# import xlrd

from web import models
from web.forms.customer import CustomerForm


def customer_list(request):
    """
    客户列表
    :return:
    """
    data_list = models.Customer.objects.all()

    return render(request, 'customer_list.html', {'data_list': data_list})


def customer_add(request):
    """
    编辑客户
    :return:
    """
    if request.method == 'GET':
        form = CustomerForm()
        return render(request, 'customer_edit.html', {'form': form})
    form = CustomerForm(data=request.POST)
    if form.is_valid():
        form.save()
        return redirect('/customer/list/')
    return render(request, 'customer_edit.html', {'form': form})


def customer_edit(request, cid):
    """
    新增客户
    :return:
    """
    obj = models.Customer.objects.get(id=cid)
    if request.method == 'GET':
        form = CustomerForm(instance=obj)
        return render(request, 'customer_add.html', {'form': form})
    form = CustomerForm(data=request.POST, instance=obj)
    if form.is_valid():
        form.save()
        return redirect('/customer/list/')
    return render(request, 'customer_add.html', {'form': form})


def customer_del(request, cid):
    """
    删除客户
    :param request:
    :param cid:
    :return:
    """
    models.Customer.objects.filter(id=cid).delete()
    return redirect('/customer/list/')

View Code

  视图(缴费的增删改查)

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.shortcuts import render, redirect

from web import models
from web.forms.payment import PaymentForm, PaymentUserForm


def payment_list(request):
    """
    付费列表
    :return:
    """
    data_list = models.Payment.objects.all()
    return render(request, 'payment_list.html', {'data_list': data_list})


def payment_add(request):
    """
    编辑付费记录
    :return:
    """
    if request.method == 'GET':
        form = PaymentForm()
        return render(request, 'payment_edit.html', {'form': form})
    form = PaymentForm(data=request.POST)
    if form.is_valid():
        form.save()
        return redirect('/payment/list/')
    return render(request, 'payment_edit.html', {'form': form})


def payment_edit(request, pid):
    """
    新增付费记录
    :return:
    """
    obj = models.Payment.objects.get(id=pid)
    if request.method == 'GET':
        form = PaymentForm(instance=obj)
        return render(request, 'payment_add.html', {'form': form})
    form = PaymentForm(data=request.POST, instance=obj)
    if form.is_valid():
        form.save()
        return redirect('/payment/list/')
    return render(request, 'payment_add.html', {'form': form})


def payment_del(request, pid):
    """
    删除付费记录
    :param request:
    :param cid:
    :return:
    """
    models.Payment.objects.filter(id=pid).delete()
    return redirect('/payment/list/')

View Code

2、权限相关(都放入项目rbac中)

  (1)模型类(models.py)

from django.db import models

class User(models.Model):
    """
    用户表
    """
    name = models.CharField(max_length=32, verbose_name='用户名')
    password = models.CharField(max_length=32, verbose_name='密码')
    roles = models.ManyToManyField(to='Role', verbose_name='用户所拥有的角色', blank=True)

    def __str__(self):
        return self.name


class Role(models.Model):
    """
    角色表
    """
    name = models.CharField(max_length=32, verbose_name='角色名称')
    permissions = models.ManyToManyField(to='Permission', verbose_name='角色所拥有的权限', blank=True)

    def __str__(self):
        return self.name


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(max_length=32, verbose_name='标题')
    url = models.CharField(max_length=32, verbose_name='权限')
    name = models.CharField(max_length=32, verbose_name="url别名", default="")  # 别名用于按钮控制,没有权限的按钮隐藏
    menu = models.ForeignKey("Menu", on_delete=models.CASCADE, null=True)  # 关联菜单表,通过menu是否有值,可以判断是否为菜单权限
    pid = models.ForeignKey("self", on_delete=models.CASCADE, null=True, verbose_name="父权限")  # 用于判断,点击添加或编辑菜单栏对应的二级菜单仍然展开,
                                                          设置了父权限和子权限,该字段是自关联,
                                                         # 有pid值就是子权限,没有pid值就是菜单权限


    class Meta:
        verbose_name_plural = '权限表'
        verbose_name = '权限表'

    def __str__(self):
        return self.title


class Menu(models.Model):
    """
    菜单表, 用于存放一级菜单标题和图案
    """
    title = models.CharField(max_length=32, verbose_name='菜单')
    icon = models.CharField(max_length=32, verbose_name='图标', null=True, blank=True)

  

  (2)将权限注入session中的方法 init_permission(request,user)

  # rbac(app) / service / rbac.py 

from rbac.models import Role

def init_permission(request,user_obj):   #参数user为当前的登录用户对象

    # 查看当前登录用户拥有的所有权限
    print("user",user) 
    permissions = Role.objects.filter(user=user_obj).values("permissions__title",  # title url用于菜单栏展示与发送请求
                                                        "permissions__url",
                                                        "permissions__name",   # 别名用于按钮控制
                                                        "permissions__pk",   # pk pid 用于判断添加或编辑时,对应的菜单权限扔展开,
                                                        "permissions__pid",  # 存放父权限,表是自关联
                                                        "permissions__menu__title",  # menu用于存一级菜单
                                                        "permissions__menu__icon",
                                                        "permissions__menu__pk").distinct()

    permission_list = []   # 存放当前登录用户所有的权限,用于在中间件进行校验字典形式
    permission_names = []  # 存放别名,用于按钮控制的时候,比较
    permission_menu_dict = {}  #存放的是菜单

    for item in permissions:

        # 构建所有权限列表用于中间件的校验,一条权限是一个字典
        permission_list.append({
            "url":item["permissions__url"],
            "title":item["permissions__title"],
            "id":item["permissions__pk"],
            "pid":item["permissions__pid"]
        })

        # 构建别名列表,用于权限的按钮控制
        permission_names.append(item["permissions__name"])

# 菜单权限
        menu_pk = item["permissions__menu__pk"]
        if menu_pk:  #判断菜单权限是否存在
            if menu_pk not in permission_menu_dict:   # 一级菜单不存在时,需要创建

                # 菜单权限的格式,其中若一级菜单下有多个二级菜单权限,那么children中会有多个字典
                permission_menu_dict[menu_pk]={
                    "menu_title":item["permissions__menu__title"],
                    "menu_icon":item["permissions__menu__icon"],
                    "children":[
                        {
                            "title":item["permissions__title"],
                            "url":item["permissions__url"],
                            "pk":item["permissions__pk"]   #存放当前的菜单权限的id值,(也是父权限的id)
                        }
                    ]
                }
            else:
                # 一级菜单存在时,直接将菜单权限添加到children中
                permission_menu_dict[menu_pk]["children"].append({
                    "title":item["permissions__title"],
                    "url":item["permissions__url"],
            "pk":item["permissions_pk"]
                })

        # 将当前登录人的所有权限,别名,菜单字典注入session中
        request.session["permission_list"]=permission_list
        request.session["permission_names"]=permission_names
        request.session["permission_menu_dict"]=permission_menu_dict

  (3)中间件的校验

  # rbac(app)  / service / middlewares.py

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,redirect
import re
from rbac.models import Permission

class PermissionMiddleWare(MiddlewareMixin):

    def process_request(self,request):

        current_path = request.path

        # 设置白名单

        for reg in ["/login/","/admin*/"]:   # 设置/admin*/  为了使用admin后台录入数据
            ret = re.search(reg,current_path)
            if ret:
                return None

        # 验证登录  登录验证的时候要根据登录认证的时候是什么方式,这里就用什么方式验证
        user_id = request.session.get("user_id")
        if not user_id:
            return redirect("/login/")


        
        # 1、校验权限 2、添加导航列表(做面包细) 3、添加show_id(用于判断当前url,与菜单权限的pk比较,菜单栏相应的二级菜单展开) 

     # 菜单权限校验    
        permission_list = request.session.get("permission_list")

        # 路径导航列表(面包细)将其存放到request内的添加属性breadcrumb(可以随便起名)中,用于在母版layout.html中渲染
        request.breadcrumb=[
 {
"title":"首页",
"url":"/"
            }
 ]

for item in permission_list:
            reg = "^%s$"%(item["url"])
            ret = re.search(reg,current_path)
            if ret:  # 菜单权限校验成功
                show_id = item["pid"] or item["id"]  # 有item["pid"](子权限url的pid值)等于item["pid"],没有值则等于item["id"](父权限的pk)
                request.show_id = show_id  # 当前路径url的 pid ,show_id ,添加到request中临时属性中,便于后面进行比较

                if item["pid"]:
# 请求为子权限时
                    p_permission = Permission.objects.filter(pk=item["pid"]).first()   #子权限的父权限
                   
             # extend 是追加多个,需要放到列表中
                    request.breadcrumb.extend([
                        # 父权限字典
                        {
                            "title":p_permission.title,
                            "url":p_permission.url
                        },
                        # 子权限字典
                        {
                            "title":item["title"],
                            # "url":item["url"]   #由于数据库中存放的是正则的字符串,不能用item["url"],可以直接使用当前路径
                            "url":request.path
                        }
                    ])
                else:
# 请求为菜单父权限时,直接加入到request.breadcrumb属性中
                    request.breadcrumb.append({
                        "title": item["title"],
                        "url": item["url"]
                    })

                return None

        return HttpResponse("无权访问该网页!")

 

templatetags文件,用于自定义过滤器或自定义过滤标签,py文件必须放在 templatetags(文件名不可更改)文件中

  # rbac(app) / templatetags / rbac.py

from django import template

register = template.Library()  # 命名必须为 register

from django.conf import settings
import re

# 1、将菜单权限字典 默认传给 templates/rbac/menu.html中
@register.inclusion_tag("rbac/menu.html")    # 默认找 templates 包中的文件,因此路径中不用写 template
def get_menu(request):
    permission_menu_dict = request.session.get("permission_menu_dict")

    # 二级菜单,点击谁,谁出现,不点击的隐藏,先默认全部隐藏,
    for val in permission_menu_dict.values():
        for item in val["children"]:
            val["class"]="hide"
            # ret = re.search("^%s$"%(item["url"]),request.path)  # 之前是只判断当前路径跟菜单权限一样展开,但是对应的添加和编辑时就不展开了,不符合需求
            if request.show_id==item["pk"]:    # 当前请求url的show_id 与菜单权限的pk一样时,那么该二级菜单权限是展开的
                val["class"]=""

    return {"permission_menu_dict":permission_menu_dict}



# 2、自定义过滤器,传值btn_url在别名列表中,那么返回True ,用于判断权限按钮隐藏是否隐藏
@register.filter
def has_permission(btn_url,request):
    permission_names=request.session.get("permission_names")
    return btn_url in permission_names

templates

    # rbac(app) / templates / rbac / menu.html 

<div class="multi-menu">

    {% for item in permission_menu_dict.values %}           # permission_menu_dict 是get_menu方法传值的,根据传的值来渲染标签,供模板调用
        <div class="item">
            <div class="title"><i class="{{ item.menu_icon }}"></i>{{ item.menu_title }}</div>
            <div class="body {{ item.class }}" >
                {% for foo in  item.children %}
                    <a href="{{ foo.url }}">{{ foo.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}

</div>

    # rbac(app) / static / css / menu.css

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

.static-menu .icon-wrap {
    width: 20px;
    display: inline-block;
    text-align: center;
}

.static-menu a {
    text-decoration: none;
    padding: 8px 15px;
    border-bottom: 1px solid #ccc;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0px 1px 1px white;
}

.static-menu a:hover {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}



.static-menu a.active {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}



.multi-menu .item {
    background-color: white;
}

.multi-menu .item > .title {
    padding: 10px 5px;
    border-bottom: 1px solid #dddddd;
    cursor: pointer;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0 1px 1px white;
}

.multi-menu .item > .body {
    border-bottom: 1px solid #dddddd;
}

.multi-menu .item > .body a {
    display: block;
    padding: 5px 20px;
    text-decoration: none;
    border-left: 2px solid transparent;
    font-size: 13px;

}

.multi-menu .item > .body a:hover {
    border-left: 2px solid #2F72AB;
}

.multi-menu .item > .body a.active {
    border-left: 2px solid #2F72AB;
}

View Code

  # rbac(app) / static / js / menu.js   

    通过点击一级菜单控制二级菜单的显示隐藏

$('.item .title').click(function () {
    $(this).next().toggleClass('hide');
    $(this).parent().siblings().children(".body").addClass("hide")
});

  (6)模板调用(渲染的简单介绍)

    ♥♥♥♥♥ {% load rbac %}  会自动去找 templatetags

    a、layout.html 菜单栏中二级菜单的渲染

<div class="left-menu">
        <div class="menu-body">

            {% load rbac %}
            {% get_menu request %}

        </div>
    </div>

     b、customer_list.html 非菜单权限按钮的控制隐藏(以添加为例)

<div class="btn-group" style="margin: 5px 0">

            {% load rbac %}    
            {% if "customer_add"|has_permission:request %}    
                <a class="btn btn-default" href="/customer/add/">
                    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
                </a>
            {% endif %}

        </div>

     c、其他模板

  layout.html  和 customer_list.html 有必要看看具体是怎么进行渲染的。

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>路飞学城</title>
    <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} ">
    <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/>
    <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/commons.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/nav.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/menu.css' %}">
    <style>
        body {
            margin: 0;
        }

        .no-radius {
            border-radius: 0;
        }

        .no-margin {
            margin: 0;
        }

        .pg-body > .left-menu {
            background-color: #EAEDF1;
            position: absolute;
            left: 1px;
            top: 48px;
            bottom: 0;
            width: 220px;
            border: 1px solid #EAEDF1;
            overflow: auto;
        }

        .pg-body > .right-body {
            position: absolute;
            left: 225px;
            right: 0;
            top: 48px;
            bottom: 0;
            overflow: scroll;
            border: 1px solid #ddd;
            border-top: 0;
            font-size: 13px;
            min-width: 755px;
        }

        .navbar-right {
            float: right !important;
            margin-right: -15px;
        }

        .luffy-container {
            padding: 15px;
        }

        .left-menu .menu-body .static-menu {

        }


    </style>
</head>
<body>

<div class="pg-header">
    <div class="nav">
        <div class="logo-area left">
            <a href="#">
                <img class="logo" src="{% static 'imgs/logo.svg' %}">
                <span style="font-size: 18px;">路飞学城 </span>
            </a>
        </div>

        <div class="left-menu left">
            <a class="menu-item">资产管理</a>
            <a class="menu-item">用户信息</a>
            <a class="menu-item">路飞管理</a>
            <div class="menu-item">
                <span>使用说明</span>
                <i class="fa fa-caret-down" aria-hidden="true"></i>
                <div class="more-info">
                    <a href="#" class="more-item">管他什么菜单</a>
                    <a href="#" class="more-item">实在是编不了</a>
                </div>
            </div>
        </div>

        <div class="right-menu right clearfix">

            <div class="user-info right">
                <a href="#" class="avatar">
                    <img class="img-circle" src="{% static 'imgs/default.png' %}">
                </a>

                <div class="more-info">
                    <a href="#" class="more-item">个人信息</a>
                    <a href="#" class="more-item">注销</a>
                </div>
            </div>

            <a class="user-menu right">
                消息
                <i class="fa fa-commenting-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                通知
                <i class="fa fa-envelope-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                任务
                <i class="fa fa-bell-o" aria-hidden="true"></i>
                <span class="badge bg-danger">4</span>
            </a>
        </div>

    </div>
</div>
<div class="pg-body">
    <div class="left-menu">
        <div class="menu-body">
            {% load rbac %}
            {% get_menu request %}
        </div>
    </div>
    <div class="right-body">
        <div>
            <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">

                {% for item in request.breadcrumb %}
                    <li><a href="{{ item.url }}">{{ item.title }}</a></li>
                {% endfor %}
            
            </ol>
        </div>
        {% block content %} {% endblock %}
    </div>
</div>


<script src="{% static 'js/jquery-3.3.1.min.js' %} "></script>
<script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script>
<script src="/static/js/menu.js"></script>
{% block js %} {% endblock %}
</body>
</html>

layout.html

android 二级菜单框架 二级菜单栏_二级菜单_02

login.html

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}

    <div class="luffy-container">
        <div class="btn-group" style="margin: 5px 0">

            {% load rbac %}
            {% if "customer_add"|has_permission:request %}
                <a class="btn btn-default" href="/customer/add/">
                    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
                </a>
            {% endif %}

        </div>
        <table class="table table-bordered table-hover">
            <thead>
            <tr>
                <th>ID</th>
                <th>客户姓名</th>
                <th>年龄</th>
                <th>邮箱</th>
                <th>公司</th>
                {% if "customer_edit"|has_permission:request %}
                    <th>编辑</th>
                {% endif %}
                {% if "customer_del"|has_permission:request %}
                    <th>删除</th>
                {% endif %}
            </tr>
            </thead>
            <tbody>
            {% for row in data_list %}
                <tr>
                    <td>{{ row.id }}</td>
                    <td>{{ row.name }}</td>
                    <td>{{ row.age }}</td>
                    <td>{{ row.email }}</td>
                    <td>{{ row.company }}</td>
                    {% if "customer_edit"|has_permission:request %}
                        <td><a style="color: #333333;" href="/customer/edit/{{ row.id }}/">
                            <i class="fa fa-edit" aria-hidden="true"></i></a></td>
                    {% endif %}
                    {% if "customer_del"|has_permission:request %}
                        <td><a style="color: #d9534f;" href="/customer/del/{{ row.id }}/"><i class="fa fa-trash-o"></i></a>
                        </td>
                    {% endif %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
{% endblock %}

customer_list.html

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}
    <div class="luffy-container">
        <form class="form-horizontal clearfix" method="post" novalidate>
            {% csrf_token %}

            {% for field in form %}
                <div class="form-group col-sm-6 clearfix">
                    <label class="col-sm-3 control-label">{{ field.label }}</label>
                    <div class="col-sm-9">
                        {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                    </div>
                </div>
            {% endfor %}
            <div class="form-group col-sm-12">
                <div class="col-sm-6">
                    <div class="col-sm-offset-3">
                        <button type="submit" class="btn btn-primary">提 交</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
{% endblock %}

customer_add.html

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}
    <div class="luffy-container">
        <form class="form-horizontal clearfix" method="post" novalidate>
            {% csrf_token %}

            {% for field in form %}
                <div class="form-group col-sm-6 clearfix">
                    <label class="col-sm-3 control-label">{{ field.label }}</label>
                    <div class="col-sm-9">
                        {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                    </div>
                </div>
            {% endfor %}
            <div class="form-group col-sm-12">
                <div class="col-sm-6">
                    <div class="col-sm-offset-3">
                        <button type="submit" class="btn btn-primary">提 交</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
{% endblock %}

customer_edit

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}

    <div class="luffy-container">
        <div style="margin: 5px 0;">
            {% load rbac %}
            {% if "payment_add"|has_permission:request %}
                <a class="btn btn-success" href="/payment/add/">
                    <i class="fa fa-plus-square" aria-hidden="true"></i> 添加缴费记录
                </a>
            {% endif %}
        </div>
        <table class="table table-bordered table-hover">
            <thead>
            <tr>
                <th>ID</th>
                <th>客户姓名</th>
                <th>金额</th>
                <th>付费时间</th>
                {% if "payment_edit"|has_permission:request %}
                    <th>编辑</th>
                {% endif %}
                {% if "payment_del"|has_permission:request %}
                    <th>删除</th>
                {% endif %}
            </tr>
            </thead>
            <tbody>
            {% for row in data_list %}
                <tr>
                    <td>{{ row.id }}</td>
                    <td>{{ row.customer.name }}</td>
                    <td>{{ row.money }}</td>
                    <td>{{ row.create_time|date:"Y-m-d H:i:s" }}</td>
                    {% if "payment_edit"|has_permission:request %}
                        <td>
                            <a style="color: #333333;" href="/payment/edit/{{ row.id }}/">
                                <i class="fa fa-edit" aria-hidden="true"></i></a>
                        </td>
                    {% endif %}
                    {% if "payment_del"|has_permission:request %}
                        <td><a style="color: #d9534f;" href="/payment/del/{{ row.id }}/"><i
                                class="fa fa-trash-o"></i></a></td>
                    {% endif %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
{% endblock %}

payment_list.html

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}
    <div class="luffy-container">
        <form class="form-horizontal clearfix" method="post" novalidate>
            {% csrf_token %}

            {% for field in form %}
                <div class="form-group col-sm-6 clearfix">
                    <label class="col-sm-3 control-label">{{ field.label }}</label>
                    <div class="col-sm-9">
                        {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                    </div>
                </div>
            {% endfor %}
            <div class="form-group col-sm-12">
                <div class="col-sm-6">
                    <div class="col-sm-offset-3">
                        <button type="submit" class="btn btn-primary">提 交</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
{% endblock %}

payment_add.html

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

{% extends 'layout.html' %}

{% block content %}

    <div class="luffy-container">
        <form class="form-horizontal clearfix" method="post" novalidate>
            {% csrf_token %}

            {% for field in form %}
                <div class="form-group col-sm-6 clearfix">
                    <label class="col-sm-3 control-label">{{ field.label }}</label>
                    <div class="col-sm-9">
                        {{ field }} <span style="color:firebrick;">{{ field.errors.0 }}</span>
                    </div>
                </div>
            {% endfor %}
            <div class="form-group col-sm-12">
                <div class="col-sm-6">
                    <div class="col-sm-offset-3">
                        <button type="submit" class="btn btn-primary">提 交</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
{% endblock %}

payment_edit.html

    # models.py  customer表和 payment表 

android 二级菜单框架 二级菜单栏_二级菜单_02

android 二级菜单框架 二级菜单栏_html_03

from django.db import models


class Customer(models.Model):
    """
    客户表
    """
    name = models.CharField(verbose_name='姓名', max_length=32)
    age = models.CharField(verbose_name='年龄', max_length=32)
    email = models.EmailField(verbose_name='邮箱', max_length=32)
    company = models.CharField(verbose_name='公司', max_length=32)

    def __str__(self):
        return self.name

class Payment(models.Model):
    """
    付费记录
    """
    customer = models.ForeignKey(verbose_name='关联客户', to='Customer',on_delete=models.CASCADE)
    money = models.IntegerField(verbose_name='付费金额')
    create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)

View Code

总结1:二级菜单分级动态显示

一级菜单需要保存在Menu表中,保存名称与图标,在权限表中有 menu 字段(foreginekey)与之关联,其中属于菜单权限的,menu字段有值

思路:菜单权限的数据格式,便于后面渲染时取值

permission_menu_dict= {
        
         1:{
            "title":"信息管理",
            "icon":"",
            "children":[
                {
                  "title":"客户列表",
                  "url":"",
           "pk":"",
                },
                 {
                  "title":"我的私户",
                  "url":"",
           "pk":"",
                 },
            ]
            
          },
          
          2:{
            "title":"财务管理",
            "icon":"",
            "children":[
                {
                  "title":"缴费列表",
                  "url":"",
            "pk":"",
                },
            ]
            
          }, 
     
       }

总结2:点击一级菜单控制二级菜单的显示隐藏

点击一级菜单时,点击谁谁展开,(点击信息管理时,客户列表展开缴费列表收起;点击财务管理时,缴费列表展开客户列表收起)

思路:可以通过DOM操作来完成,点击一级菜单时,next下一个标签是隐藏的,其他一级菜单的孩子都是hide

  # rbac(app) / static / js / menu.js    (通过点击一级菜单控制二级菜单的显示隐藏)

$('.item .title').click(function () {
    $(this).next().toggleClass('hide');
    $(this).parent().siblings().children(".body").addClass("hide")
});

总结3:二级菜单、非菜单权限 发送请求时,相应的菜单权限仍然是展开的

当前二级菜单、非菜单权限发送请求后,在菜单栏处仍然是展开当前的二级菜单

思路:

  • 在权限表中添加一个字段 pid (自关联),父权限也就是相应的菜单权限
  • 分两种情况,当前请求url 为菜单权限时或为子权限时:
  • 在中间件中定义一个 show_id 用来记录 子权限的 pid 或父权限的pk,因为在中间件中对当前登录用户所有权限进行校验,此时可以将权限的 pid 或 pk 取出来,所以在中间件中就添加  show_id
  • 将 show_id 存储在 request.show_id 的临时属性中,因为 request是全局变量,可以供后面其他文件直接使用
  • 在 templatetags 中的 rbac.py 文件中 get_menu 方法中比较 show_id 与相应菜单权限的 pk, 一样则 菜单权限是展开的

1、permission表新增一个pid字段,表示非菜单权限的父级菜单权限id,permission模型类如下:

class Permission(models.Model):
    """
    权限表
    """
    url = models.CharField(verbose_name='含正则的URL', max_length=32)
    title = models.CharField(verbose_name='标题', max_length=32)
    menu = models.ForeignKey(verbose_name='标题', to="Menu", on_delete=models.CASCADE, null=True)
    name = models.CharField(verbose_name='url别名', max_length=32, default="")
    pid = models.ForeignKey("self", on_delete=models.CASCADE, null=True, verbose_name='父权限')

def __str__(self):
        return self.title

 

# 权限表结构

        

android 二级菜单框架 二级菜单栏_html_25

2、修改权限列表数据结构,注入session,setsession.py中代码如下:

  详细见前面的代码。

 

3、# 中间件中 要做修改

for item in permission_list:
            reg = "^%s$"%(item["url"])
            ret = re.search(reg,current_path)    # 校验当前全向成功
            if ret:
                show_id = item["pid"] or item["id"]     # 有item["pid"](子权限url的pid值)等于item["pid"],没有值则等于item["id"](父权限的pk)
                request.show_id = show_id               # 当前路径url的 pid ,show_id ,添加到request中临时属性中,便于后面进行比较

4、# templatetags / rbac.py

# 1、将菜单权限字典 默认传给 templates/rbac/menu.html中
@register.inclusion_tag("rbac/menu.html")    # 默认找 templates 包中的文件,因此路径中不用写 template
def get_menu(request):
    permission_menu_dict = request.session.get("permission_menu_dict")

    # 二级菜单,点击谁,谁出现,不点击的隐藏,先默认全部隐藏,
    for val in permission_menu_dict.values():
        for item in val["children"]:
            val["class"]="hide"   # 默认是隐藏的
            # ret = re.search("^%s$"%(item["url"]),request.path)  # 之前是只判断当前路径跟菜单权限一样展开,但是对应的添加和编辑时就不展开了,不符合需求
            if request.show_id==item["pk"]:    # 当前请求url的show_id 与菜单权限的pk一样时,那么该二级菜单权限是展开的
                val["class"]=""

    return {"permission_menu_dict":permission_menu_dict}

总结4:动态显示按钮权限(非菜单权限)

  权限按钮控制,点击二级菜单时,内容区域没有的权限,其a标签按钮是隐藏的

 除了菜单权限,还有按钮权限,比如添加用户(账单),编辑用户(账单),删除用户(账单),这些不是菜单选项,而是以按钮的形式在页面中显示,但不是所有的用户都有所有的按钮权限,我们需要在用户不拥有这个按钮权限时就不要显示这个按钮,下面介绍一下思路和关键代码。

1、在permission表中增加一个字段name,permission模型类如下:

class Permission(models.Model):
    """
    权限表
    """
   url = models.CharField(verbose_name='含正则的URL', max_length=32)
    title = models.CharField(verbose_name='标题', max_length=32)
    menu = models.ForeignKey(verbose_name='标题', to="Menu", on_delete=models.CASCADE, null=True)
    name = models.CharField(verbose_name='url别名', max_length=32, default="")

   def __str__(self):
            return self.title

  注意:permission表中新增name字段别名与urls.py中的别名没有关系,当然也可以起一样的名字,心里明白他们其实并无关系即可。

2、将权限别名列表注入session,setsession.py中代码如下:

  详细代码见前面   # rbac(app) / service / rbac.py 中关于permission_names

def initial_session(user_obj, request):
    """
    将当前登录人的所有权限url列表和
    自己构建的所有菜单权限字典和
    权限表name字段列表注入session
    :param user_obj: 当前登录用户对象
    :param request: 请求对象HttpRequest
    """
    # 查询当前登录人的所有权限列表
    ret = Role.objects.filter(user=user_obj).values('permissions__url',
                                                    'permissions__title',
                                                    'permissions__name',
                              'permissions__menu__title',
                                      'permissions__menu__icon',
                                  'permissions__menu__id').distinct()
    permission_list = []
    permission_names = []
    permission_menu_dict = {}
    for item in ret:
        # 获取用户权限列表用于中间件中权限校验
        permission_list.append(item['permissions__url'])
        # 获取权限表name字段用于动态显示权限按钮
        permission_names.append(item['permissions__name'])
        
        menu_pk = item['permissions__menu__id']
        if menu_pk:
            if menu_pk not in permission_menu_dict:
                permission_menu_dict[menu_pk] = {
                    "menu_title": item["permissions__menu__title"],
                    "menu_icon": item["permissions__menu__icon"],
                    "children": [
                        {
                            "title": item["permissions__title"],
                            "url": item["permissions__url"],
                        }
                    ],
                }
            else:
                permission_menu_dict[menu_pk]["children"].append({
                    "title": item["permissions__title"],
                    "url": item["permissions__url"],
                })
    print('权限列表', permission_list)
    print('菜单权限', permission_menu_dict)
    # 将当前登录人的权限列表注入session中
    request.session['permission_list'] = permission_list
    # 将权限表name字段列表注入session中
    request.session['permission_names'] = permission_names
    # 将当前登录人的菜单权限字典注入session中
    request.session['permission_menu_dict'] = permission_menu_dict

3、自定义过滤器  # templatetags / rbac.py

  判断当前的请求路径是否在按钮权限字典中,

@register.filter
def has_permission(btn_url, request):
    permission_names = request.session.get("permission_names")
    return btn_url in permission_names    # 在则返回True

4、使用过滤器,模板(如customer_list.html)中部分权限按钮代码如下:

<div class="btn-group">
    {% load my_tags %}
    {% if "customer_add"|has_permission:request %}
         <a class="btn btn-default" href="/customer/add/">
            <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
         </a>
    {% endif %}
</div>

总结5:路径导航栏(面包屑)

面包屑,路径导航列表,在内容区域的上面,实现路径,并且点击可以直接发送请求,

如图:

    

android 二级菜单框架 二级菜单栏_html_26

 

1、中间件的时候,将路径列表存储在request.breadcrumb中

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import redirect, HttpResponse
import re
class PermissionMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        # 设置白名单放行
        for reg in ["/login/", "/admin/*"]:
            ret = re.search(reg, request.path)
            if ret:
                return None

        # 检验是否登录
        user_id = request.session.get('user_id')
        if not user_id:
            return redirect('/login/')

        # 检验权限
        permission_list = request.session.get('permission_list')

        # 路径导航列表
        request.breadcrumb = [
    {
"title": "首页",
"url": "/"
            },
        ]
        for item in permission_list:
            reg = '^%s$' % item["url"]
            ret = re.search(reg, request.path)
            if ret:
                show_id = item["pid"] or item["id"]
                request.show_id = show_id  # 给request对象添加一个属性

                # 确定面包屑列表
                if item["pid"]:
    ppermission = Permission.objects.filter(pk=item["pid"])
     .first()
  request.breadcrumb.extend(
   [{  # 父权限字典
                            "title": ppermission.title,
"url": ppermission.url
     },
    {  # 子权限字典
                            "title": item["title"],
"url": request.path
     }]
    )
else:
     request.breadcrumb.append(
     {
"title": item["title"],
"url": item["url"]
                        }
                    )
                    
                return None

        return HttpResponse('无权访问')

 

 

 2、在母版中渲染 面包屑 

   #  母版中的 html 文件

<div class="content-wrapper">
        <!-- 添加面包屑 -->
        <div>
            <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">   {% for item in request.breadcrumb %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>  {% endfor %}

</ol>
        </div>
        <br>
    

        {% block content %}
      <!-- 其他子模板中调用的时候,代码区域 -->

        {% endblock %}
    
    </div>