2 动态菜单

1 一级菜单

用户登录后,我们不应该单单只让用户拿到权限信息。应该也可以拿到动态菜单

那么如何显示一级菜单?实现思路如下:

我们需要在数据库permission表中,添加一个字段,表示该权限是否可以成为菜单。
用户第一次发送请求时,我们不单单只把权限信息放入session,还要把用户权限中可以成为菜单的放入另一个session中。
用户第二次发送请求,经过中间件,读取权限信息并进行校验,成功返回页面。在页面中的菜单,需要在session中读取。

第一步:在数据表中添加相应字段

models.py
from django.db import models


class Permission(models.Model):
    """权限表"""
    title = models.CharField(verbose_name="标题", max_length=32)
    url = models.CharField(verbose_name="含正则的URL", max_length=128)
    icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标')
    is_menu = models.BooleanField(verbose_name='是否是菜单', default=False)

    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


PS:
fontawesome图标库:http://www.fontawesome.com.cn/faicons/


第一步:获取菜单信息并存储session

settings.py
PERMISSION_SESSION_KEY = "crm_permission_url_list"
MENU_SESSION_KEY = "crm_permission_menu_list"
VALID_URL_LIST = ["/login/", "/admin/.*", ]
account.py
from django.shortcuts import render, HttpResponse, redirect

from rbac.service.init_permission import init_permission
from rbac import models


def login(request):
    if request.method == "GET":
        return render(request, "login.html")

    user = request.POST.get("user")
    pwd = request.POST.get("pwd")

    current_user = models.UserInfo.objects.filter(name=user, password=pwd).first()
    if not current_user:
        return render(request, "login.html", {"msg": "用户名密码错误"})

    # 权限信息的初始化
    init_permission(current_user, request)

    return redirect("/customer/list/")
init_permission.py
from django.conf import settings


def init_permission(current_user, request):
    """
    用户权限信息的初始化
    :param current_user:当前用户对象
    :param request:当前用户的请求数据
    :return:
    """
    # 根据当前用户信息获取此用户拥有的所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__title",
        "permissions__is_menu",
        "permissions__icon",
        "permissions__url",).distinct()

    # 获取权限+菜单信息,写入session
    menu_list = []
    permissions_list = []
    for item in permission_queryset:
        permissions_list.append(item["permissions__url"])
        if item["permissions__is_menu"]:
            temp = {
                "title": item["permissions__title"],
                "icon": item["permissions__icon"],
                "url": item["permissions__url"],
            }
            menu_list.append(temp)

    request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
    request.session[settings.MENU_SESSION_KEY] = menu_list

第三步:模板中显示菜单信息(session)

layout.html

{% load rbac %}

<div class="static-menu">
    {% static_menu request %}
</div>

rbac.py

from django.template import Library
from django.conf import settings

register = Library()


@register.inclusion_tag("rbac/static_menu.html")
def static_menu(request):
    """创建一级菜单"""
    menu_list = request.session[settings.MENU_SESSION_KEY]
    return {"menu_list": menu_list, "path": request.path_info}

static_menu.html

{% for item in menu_list %}
    <a href="{{ item.url }}" class="{% if path == item.url %}active{% endif %}">
        <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}</a>
{% endfor %}

image

一级菜单示例代码下载


2 二级菜单

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

1 第一步:session中存储的菜单信息结构

{
    1: {
        "title": "信息管理",
        "icon": "x1",
        "children": [
            {"title": "客户列表", "url": "/customer/list/"}
            {"title": "账单列表", "url": "/account/list/"}
        ]
    },
    2: {
        "title": "用户管理",
        "icon": "x2",
        "children": [
            {"title": "个人资料", "url": "/userinfo/list/"}
        ]
    },
}

2 第一步:数据表中的修改

models.py
from django.db import models


class Menu(models.Model):
    title = models.CharField(verbose_name="一级菜单名称", max_length=32)
    icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)

    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表示不是菜单,非null表示是二级菜单", on_delete=models.CASCADE)

    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

3 第三步:页面显示二级菜单

layout.html

{% load static %}
{% load rbac %}

<div class="static-menu">
    {% multi_menu request %}
</div>
rbac.py
import re
from django.template import Library
from django.conf import settings
from collections import OrderedDict

register = Library()


