Django Rbac组件开发详解

Section 1 权限(rbac)

步骤:

1.创建django project,luffy_permission

django-admin startproject luffy_permission
python manage.py startapp rbac
python manage.py startapp web

 

 

2.两个app应用

-- rbac ,权限组件 -- web,销售管理系统

# 目录结构
luffy_permission/
├── db.sqlite3
├── luffy_permission
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── rbac            # 权限组件,便于以后应用到其他系统
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── templates
└── web            # 客户管理业务
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py

 

 

3.app:rbac

-- 将权限相关的表通过模型类编写到次app的models.py中 -- 权限表结构设计

# 进行权限分配时只需要给指定角色分配一次权限,再给众多用户分配一次角色即可
from django.db import models
​
class Pemission(models.Model):
    # 权限表
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)
    
    def __str__(self):
        return self.title
    
class Role(models.Model):
    # 角色表
    title = models.CharField(vebose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='角色拥有的权限', to='Pemission', 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=64)
    roles = models.ManyToManyField(verbose_name='用户拥有的角色', to='Role', blank=True)
    
    def __str__(self):
        return self.title

 

 

4.app:web

-- 将销售管理系统表通过模型类编写到次app的models.py中 -- 销售系统的业务相关代码:

# 表结构
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)
​
​
class Payment(models.Model):
    """
    付费记录
    """
    customer = models.ForeignKey(verbose_name='关联客户', to='Customer')
    money = models.IntegerField(verbose_name='付费金额')
    create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)

 

 

5.两个app的整合

  • 客户管理系统中的URL:

    • 客户管理

      1. 客户列表:/customer/list/

      2. 添加客户:/customer/add/

      3. 删除客户:/customer/list/(?P<cid>\d+)/

      4. 修改客户:/customer/edit/(?P<cid>\d+)/

      5. 批量导入:/customer/import/

      6. 下载模板:/customer/tpl/

    • 账单管理

      1. 账单列表:/payment/list/

      2. 添加账单:/payment/add/

      3. 删除账单:/payment/del/(?P<pid>\d+)/

      4. 修改账单:/payment/edit/<?P<pid>\d+/

         

    5.1 基于admin进行权限信息录入;

    5.2 基于admin进行权限和角色信息的分配:创建角色,创建用户,为用户分配角色,为角色分配权限;

     

 

 

6.快速完成基本权限的控制

 

# 用户登录
from django.shortcuts import HttpResponse,render
from rbaac 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.object.filter(name=user, password=pwd).first()
    if not current_user:
        return render(request, 'login.html',{'msg':'用户名或密码错误'})
    
    # 根据当前用户信息获取此用户所拥有的所有权限,并放入session
    permission_queryset = current_user.roles.filter(permission__isnull=False),values('permission__id','permission__url').distinct() # {'permission_id': 1, 'permission_url': '/customer/list/'}...
    
    #获取权限中所有的URL
    permission_list = [item['permission_url'] for item in permission_queryset]
    request.session['luffy_permission_url_list'] = permission_list
    
    return redirect('/customer/list/')

将用户所有的权限写入session后编写中间件实现用户权限信息校验(权限信息初始化)

import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
from django.middleware.csrf import CsrfViewMiddleware
​
class CheckPermission(MiddlewareMixin):
    # 用户权限信息校验
    def process_request(self,request):
        # 当请求进入时触发执行
        '''
        1. 获取当前用户请求的URL
        2. 获取当前用户在session中保存的权限列表['/customer/list/', '/customer/list/(?P<cid>\\d+)/']
        '''
        # http://127.0.0.1:8000/customer/list/           -->  /customer/list/
        # http://127.0.0.1:8000/customer/list/?age=10    -->  /customer/list/
        valid_url_list = [
            '/login/',
            '/admin/.*'
        ]
        current_url = request.path_info
        for valid_url in valid_url_list:
            if re.match(valid_url, current_url):
                # 白名单中的URL无需验证即可访问
                return None
        permission_list = request.session.get('luffy_permission_url_list')
        if not permission_list:
            return HttpResponse('未获取到用户权限信息,请登录!')
        
        flag = False
        
        for url in permission_list:
            reg = '^%s$' % url
            if re.match(reg, current_url):
                flag = True
                break
        if not flag:
            return HttpResponse('无权访问')
       # 写完后在settings.py中写入中间件 

 

7.功能完善

  • a. 用户登录和权限初始化拆分

    1. 将权限相关的功能放到rbac应用下,以便以后组件的使用;

    2. 可以将 session key 放入 settings.py 中方便后期统一修改;

    3. 将之前写在web应用中的中间件放入rbac.middlewares中,并改名为RbacMiddleware;

    4. 将中间件的白名单放到 settings.py 中方便配置;

总结:6/7属于进行权限控制

 

 

8.动态菜单功能

8.1 一级菜单

a. 表结构修改

from django.db import models
​
class Pemission(models.Model):
    # 权限表
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)
    is_menu = models.BooleanField(verbose_name='是否可以做菜单', default=False)
    icon = models.CharField(verbose_name='图标', null=True, blank=True) 
    # null:数据库可为空,blank:admin添加可为空
    
    def __str__(self):
        return self.title

