Django:二级菜单权限动态显示

 

阅读目录

 


回到顶部

二级菜单权限

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

 

介绍及需求

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

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

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

 

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

 

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

View Code

  视图(缴费的增删改查)

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("无权访问该网页!")

 

 

 (4)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

 

 (5)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

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 文件中的 rbac

    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 有必要看看具体是怎么进行渲染的。

layout.html

login.html

customer_list.html

customer_add.html

customer_edit

payment_list.html

payment_add.html

payment_edit.html

models.py customer表和 payment表

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

 

 

# 权限表结构

 

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:路径导航栏(面包屑)

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

如图:

    

 

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>

 

 

posted on 2022-08-10 08:31  root-123  阅读(806)  评论(0编辑  收藏  举报