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:
-
客户管理
-
客户列表:/customer/list/
-
添加客户:/customer/add/
-
删除客户:/customer/list/(?P<cid>\d+)/
-
修改客户:/customer/edit/(?P<cid>\d+)/
-
批量导入:/customer/import/
-
下载模板:/customer/tpl/
-
-
账单管理
-
账单列表:/payment/list/
-
添加账单:/payment/add/
-
删除账单:/payment/del/(?P<pid>\d+)/
-
修改账单:/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. 用户登录和权限初始化拆分
-
将权限相关的功能放到rbac应用下,以便以后组件的使用;
-
可以将 session key 放入 settings.py 中方便后期统一修改;
-
将之前写在web应用中的中间件放入rbac.middlewares中,并改名为RbacMiddleware;
-
将中间件的白名单放到 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,
}
)

浙公网安备 33010602011771号