b. 获取菜单信息并保存到session

 
# 根据当前用户信息获取此用户所拥有的所有权限,并放入session
permission_queryset = current_user.roles.filter(permission__isnull=False),values('permission__id','permission__title','permission__is_menu','permission__icon','permission__url').distinct() # {'permission_id': 1, 'permission_url': '/customer/list/'}...
​
# 获取权限+菜单信息
# 获取权限中所有的URL
menu_list = []
permission_list = []
for item in permission_queryset:
    permission_list.append(item['permission_url'])
    if item['permission__is_menu']:
        temp = {
            'title':item['permission__title'],
            'icon':item['permission__icon'],
            'url':item['permission__url']
        }
        menu_list.append(temp) 
        
request.session['luffy_permission_url_list'] = permission_list
request.session['luffy_permission_menu_key'] = menu_list

c. 模板中显示菜单信息(session)

<div class="pg-body">
    <div class="left-menu">
        <div class="menu-body">
            <div class="static-menu">
                {% for item in request.session.luffy_permission_menu_key %}
                    <a href="{{ item.url }}" class="active">
                        <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}
                    </a>
                {% endfor %}
            </div>
        </div>
    </div>

inclusion_tag

用于在模板里实现复杂代码的优化(一行搞定)

注意:inclusion_tag的使用(详见视频第七模块1.15 28:40)

8.2 二级菜单

a. 构建用于session存储的数据结构:

{
    1:{
        title:'信息管理',
        icon:'fa-xxxx',
        sub_menu:[
            {'title':'客户列表', 'url':'/customer/list/'},
            {'title':'账单列表', 'url':'/account/list/'},
        ]
    },
    2:{
        title:'信息管理',
        icon:'fa-xxxx',
        sub_menu:[
            {'title':'个人资料', 'url':'/userinfo/list/'},
        ]
    }
}

获取菜单信息并保存到session

# 根据当前用户信息获取此用户所拥有的所有权限,并放入session
permission_queryset = current_user.roles.filter(permission__isnull=False),values('permission__id','permission__title','permission__url','permission__menu_id','permission__menu__title','permission__menu__icon').distinct() # {'permission_id': 1, 'permission_url': '/customer/list/'}...
​
# 获取权限+菜单信息
# 获取权限中所有的URL
menu_dict = {}
permission_list = []
for item in permission_queryset:
    permission_list.append(item['permission_url'])
    menu_id = item['permission__menu_id']
    if not menu_id:
        continue
    node = {'title':item['permission__title'], 'url':item['permission__url']}
    if menu_id in menu_dict:
        menu_dict[menu_id]['sub_menu'].append(node)
    else:
        menu_dict[menu_id] = {
            'title':item['permission__menu__title'],
            'icon':item['permission__menu__icon'],
            'sub_menu':[node,]
        }
        
request.session['luffy_permission_url_list'] = permission_list
request.session['luffy_permission_menu_key'] = menu_list