@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :return:
    """
    menu_dict = request.session[settings.MENU_SESSION_KEY]

    # 对字典的key进行排序
    key_list = sorted(menu_dict)

    # 空的有序字典
    ordered_dict = OrderedDict()

    for key in key_list:
        val = menu_dict[key]
        val['class'] = 'hide'

        for per in val['children']:
            regex = "^%s$" % (per['url'],)
            if re.match(regex, request.path_info):
                per['class'] = 'active'
                val['class'] = ''
        ordered_dict[key] = val

    return {'menu_dict': ordered_dict}
static_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 class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>

二级菜单示例代码下载

3 默认展开非菜单URL

由于很多URL都是不能作为菜单,所以当点击该类功能时,是无法默认展开菜单的,如:

  • 删除
  • 修改
    ...

image


点击非菜单的权限时,默认选中或默认展开。

当点击某个不能成为菜单的权限时,指定一个可以成为菜单的权限,让其默认选中及展开


第一步:修改models表结构

models.py
from django.db import models


class Menu(models.Model):
    """
    菜单表
    """
    title = models.CharField(verbose_name='一级菜单名称', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True)

    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表示不是菜单;非null表示是二级菜单',
                             on_delete=models.CASCADE)

    pid = models.ForeignKey(verbose_name="关联的权限", to="Permission", null=True, blank=True, related_name="parents",
                            help_text="对于非菜单权限需要选择一个可以成为菜单的权限,用于做默认展开和选中菜单",
                            on_delete=models.CASCADE)

    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

image

思路梳理:

登录,做权限和菜单的初始化:
  - 获取菜单信息
    {
        1: {
            "title": "信息管理",
            "icon": "x1",
            "children": [
                {"id": 1, "title": "客户列表", "url": "/customer/list/"}  # 可以做菜单权限的id
            ]
        },
        2: {
            "title": "用户管理",
            "icon": "x2",
            "children": [
                {"id": 7, "title": "账单列表", "url": "/account/list/"}  # 可以做菜单权限的id
            ]
        },
    }
  - 获取权限信息
   [
       {"id": 1, "url":"/customer/lsit/", "pid": null},  # 可以做菜单
       {"id": 2, "url":"/customer/add/", "pid": 1},  # 不可以做菜单
   ]

再次来访问
  - 中间件进行权限的校验(根据权限信息)
    获取ID或PID (应该被选中的可以做菜单的权限ID)

模板中使用inclusion_tag生成动态菜单(根据菜单信息进行动态生成)

第二步:登录,做权限和菜单的初始化

init_permission.py
from django.conf import settings


def init_permission(current_user, request):
    """
    用户权限信息的初始化
    :param current_user:当前用户对象
    :param request:当前用户的请求数据
    :return:
    """
    # 根据当前用户信息获取此用户拥有的所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__title",
        "permissions__url",
        "permissions__pid_id",
        "permissions__menu__id",
        "permissions__menu__title",
        "permissions__menu__icon", ).distinct()

    # 获取权限+菜单信息,写入session
    menu_dict = {}
    permissions_list = []
    for item in permission_queryset:
        permissions_list.append(
            {"id": item["permissions__id"], "url": item["permissions__url"], "pid": item['permissions__pid_id']})

        menu_id = item["permissions__menu__id"]
        if not menu_id:
            continue
        node = {"id": item["permissions__id"], "title": item["permissions__title"], "url": item["permissions__url"]}
        if menu_id in menu_dict:
            menu_dict[menu_id]["children"].append(node)
        else:
            menu_dict[menu_id] = {
                "title": item["permissions__menu__title"],
                "icon": item["permissions__menu__icon"],
                "children": [node, ]
            }

    request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
    request.session[settings.MENU_SESSION_KEY] = menu_dict

第三步:中间件进行权限的校验,模板中使用inclusion_tag生成动态菜单

middlewares/rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings


class RbacMiddleware(MiddlewareMixin):
    """
    用户权限信息校验
    """

    def process_request(self, request):
        """
        当用户请求刚进入时候出发执行
        :param request:
        :return:
        """

        """
        1. 获取当前用户请求的URL
        2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
        3. 权限信息匹配
        """
        current_url = request.path_info
        for valid_url in settings.VALID_URL_LIST:
            if re.match(valid_url, current_url):
                # 白名单中的URL无需权限验证即可访问
                return None

        permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
        if not permission_list:
            return HttpResponse('未获取到用户权限信息,请登录!')

        flag = False

        for item in permission_list:
            reg = "^%s$" % item["url"]
            if re.match(reg, current_url):
                flag = True
                request.current_selected_permission = item["pid"] or item["id"]
                break

        if not flag:
            return HttpResponse('无权访问')

templatetags/rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.template import Library
from django.conf import settings
from collections import OrderedDict

register = Library()


@register.inclusion_tag('rbac/static_menu.html')
def static_menu(request):
    """
    创建一级菜单
    :return:
    """
    menu_list = request.session[settings.MENU_SESSION_KEY]
    return {'menu_list': menu_list, "path": request.path_info}


@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :return:
    """
    menu_dict = request.session[settings.MENU_SESSION_KEY]

    # 对字典的key进行排序
    key_list = sorted(menu_dict)

    # 空的有序字典
    ordered_dict = OrderedDict()

    for key in key_list:
        val = menu_dict[key]
        val['class'] = 'hide'

        for per in val['children']:
            if per["id"] == request.current_selected_permission:
                per['class'] = 'active'
                val['class'] = ''
        ordered_dict[key] = val

    return {'menu_dict': ordered_dict}