b. 数据表结构修改

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 Pemission(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表示二级菜单')
    
    def __str__(self):
        return self.title    

c. 页面显示二级菜单

 

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

当点击某个不能成为菜单的权限时,指定一个可以成为菜单的权限,让其默认选中并展开 a. 数据库添加新字段 -- 为权限表添加 pid_id 字段(能做菜单的权限此字段为 null,非null的则为不能做菜单的权限) b. 设计思路 -- 登录,做权限和菜单初始化;

# 获取菜单信息
{
    1:{
        'title': '信息管理',
        'icon': 'fa-xxxx',
        'class': '',
        'sub_menu':[
            {'id':1, 'url': '/cusotmer/list/', 'title': '客户列表'}
        ],
    },
     2:{
        'title': '用户管理',
        'icon': 'fa-xxxx',
        'class': 'hide',
        'sub_menu':[
            {'id':7, 'url': '/payment/list/', 'title': '账单列表'}
        ],
    }
}
# 获取权限信息
[
    {'id':1, 'url':'/customer/list/','pid':null},           # 客户列表,可做菜单的权限
    {'id':2, 'url':'/customer/add/','pid':1},               # 添加客户,不可做菜单
    {'id':2, 'url':'/customer/del/(?P<cid>\d+)/','pid':1},   # 删除客户,不可做菜单
]
​
# 权限信息由之前的权限列表(列表内每个元素为权限)变更为新权限列表(新列表内的元素为字典,如上代码所示
# 由于初始化的数据结构变更,因而中间件的代码需做相应调整

-- 中间件 -- 当权限和菜单初始化以后中间件会进行权限的校验(根据权限信息) -- 代码做相应的调整获取session中的 ID 或 PID(用以给视图函数判断菜单关系)

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

 

10.路径导航

10.1 初始化的时候

# 根据当前用户信息获取此用户所拥有的所有权限,并放入session
permission_queryset = current_user.roles.filter(permission__isnull=False),values('permission__id','permission__title','permission__url','permission__pid_id','permission__pid__title','permission__pid__url','permission__menu_id','permission__menu__title','permission__menu__icon').distinct() # {'permission_id': 1, 'permission_url': '/customer/list/'}...
​
# 获取权限+菜单信息
# 获取权限中所有的URL
menu_dict = {}
permission_list = []
for item in permission_queryset:
    permission_list.append(
        {
            'id':item['permissions__id'],
            'url':item['permissions__url'],
            'title':item['permissions__title'],
            'pid':item['permissions__pid_id'],
            'p_title':item['permission__pid__title'],
            'p_url':item['permission__pid__url'],
        }
    )
    menu_id = item['permission__menu_id']
    if not menu_id:
        continue
    node = {'title':item['permission__title'], 'url':item['permission__url']}
    if menu_id in menu_dict:
        menu_dict[menu_id]['sub_menu'].append(node)
    else:
        menu_dict[menu_id] = {
            'title':item['permission__menu__title'],
            'icon':item['permission__menu__icon'],
            'sub_menu':[node,]
        }
        
request.session['luffy_permission_url_list'] = permission_list
request.session['luffy_permission_menu_key'] = menu_list

10.2 中间件

current_url = request.path_info
        for valid_url in valid_url_list:
            if re.match(valid_url, current_url):
                # 白名单中的URL无需验证即可访问
                return None
        permission_list = request.session.get('luffy_permission_url_list')
        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':'url','class':'active'}])
                else:
                    url_record.extend([
                        {'title':item['p_title'],'url':'p_url'},
                        {'title':item['title'],'url':'url','class':'active'},
                    ])
                request.breadcrumb = url_record
                break
        if not flag:
            return HttpResponse('无权访问')

10.3 创建 inclusion_tag

# 目录位置(templatetags/rbac.py)
@register.inclusion_tag('rbac/breadcrumb.html')
def breadcrumb(request):
    return {'record_list':request.breadc}

10.4 breadcrumb.html

<!-- 实现最终样式,页面中导航栏的当前页面显示灰色-->
<div>
    <ol class="breadcrumb no-radius no-margin" style="boder-bottom: 1px solid #ddd;">
        {% for item in record_list %} 
            {% if item.class %} <!-- 判断是否具有class属性-->
                <li class="active">{{ item.title }}</li>
            {% else %}
                <li><a href="{{ item.url }}">{{ item.title }}</a></li>
            {% endif %}
        {% endfor %}
    </ol>
</div>

 

 

11.权限粒度控制到按钮级别

1. 为 "setting.py" 中(class perssion)添加 "name" 字段;
2. 在初始化方法中将原来的列表 "permission_list" 变为字典 "permission_dict",并将第一步中新添加的 "name" 字段作为新字典的 key 
# name作为新字典的键可以在稍后在前端判断是否拥有权限时直接用 "{% if "name" in permission_dict%}....
3. 在 "templatetags/rbac.py" 中用 filter 写上一个函数:
    @register.filter
    def has_permission(request,name):
        # filter最多只有2个参数,且可以作为前端if判断的条件,simple_tag, inclusion_tag都不可以
        if name in request.session[setting.PERMISSION_SESSION_KEY]:
            return Ture
        
4. 前端调用 filter 
        # {% if 参数1|函数名:"参数2"%}
        {% if request|has_permission:"customer_add "%}

总结 ​权限控制 ​动态菜单 ​权限的分配

问题:以前你是如何做权限分配的?给某个用户分配一个角色?某个人分配某个权限。 ​答案:django admin进行录入或直接在数据库中进行更改

 

12.权限分配(重点)

12.1 角色管理

知识点:

12.1.1 ModelForm组件
from django.forms import ModelForm
​
class StudentList(ModelForm):
    class Meta:
        model = models.Student  #对应的Model中的类
        fields = "__all__"      #字段,如果是__all__,就是表示列出所有的字段
        exclude = None          #排除的字段
        labels = None           #提示信息
        help_texts = None       #帮助提示信息
        widgets = None          #自定义插件
        error_messages = None   #自定义错误信息
#error_messages用法:
        error_messages = {
            'name':{'required':"用户名不能为空",},
            'age':{'required':"年龄不能为空",},
        }
​
#widgets用法,比如把输入用户名的input框给为Textarea
#首先得导入模块
        from django.forms import widgets as wid  #因为重名,所以起个别名
        widgets = {
            "name":wid.Textarea(attrs={"class":"c1"}) #还可以自定义属性
        }
#labels,自定义在前端显示的名字
​
    labels= {
            "name":"用户名"
        }

 

12.1.2 反向解析

根据 namespace 和 name 反向生成url;

from django.conf.urls import url, include
from django.contrib import admin
​
# 根路径包含子路径分发可通过namespace和name反向生成url
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^rbac/', include('rbac.urls',namespace='rbac')),
    url(r'^', include('web.urls')),
]
# 子路径
urlpatterns = [
    url(r'^role/list/$', role.role_list, name='role_list'),  # rbac:role_list
    url(r'^role/add/$', role.role_add, name='role_add'),  # rbac:role_add
]
  • 模板的查找顺序

实现思路:

-- 角色添加(modelform,路由-反向解析) -- 角色修改(思考:是否可以和角色添加共用一个模板页面;Tips:使用modelform实现修改) -- 角色删除 -- 角色删除提示(1个删除模板页面,2个按钮【取消(模板写变量返回);确认(本质上为submit按钮)】)

 

12.2 用户管理

知识点:

12.2.1 ModelForm
  • 字段的自定制 (在ModelForm中加入原model函数没有的字段)

  • 钩子方法(用于检测两次密码输入是否一致)

    def clean_confirm_password(self):
        password = self.cleaned_data['password']
        comfirm_password = self.cleaned_data['comfirm_password']
        if password != comfirm_password:
            raise ValidationError('两次密码输入不一致')
            return comfirm_password

    错误提示:

    自定义错误提示(error_messages())

    • 设置错误信息中文显示:在setting中直接修改(LANGUAGE_CODE = 'zh-hans')

    • 重写init方法,统一给所有字段添加属性(form-control)

示例代码:

# 字段的自定制 (在ModelForm中加入原model函数没有的字段)
# 钩子方法
def clean_confirm_password(self):
    password = self.cleaned_data['password']
    comfirm_password = self.cleaned_data['comfirm_password']
    if password != comfirm_password:
        raise ValidationError('两次密码输入不一致')
    return comfirm_password 
​

 

 
12.3 菜单和权限管理

知识点:

  • 一级菜单

    • 保留url中的原搜索条件(重点)

    • 模板中整形转换成字符串 1|safe

    • ModelForm定制radio

  • 二级菜单

    • ModelForm 显示默认值

    • ModelForm save之前对其instance进行修改

    • BootStrapModelForm基类实现统一的样式定制(ModelForm的 init方法)

 

12.3.1 一级菜单
  • 展示:

    • 搭建页面(orm拿到数据库中菜单数据,在前端展示)

    • 加载样式

    • 在继承的基类模板中使用 {% block CSS%}在展示页面中添加此页面自己的样式

    • 注意:在判断是否添加 class:active 时保证数据类型一致 (后端拿到的GET请求中的mid数据为字符串类型,可在前端写 row.id|safe转为字符串)

    • 生成带有原搜索条件的url

    • 效果:点击一级菜单中新增、编辑、删除进行页面跳转时保留原始页面的搜索结果,在操作完成跳转回原页面的时 候将之前的搜索结果与原页面url进行拼接,实现在跳转过程中保留原页面搜索结果的效果

  • 知识点:

    • simpletag

    • QueryDict

    • 将搜索条件全部打包成一个: query_dict = QueryDict(mutable=True) query_dict['_filter'] = request.GET.urlencode()

    • reverse

示例代码:

from django.http import QueryDict
                    
@register.simple_tag
def memory_url(request,name,*args,**kwargs):
    # 跳转到操作页面时生成带有原搜索条件的url(替代了模板中的url)
    basic_url = reverse(name,args=args,kwargs=kwargs)
​
    #当前URL中无参数
    if not request.GET:
        return basic_url
​
    query_dict = QueryDict(mutale=True)
    query_dict['_filter'] = request.GET.urlencode()
​
    return "%s?%s" %(basic_url,query_dict.urlencode())
​
def memory_reverse(request,name,*args,**kwargs):
    # 操作完成后跳转回原页面时反向生成url
    url = reverse(name,args=args,kwargs=kwargs)
    origin_params = request.GET.get('_filter')
    if origin_params:
        url = "%s?%s" %(url,origin_params,)
​
    return url
12.3.2 二级菜单
  • 展示(和一级菜单相同)

  • 增删改(和一级菜单相同)

12.3.3 三级菜单
  • 展示(和一级菜单相同)

  • 增删改(和一级菜单相同)

 

 
12.4 批量的权限操作

Formset

  • 什么是formset,应用场景是什么?

  • 答:Form组件或ModelForm组件用于做一个表单验证,formset用于做多个表单的验证,可用于批量操作

自动发现项目中的url

  • 给你一个项目,请帮我获取当前项目中都有哪些url以及name rbac:permission_list

  • 实现思路

权限的批量操作

  • 唯一约束错误信息

知识点:

12.4.1 formset
  • 用以批量校验用户提交的数据(里面都是form对象)

12.4.2 自动发现项目中的url
  • 1.从settings字符串导入根路由

  • 2.递归获取所有路由

    • 判断是否路由分发:URLResolver(是,表示还有下层),URLPattern(否,表示已找到url)

    • 是路由分发:将pre_namespace和namespace拼接起来作为参数传入递归调用的recursion_urls()中

    • 不是路由分发:先拼接别名,后拼接url(pre_url + item._regex)后替换正则符号,存储到字典里;

  • 3.返回字典(此时已收集到项目中所有的url)