layout.html

{% load static %}
{% load rbac %}

<div class="static-menu">
    {% multi_menu request %}
</div>

非菜单默认选中代码下载


4 动态菜单之路径导航

init_permission.py
from django.conf import settings


def init_permission(current_user, request):
    """
    用户权限信息的初始化
    :param current_user:当前用户对象
    :param request:当前用户的请求数据
    :return:
    """
    # 根据当前用户信息获取此用户拥有的所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__title",
        "permissions__url",
        "permissions__pid__id",
        "permissions__pid__title",
        "permissions__pid__url",
        "permissions__menu__id",
        "permissions__menu__title",
        "permissions__menu__icon", ).distinct()

    # 获取权限+菜单信息,写入session
    menu_dict = {}
    permissions_list = []
    for item in permission_queryset:
        permissions_list.append(
            {
                "id": item["permissions__id"],
                "title": item["permissions__title"],
                "url": item["permissions__url"],
                "pid": item['permissions__pid__id'],
                "p_title": item["permissions__pid__title"],
                "p_url": item["permissions__pid__url"]
            })

        menu_id = item["permissions__menu__id"]
        if not menu_id:
            continue
        node = {"id": item["permissions__id"], "title": item["permissions__title"], "url": item["permissions__url"]}
        if menu_id in menu_dict:
            menu_dict[menu_id]["children"].append(node)
        else:
            menu_dict[menu_id] = {
                "title": item["permissions__menu__title"],
                "icon": item["permissions__menu__icon"],
                "children": [node, ]
            }

    request.session[settings.PERMISSION_SESSION_KEY] = permissions_list
    request.session[settings.MENU_SESSION_KEY] = menu_dict

rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.conf import settings


class RbacMiddleware(MiddlewareMixin):
    """
    用户权限信息校验
    """

    def process_request(self, request):
        """
        当用户请求刚进入时候出发执行
        :param request:
        :return:
        """

        """
        1. 获取当前用户请求的URL
        2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
        3. 权限信息匹配
        """
        current_url = request.path_info
        for valid_url in settings.VALID_URL_LIST:
            if re.match(valid_url, current_url):
                # 白名单中的URL无需权限验证即可访问
                return None

        permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
        if not permission_list:
            return HttpResponse('未获取到用户权限信息,请登录!')

        flag = False
        url_record = [
            {"title": "首页", "url": "#"}
        ]

        for item in permission_list:
            reg = "^%s$" % item["url"]
            if re.match(reg, current_url):
                flag = True
                request.current_selected_permission = item["pid"] or item["id"]
                if not item["pid"]:
                    url_record.extend([{"title": item["title"], "url": item["url"]}])
                else:
                    url_record.extend([
                        {"title": item["p_title"], "url": item["p_url"]},
                        {"title": item["title"], "url": item["url"]},
                    ])
                request.url_record = url_record
                break
        if not flag:
            return HttpResponse('无权访问')

layout.html
    <div class="right-body">
        <div>
            <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">

                {% for item in request.url_record %}
                    <li><a href="{{ item.url }}">{{ item.title }}</a></li>
                {% endfor %}

            </ol>
        </div>
        {% block content %} {% endblock %}
    </div>

路径导航代码下载

posted @ 2022-08-26 11:50  角角边  Views(79)  Comments(0Edit  收藏  举报