# 自动获取所有的url
# 函数执行顺序:get_all_url_dict() => recursion_urls() => check_url_exclude()
#             项目执行函数递归调用      逐级获取url           判断排除一些url
import re
from collections import OrderedDict
from django.conf import settings
from django.utils.module_loading import import_string
from django.urls import URLPattern, URLResolver
​
​
def check_url_exclude(url):
    """
    排除一些特定的URL(获取到除名单外的url)
    :param url:
    :return:
    """
    """
    AUTO_DISCOVER_EXCLUDE = [
    '/admin/.*'
]
    """
    for regex in settings.AUTO_DISCOVER_EXCLUDE:
        if re.match(regex, url):
            return True
​
​
def recursion_urls(pre_namespace, pre_url, urlpatterns, url_ordered_dict):
    """
    递归的去获取URL
    :param pre_namespace: namespace前缀,以后用户拼接name
    :param pre_url: url前缀,以后用于拼接url
    :param urlpatterns: 路由关系列表
    :param url_ordered_dict: 用于保存递归中获取的所有路由
    :return:
    """
    for item in urlpatterns:
        """
        urlpatterns = [
            re_path(r'^admin/', admin.site.urls),  # URLResolver表示为路由,还有下层
            re_path(r'^rbac/', include(('rbac.urls', 'rbac'), namespace='rbac')),  # URLResolver表示为路由,还有下层
            re_path(r'^', include('web.urls')),  # URLResolver表示为路由,还有下层
            ]
        """
        if isinstance(item, URLPattern):  # 非路由分发,讲路由添加到url_ordered_dict
            if not item.name:
                continue
            if pre_namespace:
                name = "%s:%s" % (pre_namespace, item.name)
            else:
                name = item.name
            # url处理
            url = pre_url + item._regex  # /rbac/user/edit/(?P<pk>\d+)/   | item._regex表示当前item的url
            url = url.replace('^', '').replace('$', '')  # 将拼接好的url去掉正则表达式的起始符(^),终止符($)
​
            if check_url_exclude(url):
                continue
​
            url_ordered_dict[name] = {'name': name, 'url': url}  # 将结果写到字典里
​
        elif isinstance(item, URLResolver):  # 路由分发,递归操作
            if pre_namespace:
                if item.namespace:
                    namespace = "%s:%s" % (pre_namespace, item.namespace,)
                else:
                    namespace = item.namespace
            else:
                if item.namespace:
                    namespace = item.namespace
                else:
                    namespace = None
            recursion_urls(namespace, pre_url + item.regex.pattern, item.url_patterns, url_ordered_dict)
​
​
def get_all_url_dict():
    """
    获取项目中所有的URL(必须有name别名)
    :return:
    """
    url_ordered_dict = OrderedDict()
​
    md = import_string(settings.ROOT_URLCONF)  # from luff.. import urls 从settings字符串导入根路由
    recursion_urls(None, '/', md.urlpatterns, url_ordered_dict)  # 递归去获取所有的路由
​
    return url_ordered_dict
12.4.3 权限的批量操作

思路:

获取项目中的所有权限;set1

去数据库中获取已经录入的权限;set2

  • 情况一:

    自动发现 > 数据库 ----> 实现批量添加 ps:通过name进行对比

    set1 - set2 ==> 添加

  • 情况二:

    数据库 > 自动发现 ----> 实现批量删除

    set2 - set1 ==> 删除

  • 情况三:

    数据库 = 自动发现 ----> 实现批量更新(权限数量相等,但权限不一致)

    set3 = set2 & set1 ==> 更新

def multi_permissions(request):
    """
    批量操作权限
    :param request:
    :return:
    """
    post_type = request.GET.get('type')
    generate_formset_class = formset_factory(MultiAddPermissionForm, extra=0)
    update_formset_class = formset_factory(MultiEditPermissionForm, extra=0)
​
    generate_formset = None
    update_formset = None
    if request.method == 'POST' and post_type == 'generate':
        # pass # 批量添加
        formset = generate_formset_class(data=request.POST)  # 实例化formset对象
        if formset.is_valid():
            object_list = []
            post_row_list = formset.cleaned_data
            has_error = False
            for i in range(0, formset.total_form_count()):
                row_dict = post_row_list[i]
                try:
                    new_object = models.Permission(**row_dict)
                    new_object.validate_unique()  # 验证唯一索引
                    object_list.append(new_object)
                except Exception as e:
                    formset.errors[i].update(e)
                    generate_formset = formset
                    has_error = True
            if not has_error:
                # 验证没有错误执行批量添加(orm bulk_create语句)
                models.Permission.objects.bulk_create(object_list, batch_size=100)
        else:
            generate_formset = formset
​
    if request.method == 'POST' and post_type == 'update':
        # pass  # 批量更新
        formset = update_formset_class(data=request.POST)
        if formset.is_valid():
            post_row_list = formset.cleaned_data
            for i in range(0, formset.total_form_count()):
                row_dict = post_row_list[i]
                permission_id = row_dict.pop('id')
                try:
                    row_object =
                    models.Permission.objects.filter(id=permission_id).first()
                    for k, v in row_dict.items():
                        setattr(row_object, k, v)
                    row_object.validate_unique()
                    row_object.save()
                except Exception as e:
                    formset.errors[i].update(e)
                    update_formset = formset
        else:
            update_formset = formset
​
    # 1. 获取项目中所有的URL&将url别名添加到集合中(自动发现)
    all_url_dict = get_all_url_dict()
    """
    {
        'rbac:role_list':{'name': 'rbac:role_list', 'url': '/rbac/role/list/'},
        'rbac:role_add':{'name': 'rbac:role_add', 'url': '/rbac/role/add/'},
        ....
    }
    """
    router_name_set = set(all_url_dict.keys())
​
    # 2. 获取数据库中所有的URL&将url别名添加到集合中(数据库)
    permissions = models.Permission.objects.all().values('id', 'title', 'name', 'url', 'menu_id', 'pid_id')
    permission_dict = OrderedDict()
    permission_name_set = set()
    for row in permissions:
        permission_dict[row['name']] = row
        permission_name_set.add(row['name'])
    """
    {
        'rbac:role_list': {'id':1,'title':'角色列表',name:'rbac:role_list',url.....},
        'rbac:role_add': {'id':1,'title':'添加角色',name:'rbac:role_add',url.....},
        ...
    }
    """
​
    for name, value in permission_dict.items():
        router_row_dict = all_url_dict.get(name)  # {'name': 'rbac:role_list', 'url': '/rbac/role/list/'},
        if not router_row_dict:  # 数据库有而自动发现没有
            continue  # (不做处理)
        if value['url'] != router_row_dict['url']:
            value['url'] = '路由和数据库中不一致'
​
    # 3. 应该添加、删除、修改的权限有哪些?
    # 3.1 计算出应该增加的name
    if not generate_formset:
        generate_name_list = router_name_set - permission_name_set  # 情况一
        generate_formset = generate_formset_class(
            initial=[row_dict for name, row_dict in all_url_dict.items() if name in generate_name_list])  # 将generate_name_list中的数据添加给row_dict
​
    # 3.2 计算出应该删除的name
    delete_name_list = permission_name_set - router_name_set
    delete_row_list = [row_dict for name, row_dict in permission_dict.items() if name in delete_name_list]  # 
​
    # 3.3 计算出应该更新的name
    if not update_formset:
        update_name_list = permission_name_set & router_name_set
        update_formset = update_formset_class(
            initial=[row_dict for name, row_dict in permission_dict.items() if name in update_name_list])
​
    return render(
        request,
        'rbac/multi_permissions.html',
        {
            'generate_formset': generate_formset,
            'delete_row_list': delete_row_list,
            'update_formset': update_formset,
        }
    )

 

 

12.5 权限分配
12.5.1 展示用户、角色、权限信息

 

  • 思路:

    权限分配部分需要构建一个拥有三层嵌套的数据格式,传给模板,模板通过三层for循环来实现整体的效果

# 小例子:
menu_list = [
    {'id': 1, 'title': '菜单1'},
    {'id': 2, 'title': '菜单2'},
    {'id': 3, 'title': '菜单3'},
]
​
menu_dict = {}
"""
{
    1:{'id': 1, 'title': '菜单1'},
    2:{'id': 2, 'title': '菜单2'},
    3:{'id': 3, 'title': '菜单3'},
}
"""
for item in menu_list:
    menu_dict[item['id']] = item
​
# menu_dict[2]['title'] = '666'
​
menu_dict[2]['children'] = [11,22]
​
print(menu_list)
# [{'id': 1, 'title': '菜单1'}, {'id': 2, 'title': '菜单2', 'children': [11, 22]}, {'id': 3, 'title': '菜单3'}]

menu_dict 字典 for 循环 menu_list,引用的是具有相同内存地址的字典,因此修改menu_dict 时,menu_list里的元素也会同步改变。

# 所有的菜单(一级菜单)
    all_menu_list = models.Menu.objects.values('id', 'title')
    """
    [
        {id:1,title:菜单1,children:[{id:1,title:x1, menu_id:1,'children':[{id:11,title:x2,pid:1},] },{id:2,title:x1, menu_id:1 },]},
        {id:2,title:菜单2,children:[{id:3,title:x1, menu_id:2 },{id:5,title:x1, menu_id:2 },]},
        {id:3,title:菜单3,children:[{id:4,title:x1, menu_id:3 },]},
    ]
    """
    all_menu_dict = {}
    """
       {
           1:{id:1,title:菜单1,children:[{id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },{id:2,title:x1, menu_id:1,children:[] },]},
           2:{id:2,title:菜单2,children:[{id:3,title:x1, menu_id:2,children:[] },{id:5,title:x1, menu_id:2,children:[] },]},
           3:{id:3,title:菜单3,children:[{id:4,title:x1, menu_id:3,children:[] },]},
       }
       """
    for item in all_menu_list:
        item['children'] = []
        all_menu_dict[item['id']] = item
​
    # 所有二级菜单
    all_second_menu_list = models.Permission.objects.filter(menu__isnull=False).values('id', 'title', 'menu_id')
​
    """
    [
        {id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },   
        {id:2,title:x1, menu_id:1,children:[] },
        {id:3,title:x1, menu_id:2,children:[] },
        {id:4,title:x1, menu_id:3,children:[] },
        {id:5,title:x1, menu_id:2,children:[] },
    ]
    """
    all_second_menu_dict = {}
    """
        {
            1:{id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },   
            2:{id:2,title:x1, menu_id:1,children:[] },
            3:{id:3,title:x1, menu_id:2,children:[] },
            4:{id:4,title:x1, menu_id:3,children:[] },
            5:{id:5,title:x1, menu_id:2,children:[] },
        }
        """
    for row in all_second_menu_list:
        row['children'] = []
        all_second_menu_dict[row['id']] = row
​
        menu_id = row['menu_id']
        all_menu_dict[menu_id]['children'].append(row)
​
    # 所有三级菜单(不能做菜单的权限)
    all_permission_list = models.Permission.objects.filter(menu__isnull=True).values('id', 'title', 'pid_id')
    """
    [
        {id:11,title:x2,pid:1},
        {id:12,title:x2,pid:1},
        {id:13,title:x2,pid:2},
        {id:14,title:x2,pid:3},
        {id:15,title:x2,pid:4},
        {id:16,title:x2,pid:5},
    ]
    """
    for row in all_permission_list:
        pid = row['pid_id']
        if not pid:
            continue
        all_second_menu_dict[pid]['children'].append(row)
​
    """
    [
        {
            id:1,
            title:'业务管理'
            children:[
                {
                    'id':11, 
                    title:'账单列表',
                    children:[
                        {'id':12,title:'添加账单'}
                    ]
                },
                {'id':11, title:'客户列表'},
            ]
        },
        
    ]
    """

模板语法:

三层循环来做出上图权限分配的三级效果

 

 

12.5.2 选择用户、角色时,页面上的默认选项
# 获取当前用户拥有的所有角色
    if user_id:
        user_has_roles = user_object.roles.all()
    else:
        user_has_roles = []
​
    user_has_roles_dict = {item.id: None for item in user_has_roles}
​
    # 获取当前用户用户用户的所有权限
​
    # 如果选中的角色,优先显示选中角色所拥有的权限
    # 如果没有选择角色,才显示用户所拥有的权限
    if role_object:  # 选择了角色
        user_has_permissions = role_object.permissions.all()
        user_has_permissions_dict = {item.id: None for item in user_has_permissions}
​
    elif user_object:  # 未选择角色,但选择了用户
        user_has_permissions = user_object.roles.filter(permissions__id__isnull=False).values('id',
                                                                                              'permissions').distinct()
        user_has_permissions_dict = {item['permissions']: None for item in user_has_permissions}
    else:
        user_has_permissions_dict = {}
​
    all_user_list = models.UserInfo.objects.all()
​
    all_role_list = models.Role.objects.all()
​
    menu_permission_list = []

模板:

角色信息展示:

 

12.5.3 角色和权限分配【保存按钮】

form表单的提交(模板可用input标签的submit,也可以用button按钮)

后端部分需要区分提交到后台的数据来源于哪个按钮,可以在模板部分用type加以区分

    user_id = request.GET.get('uid')
    user_object = models.UserInfo.objects.filter(id=user_id).first()
    if not user_object:
        user_id = None
​
    role_id = request.GET.get('rid')
    role_object = models.Role.objects.filter(id=role_id).first()
    if not role_object:
        role_id = None
    
    # 数据库更新
    if request.method == 'POST' and request.POST.get('type') == 'role': # 重点
        role_id_list = request.POST.getlist('roles')
        # 用户和角色关系添加到第三张表(关系表)
        if not user_object:
            return HttpResponse('请选择用户,然后再分配角色!')
        user_object.roles.set(role_id_list)
​
    if request.method == 'POST' and request.POST.get('type') == 'permission':
        permission_id_list = request.POST.getlist('permissions')
        if not role_object:
            return HttpResponse('请选择角色,然后再分配权限!')
        role_object.permissions.set(permission_id_list)

 

 

12.5.4 全选功能的实现

通过以下javascript代码实现:

<script type="text/javascript">
        $(function () {
            $('.check-all input:checkbox').change(function () {
                $(this).parents('.root').next().find(':checkbox').prop('checked',$(this).prop('checked'));
            })
        })
    
    </script>

 

权限分配完整代码:

def distribute_permissions(request):
    """
    权限分配
    :param request:
    :return:
    """
​
    user_id = request.GET.get('uid')
    user_object = models.UserInfo.objects.filter(id=user_id).first()
    if not user_object:
        user_id = None
​
    role_id = request.GET.get('rid')
    role_object = models.Role.objects.filter(id=role_id).first()
    if not role_object:
        role_id = None
​
    if request.method == 'POST' and request.POST.get('type') == 'role':
        role_id_list = request.POST.getlist('roles')
        # 用户和角色关系添加到第三张表(关系表)
        if not user_object:
            return HttpResponse('请选择用户,然后再分配角色!')
        user_object.roles.set(role_id_list)
​
    if request.method == 'POST' and request.POST.get('type') == 'permission':
        permission_id_list = request.POST.getlist('permissions')
        if not role_object:
            return HttpResponse('请选择角色,然后再分配权限!')
        role_object.permissions.set(permission_id_list)
​
    # 获取当前用户拥有的所有角色
    if user_id:
        user_has_roles = user_object.roles.all()
    else:
        user_has_roles = []
​
    user_has_roles_dict = {item.id: None for item in user_has_roles}
​
    # 获取当前用户用户用户的所有权限
​
    # 如果选中的角色,优先显示选中角色所拥有的权限
    # 如果没有选择角色,才显示用户所拥有的权限
    if role_object:  # 选择了角色
        user_has_permissions = role_object.permissions.all()
        user_has_permissions_dict = {item.id: None for item in user_has_permissions}
​
    elif user_object:  # 未选择角色,但选择了用户
        user_has_permissions = user_object.roles.filter(permissions__id__isnull=False).values('id',
                                                                                              'permissions').distinct()
        user_has_permissions_dict = {item['permissions']: None for item in user_has_permissions}
    else:
        user_has_permissions_dict = {}
​
    all_user_list = models.UserInfo.objects.all()
​
    all_role_list = models.Role.objects.all()
​
    menu_permission_list = []
​
    # 所有的菜单(一级菜单)
    all_menu_list = models.Menu.objects.values('id', 'title')
    """
    [
        {id:1,title:菜单1,children:[{id:1,title:x1, menu_id:1,'children':[{id:11,title:x2,pid:1},] },{id:2,title:x1, menu_id:1 },]},
        {id:2,title:菜单2,children:[{id:3,title:x1, menu_id:2 },{id:5,title:x1, menu_id:2 },]},
        {id:3,title:菜单3,children:[{id:4,title:x1, menu_id:3 },]},
    ]
    """
    all_menu_dict = {}
    """
       {
           1:{id:1,title:菜单1,children:[{id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },{id:2,title:x1, menu_id:1,children:[] },]},
           2:{id:2,title:菜单2,children:[{id:3,title:x1, menu_id:2,children:[] },{id:5,title:x1, menu_id:2,children:[] },]},
           3:{id:3,title:菜单3,children:[{id:4,title:x1, menu_id:3,children:[] },]},
       }
       """
    for item in all_menu_list:
        item['children'] = []
        all_menu_dict[item['id']] = item
​
    # 所有二级菜单
    all_second_menu_list = models.Permission.objects.filter(menu__isnull=False).values('id', 'title', 'menu_id')
​
    """
    [
        {id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },   
        {id:2,title:x1, menu_id:1,children:[] },
        {id:3,title:x1, menu_id:2,children:[] },
        {id:4,title:x1, menu_id:3,children:[] },
        {id:5,title:x1, menu_id:2,children:[] },
    ]
    """
    all_second_menu_dict = {}
    """
        {
            1:{id:1,title:x1, menu_id:1,children:[{id:11,title:x2,pid:1},] },   
            2:{id:2,title:x1, menu_id:1,children:[] },
            3:{id:3,title:x1, menu_id:2,children:[] },
            4:{id:4,title:x1, menu_id:3,children:[] },
            5:{id:5,title:x1, menu_id:2,children:[] },
        }
        """
    for row in all_second_menu_list:
        row['children'] = []
        all_second_menu_dict[row['id']] = row
​
        menu_id = row['menu_id']
        all_menu_dict[menu_id]['children'].append(row)
​
    # 所有三级菜单(不能做菜单的权限)
    all_permission_list = models.Permission.objects.filter(menu__isnull=True).values('id', 'title', 'pid_id')
    """
    [
        {id:11,title:x2,pid:1},
        {id:12,title:x2,pid:1},
        {id:13,title:x2,pid:2},
        {id:14,title:x2,pid:3},
        {id:15,title:x2,pid:4},
        {id:16,title:x2,pid:5},
    ]
    """
    for row in all_permission_list:
        pid = row['pid_id']
        if not pid:
            continue
        all_second_menu_dict[pid]['children'].append(row)
​
    """
    [
        {
            id:1,
            title:'业务管理'
            children:[
                {
                    'id':11, 
                    title:'账单列表',
                    children:[
                        {'id':12,title:'添加账单'}
                    ]
                },
                {'id':11, title:'客户列表'},
            ]
        },
        
    ]
    """
​
    return render(
        request,
        'rbac/distribute_permissions.html',
        {
            'user_list': all_user_list,
            'role_list': all_role_list,
            'all_menu_list': all_menu_list,
            'user_id': user_id,
            'role_id': role_id,
            'user_has_roles_dict': user_has_roles_dict,
            'user_has_permissions_dict': user_has_permissions_dict,
        }
    )
​

 

posted @ 2022-04-03 16:24  楽仔  阅读(190)  评论(0)    收藏  举报