菜单和权限管理
在前面一章节博客中, 我们进行了基本的权限控制和动态菜单的实现.
细心的你应该可以发现, 完成这些功能的前提是 进行权限、用户、角色信息的录入!
当时我们是基于Django admin后台进行的信息录入. 这种方式不太友好. 接下来, 我们换一种方式完成权限分配相关的操作.
注: 为了方便进行编码,先在layout.html中注释掉 动态菜单和面包屑 的代码 + 注释掉 中间件的代码!!
- 角色管理
- 用户管理
- 菜单和权限管理
- 批量的权限操作
- 分配权限 ==> 专门用来处理 角色:用户:权限三者之间关系的,<外键> "因为篇幅原因,分配权限单独弄了一个md来写"
2
3
4
5
# 角色管理
此处对Role表的title字段进行处理, permissions外键字段在 后续的分配权限处 进行处理!!
应用场景: 就是纯粹的对一张单表CURD, 不涉及到外键、自定义字段啥的! So, 添加和删除都使用的是同一个Form表单!
(*≧ω≦) CURD都大同小异, 后续复杂的场景都是基于下面这个模版根据需求作的一些改变!! 慢慢悟,对比比较,很快就会明白的.
from django.shortcuts import render, redirect
from django.urls import reverse
from apps.rbac import models
from .form import RoleModelForm
def role_list(request):
"""角色列表"""
role_queryset = models.Role.objects.all()
return render(request, 'rbac/role/role_list.html', {'roles': role_queryset})
def role_add(request):
"""添加角色"""
if request.method == 'GET':
form = RoleModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = RoleModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:role_list'))
return render(request, 'rbac/change.html', {'form': form})
def role_edit(request, pk):
"""编辑角色"""
obj = models.Role.objects.filter(id=pk).first()
if not obj: # -- 保证不会人为的修改页面url中的id值!!
return render(request, 'rbac/500.html', {"msg": f"ID值为{pk}的角色不存在,请联系管理员!"})
if request.method == 'GET':
form = RoleModelForm(instance=obj) # -- 修改时显示初始数据!!
return render(request, 'rbac/change.html', {'form': form})
form = RoleModelForm(data=request.POST, instance=obj) # -- 前者用于校验,后者是确定修改哪条记录
if form.is_valid():
form.save()
return redirect(reverse('rbac:role_list'))
return render(request, 'rbac/change.html', {'form': form})
def role_del(request, pk):
"""删除角色"""
role_queryset = models.Role.objects.filter(id=pk)
if not role_queryset:
return render(request, 'rbac/500.html', {"msg": "该角色不存在,请联系管理员!"})
origin_url = reverse('rbac:role_list')
if request.method == 'GET':
return render(request, 'rbac/delete.html', {'cancel': origin_url})
role_queryset.delete()
return redirect(origin_url)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
效果动态如下:
具体的代码就不展示啦!就是普遍的Django CURD. 注意几个点:
■ 根据namcespace和name反向生成URL
- 根路由
path('rbac/', include('apps.rbac.urls', namespace="rbac")),
- app_rbac
path('role/', include('apps.rbac.views.role.routers')),
- role的routers
urlpatterns = [
path('list/', views.role_list, name='role_list'),
path('add/', views.role_add, name='role_add'),
path('edit/<int:pk>/', views.role_edit, name='role_edit'),
path('del/<int:pk>/', views.role_del, name='role_del'),
]
- 模版中 举个栗子
<a href="{% url 'rbac:role_edit' row.pk %}">
2
3
4
5
6
7
8
9
10
11
12
13
■ 添加和编辑模版代码的复用
细心的观察 添加和编辑的视图代码 返回给前端模版的数据 都是form对象!! "在模版中通过for循环进行渲染即可!!"
★ form对象中包含多少个字段对象,都是在各自的modelform中处理好了的.
So,添加和编辑的模版代码是一样的,我们命名为 change.html <在其他地方也可以进行复用>
PS: html中的label对应ORM表中字段的verbose_name的属性值
2
3
4
5
■ 删除模版代码的复用
删除的模版代码也可进行复用 特别注意的是:
1> "传入模版中的cancel值" 表明点击取消按钮后 返回"哪里"的列表页面! 示例中是返角色列表!!
2> 点击确认按钮后 使用的是表单的提交!form标签没写method,代表提交到当前url地址!
2
3
# 用户管理
UserInfo表的roles外键字段在 后续的分配权限处 进行处理!!
用户管理的CURD相对于角色管理的CURD, 复杂那么一丢丢. 涉及到了以下知识点!!
额外字段、定义插件的两种方式、重写__init__
方法、modelForm的继承、勾子函数、readonly+initial 、中文的错误提示设置.
先来看效果图.Hhh
用户新增页面 > UserModelForm.
用户编辑页面 > UpdateUserModelForm.
密码重置页面 > ResetPasswordUserModelForm.
■ 用户列表页面 - 与角色管理中角色列表差不多
◎ UserInfo表字段 name、password、email、roles
- 在视图函数里返回前端模版的是 用户对象,该用户对象虽包含了该用户的所有的信息<eg:包括用户密码>
但最终渲染到模版中的信息有哪些,模版中是可以把控的!! -- 展示字段 用户名、邮箱
- 模版中可通过a标签跳转其他功能 新增用户、重置密码、操作<编辑/删除>
2
3
4
■ 用户新增页面+重置密码页面. readonly+initial
◎ UserInfo表字段 name、password、email、roles
- 用户新增页面 > UserModelForm
- 展示字段 name、email、password + 额外字段confirm_password
需在勾子函数中验证 新增用户的密码和确认密码是否一致
- 定制插件的方式 保证了 在页面中输入密码和确认密码时 输入密码时是隐式的.
- 密码重置页面 > ResetPasswordUserModelForm
- 展示字段 name、password + 额外字段confirm_password
需在勾子函数中验证 新增用户的密码和确认密码是否一致
- 在重置密码的页面, 用户名字段是不能编辑的. - 用 readonly 和 initial 来实现的!! ★★★
- 定制插件的方式 保证了 在页面中输入密码和确认密码时 是以小黑点的样子隐藏了.
▲ 注意:若使用的是用 disabled 和 initial. 因为disabled是不提交其值的,所以后台在字段验证时,会字段报错,该值必填..
ps: 上述程序有漏洞哦!我可以在浏览器右键检查,改用户名的值,Hhhh
2
3
4
5
6
7
8
9
10
11
12
13
14
■ 用户编辑页面
▲ 用户编辑页面 > UpdateUserModelForm
- 展示字段 name、email
2
■ 删除用户功能 与 角色管理中的删除一致
■ 关于错误提示信息的中文显示,两种方式
1)每个字段用插件自定义 error_messages={"required": "用户名不能为空!"},
2)一劳永逸 在settings.py配置文件中
LANGUAGE_CODE = 'en-us' 改为 LANGUAGE_CODE = 'zh-hans'
2
3
# 菜单和权限管理
(´・Д・)」三级联动、保留URL中的原搜索条件、ModelFrom中定制radio、ModelFrom 在save之前对其instance进行修改
# 三级联动
用一个路由/一个视图函数 实现一级菜单列表+二级菜单列表+非菜单权限列表 属实是三级联动了Hhh
一级菜单<处理的是Menu表> -- 二级菜单<处理的Premission表中能当菜单的权限> -- 非菜单权限<处理的Premission表中不能当菜单的权限>.
通过传递给模版first_menu_id、second_menu_id 实现选中的样式.以及新增按钮是否展示.
通过 url的mid / first_menu_id获取到二级菜单的所有数据、通过url的sid / second_menu_id获取到非菜单权限的所有数据.
敲黑板!! ?mid=2 后端接收到的是str类型 {row.id|safe} safe将row.id 整型变成了字符串 ★★★
■ http://127.0.0.1:8000/rbac/menu/list/
当访问该网址的时候, 执行截图中"绿色"标注的部分, 获得一级菜单的数据, 传递给模版, 在模版中进行渲染!!
此时, 二三级的数据是没有的!! (一级菜单没选的话,二级菜单是没有数据的,二级菜单面板上也不该有新增按钮)
■ http://127.0.0.1:8000/rbac/menu/list/?mid=1
--> mid表明用户选择的一级菜单.
当访问该网址的时候, 执行截图中"红色"标注的部分!!
简单来说, <a href="?mid={/{row.id}/}">
通过url的mid这个param实现了 一级二级的联动.
- 实现了一级菜单选中的样式
后端获取url参数,first_menu_id = request.GET.get("mid")
, 将该参数传递到模版中!!
在模版中<tr class="{% if row.id|safe == first_menu_id %}dc_menu_active{% endif %}">
- 获取了二级菜单的数据.
这里很有意思! 我们不能直接通过models.Permission.objects.filter(menu__id=first_menu_id)
来获取!!
因为URL中没有mid参数的话,request.GET.get("mid")
的结果是None.
根据Premission表的结构, 可知:models.Permission.objects.filter(menu__id=None)
得到的是 所有非菜单权限!!
这不是我们想要的, 所以进行截图中那样的判断! - first_menu_id 这个变量在模版中还有一个妙用!! 一级菜单没选择,二级菜单的面板上不应该有"新增"的按钮!!
Q: 需要考虑的是, 用户可以通过手动给url添加mid参数来使二级面板上出现新增按钮, 如何解决呢?
只需把截图的视图函数中注释的部分打开! 简单说就是在db中看存在不存在!!(之所以注释掉了,是因为每次都要查一下, 消耗性能
■ http://127.0.0.1:8000/rbac/menu/list/?mid=1&sid=1
--> mid表明用户选择的一级菜单.sid表明用户选择的二级菜单!!
二级三级的联动同理, 通过sid实现了二级菜单的选中样式+获取了三级面板的数据+三级面板"新增"按钮是否展示!!
具体的就不再赘述!! 但特别提醒的是: <a href="?mid={/{first_menu_id}/}&sid={/{row.id}/}">
三级联动,模版中其他需要注意的地方.
◆ 二级可作菜单的权限面板中
- 为了避免 url 太长 导致二级菜单的表格的样式出现问题 合并单元格
将CODE列和URL列放到了同一列 Hhhh -- rowspan="2"占两行;colspan="2"占两列;style="border-bottom: 0"
- 二级菜单选中的样式
<a href="?mid={{ choice_menu_id }}&sid={{ row.id }}">{{ row.title }}</a>
同样的套路,后端接收到url的sid这个param,再传给前端模版,模版中进行比对,对的上则加上选中样式
<tr class="{% if row.id|safe == second_menu_id %}dc_menu_active{% endif %}"> 两个tr都要判断哦!
◆ 非菜单权限就不需要默认选中啦,展示即可
2
3
4
5
6
7
8
9
# 保留URL原搜索条件
就该示例而言: 当我们完成一些功能(eg:新增、编辑、删除)后, 返回原来的菜单列表页面, 页面上的菜单选中的样式依旧存在!!
★★★ 换个说法,点击新增、删除、编辑按钮生成的url,携带当前url中的参数
这还是蛮重要的细节<保留原搜索条件的功能 Django Admin的源码就是这么实现的> (它也可以应用到角色和用户管理里面!!)
Q: 新生成的url中产生的url参数可能与原搜索条件的url参数可能会产生冲突/混乱.
A: 解决方案: QueryDict - &被转义了 等于"="符号也被转义
当前url地址 http://127.0.0.1:8000/rbac/menu/list/?mid=9&sid=25
mid、sid选中了一级二级菜单.
首先要知道当前访问的url地址对应的Django模版中, 还有修改、删除等按钮..
就删除一级菜单的按钮而言, 此时已经自动渲染成了 href="/rbac/menu/del/9/?_filter=mid%3D9%26sid%3D25"
当我们点击一级菜单的删除按钮后, 在对应的视图函数里, 取出 删除url中的参数 拼接到要删除后需跳转的地址上!!
# ModelFrom定制radio
在ModelForm中定制 radio输入框 实现图标列表!! Ahh
Ps: 在前面我们设计一级菜单表的时候, 该表中的menu图标字段是可为空的, 现在应该改为不为空!!
make_safe 表明不是以html代码显示, html渲染后显示,它是安全的
# ModelFrom显示默认值
通过 first_menu_id 和 initial 来实现
一级、二级、三级面板的数据列表展示在 "三级联动" 那已经实现啦!
一级菜单的CUD, 该说的 定制radio、保留URL原搜索条件(这个可复用) 都已经阐述了, 其余的不再赘述. 自己看代码都看得懂.
下面我们先来瞅瞅二级菜单的CUD, 值得关注的地方就是.
1> 新增二级菜单的时候 ModelFrom显示默认值!! 需传代表 用户选择的一级菜单的参数 first_menu_id!!
2> 修改和删除跟前面的CURD的模版中一样,只需要传当前二级菜单表格中某一条数据的pk就行,Hhh
Ps: self.fields['menu'].empty_label = "请选择一级菜单"
会将 "------"变成了"请选择一级菜单"
★ 思考一个问题!!
♀ 思考下:
假比 一级菜单我们选择的是 权限管理, 其ID值是9!!
模版中href="{% memory_url request 'rbac:menu_second_add' first_menu_id=first_menu_id %}"经过渲染后的地址是
http://127.0.0.1:8000/rbac/menu/second/add/9/?_filter=mid%3D9
So,点击二级面板中的新增,跳转的是该地址!!
「知道为啥渲染后是这个地址吗?因为
path('second/add/<int:first_menu_id>/', views.menu_second_add, name='menu_second_add'),」
该地址会在浏览器的URL栏显示,用户可以人为的修改 url地址中的 数字9 <该数字代表用户选择的一级菜单>
假比将 9改为了 900, ID为900的一级菜单是不存在的!!
代入代码中 initial={'menu':first_menu_obj} 实验效果相当于失效了. 页面上 显示的是 "---------",当然用户可在页面上自己选.
综上,我们不用做任何额外的处理!!(eg:在initial之前判断first_menu_id是否存在 是不用做的!!)
2
3
4
5
6
7
8
9
10
11
12
13
补充!! 严谨点,在二级菜单新增时,menu字段不应该为空,Hhh
在Permission表中 menu字段是可以为空的 , 因为要兼顾非菜单权限,非菜单权限时pid有值,menu没值
但在二级面板中 针对可做菜单权限的增加. 因为是可做菜单的权限,所以menu应该有值,pid没值!!
综上,严谨点,用户选择 "---------" 后端校验应该通过不了!! 怎么做呢?需要改一下ModelForm!!
class BasicModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(BasicModelForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'layui-input'
field.widget.attrs['autocomplete'] = "off"
self.fields['menu'].empty_label = "请选择一级菜单" # !!!(¯﹃¯)增加用户体验
self.fields['menu'].required = True # !!!(¯﹃¯)字段校验不能为空. 数据库中的该字段是可以为空的.相当于覆盖啦.
class SecondMenuModelForm(BasicModelForm):
"""添加修改二级菜单时使用的"""
class Meta:
model = models.Permission
exclude = ["pid"]
■ 再想多一点,想让把这个菜单权限变成非菜单权限呢? 去批量处理的页面进行操作!!那里可以进行更新.Hhh
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# save前修改instance
实现 非菜单权限的新增, 有两个方案!! 此示例我们选择用方案二 在save前修改instance来实现!!
目前, 一级和二级面板中的CURD+三级面板中的R 我们都已经完成. 下面我们来瞅瞅 三级面板中的 CUD!!
★ 思考一个问题!!
针对二级面板中 可做菜单权限的增加! 我们想的是 二级菜单归属的一级菜单可能会经常改变,所以可以让用户自己选!!
那么三级面板中 非菜单权限的增加. 也这样吗? No!
我们发现 非菜单权限一般都是新增、修改、删除,其归属的二级菜单一般不会改变!!
★ 所以 我们规定 新增的非菜单权限的归属必定是当前选中的二级菜单!!!!
非菜单权限的新增 如何实现呢?
◎ Permission表已有字段 title、url、menu、pid、name
首先要明确menu字段是用不到的!!因为其在db中可为空,所以不使用该字段,在save时会自动赋值Null
- 方案一
使用字段 title、url、pid、name
之所以 使用了pid 是想让当前新增的非菜单权限归属的二级菜单给用户进行展示!!
"该pid需要像可做菜单权限的新增一样 这里是通过second_menu_id传递!!"
path('permission/add/<int:second_menu_id>/', views.permission_add, name='permission_add'),
但默认展示的该字段用户改不了,但会表单提交到后端!! 技术要点: readonly+initial "(´・Д・)」参考-用户管理处的重置密码页面!!"
- 方案二 (该示例我们采用它!!)
使用字段 title、url、name
path('permission/add/<int:second_menu_id>/', views.permission_add, name='permission_add'),
后端获取到second_menu_id后,就知道用户选择的二级菜单是哪个了!!
(严谨性,需要验证该值背后的对象是否存在!! "与可做菜单权限的新增的代码进行对比,就很容易知道怎么回事啦!")
接下来重点在于!
在保存之前往instance里加点东西!! ★★★
form.instance.pid = second_menu_obj
form.save()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Ps: 非菜单权限的编辑和删除类似!
# formset的使用
■ formset是什么?
Form组件或ModelForm组件用于做"一个"表单的验证, formset用于做"多个"表单的验证
表格里每一行都是一个表单,多个行由formset组合在了一起,批量的进行了验证■ 应用场景: 批量操作
为啥不用modelformset_factory呢??? 我试着捣鼓了下.. 有些技术问题 Hhh不想捣鼓了.
武sir也说, ModelFormSet在该rbac项目中不适合..(¯﹃¯)
# formset的批量添加
(´・Д・)」实现批量添加/增加的功能,并且有了"唯一"的异常也能捕获的
关键代码如下:
因为截图中空白区域有限, 在这里补充两个细节!!
■ formset.is_valid()
- 注意哦,因为使用的是forms.Form,所以是根据我们在表单中自己定义的字段进行的验证哦!
- "Don't ask why?!" 实验结果就是这样的!! (¯﹃¯)想要知道为啥就去研究formset的源码,Hhh
1. 表格中的两行 都不填.内部不会做验证, 直接让你通过
formset.cleaned_data ==> [{}, {}]
2. 第一行数据哪怕写了一个,都会对这一行数据进行表单验证; 第二行啥都不写,是不会对这一行数据进行表单验证的,直接通过
formset.cleaned_data ==> [{'title': '11', 'url': '111', 'name': '111', 'menu_id': '', 'pid_id': ''}, {}]
■ post_row_list = formset.cleaned_data
要注意一个细节,当formset里没有错误信息的时,执行formset.cleaned_data才不会报错
若检查formset中没有错误信息,才能将用户提交的数据获取到!! 每次执行formset.cleaned_data该语句,都会检查
因为后续可能会人为的往formset添加错误 So,进行了一个赋值!!
'\(≧▽≦)/' formset_class = formset_factory(MultiPermissionForm, extra=2)
Q: 在添加的时候/更新的时候,默认的显示几行初始数据,如何实现?/若extra=0的话,不会额外生成,那么个数如何指定呢?
A: formset_class()实例化时使用initial参数!传入[{},{}],像这样就会对应两条数据 暂略.
<在后面权限的批量添加一级批量更新代码中,发送GET请求时,就是这样弄的!Hhh>
(´・Д・)」 Ps:你对比下角色管理那CURD的模版,会有不一样的feel哦!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# formset的批量更新
(´・Д・)」实现批量更新, 相比于添加, Form中多了一个ID字段, 因为需根据这个ID字段进行更新!!
关键代码如下:
# 自动发现项目中URL
给你一个项目, 请帮我获取当前项目中都有哪些URL以及name!!
注意哦, name是用于反向生成url,所以有namespace的话,需要进行namespace的拼接!
在任何一个视图函数中执行 下面的get_all_url_dict方法就可获取项目中的所有路由!!
构建的数据结构: eg: {"rbac:menu_list":{"name":"rbac:menu_list","url":"/rbac/menu/list/"},...,...}
代码中的批注已经很清晰啦, 不再过多阐述.ψ(`∇´)ψ
from collections import OrderedDict # 有序字典
from django.conf import settings
from django.utils.module_loading import import_string # 根据字符串的形式导入该模块
from django.urls.resolvers import URLResolver, URLPattern
import re
def check_url_exclude(url):
"""自定制 排除一些特定的URL"""
"""
AUTO_DISCOVER_EXCLUDE = [
'/admin/.*',
'/rbac/login/',
]
"""
for regex in settings.AUTO_DISCOVER_EXCLUDE:
if re.match(regex, url):
return True
def recursion_url(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:
""" eg: 根路由下的urlpatterns循环出来的item
<URLResolver <URLPattern list> (admin:admin) 'admin/'>
<URLResolver <module 'apps.rbac.urls' from '/Desktop/dc_crm/apps/rbac/urls.py'> (rbac:rbac) 'rbac/'>
<URLResolver <module 'apps.web.urls' from '/Desktop/dc_crm/apps/web/urls.py'> (None:None) 'web/'>
<URLPattern 'xxx/'>
- Django1.x中是RegexURLResolver、RegexURLPattern
-- from django.urls import RegexURLResolver, RegexURLPattern
- Django2.x开始是URLResolver、URLPattern
-- from django.urls.resolvers import URLResolver, URLPattern
"""
if isinstance(item, URLResolver): # 证明是路由分发,那么需要进行递归操作!!
# ■ 该语句体里表明,是进行路由分发的路由,该路由肯定不具备name值的,所以只能先对namespace进行处理!!
# [前面n级路由分发拼接的namespace] [当前的路由分发的namespace] 有 无 -- 排列组合,一共四种情况 Hhh
# 情况1: 当前路由分发有namespace和前面的n级路由分发经过迭代后拼接的namespace 都有,再次进行拼接
if pre_namespace and item.namespace:
namespace = f"{pre_namespace}:{item.namespace}"
# 情况2: 前面的n级路由分发都没有namespace,当前路由分发有
elif not pre_namespace and item.namespace:
namespace = item.namespace
# 情况3: 前面的n级路由分发经过迭代后拼接的namespace 有, 当前路由分发没有
elif pre_namespace and not item.namespace:
namespace = pre_namespace
else: # 情况4: 所有的路由分发都没有namespace
namespace = None
"""
- Django1.x中这么写
recursion_urls(namespace, pre_url + item.regex.pattern, item.url_patterns, url_ordered_dict)
- Django2以上不支持 item.regex
item.regex.pattern 应改为 item.pattern.regex.pattern
"""
# ■ url需要拼接当前路由的url前缀!! Ps: 你无需担心path中的<str:pk>之类的写法,它内部会自动转换为正则的样式!!
url = pre_url + item.pattern.regex.pattern
recursion_url(namespace, url, item.url_patterns, url_ordered_dict) # <★开始递归>
# <★★★其实它就是递归结束的条件,尽管执行它时,return的是None 也证明开始了回溯的过程!!>
elif isinstance(item, URLPattern): # 证明是非路由分发,那么需将路由添加到url_ordered_dict字典中!
if not item.name: # 该url没有name,那么就不管
continue
# 若该url的上一级进行了路由分发,分发时设置了namespace,那么需要将namespace与当前name进行拼接!
if pre_namespace:
name = "%s:%s" % (pre_namespace, item.name)
else:
name = item.name
# 同理,若该url的上一级进行了路由分发,分发时设置了url前缀,那么需要进行拼接! 前缀 + 当前url
# 仔细想一想,其实每个上一级都有url前缀,哪怕是根路由,我们也手动传入了 "/" 前缀
# item.pattern.regex.pattern 可取到当前路由的url, Don't ask me why, 武sir在源码中看到的!
url = pre_url + item.pattern.regex.pattern
# eg: /^rbac/^user/edit/(?P<pk>\d+)/$ --> /rbac/user/edit/(?P<pk>\d+)/
url = url.replace('^', '').replace('$', '')
# □ 排除的自定制!! 比如排除,admin为前缀的url、login登陆
if check_url_exclude(url):
continue
url_ordered_dict[name] = {"name": name, "url": url}
def get_all_url_dict():
"""获取项目中所有的<有name>的URL (约定前提:URL必须得有name别名该URL才能被自动发现!!)"""
# eg: {"rbac:menu_list":{"name":"rbac:menu_list","url":"/rbac/menu/list/"},...,...}
# 为啥要这么构建数据结构?
# 1.name值我们在数据库里设置了unique=True,具备唯一性,所有name值可以作为key
# 2.name还可以做粒度控制到按钮的权限判断
# So,我们约定俗成,项目中的url必须设置name,不然不会自动找到它!!
url_ordered_dict = OrderedDict()
# settings.ROOT_URLCONF 表明根路由的路径,我们需根据字符串的形式导入该模块
md = import_string(settings.ROOT_URLCONF) # 'dc_crm.urls' --> from dc_crm import urls
# [item for item in md.urlpatterns] # item ==> RegexURLResolver/URLResolver RegexURLPattern/URLPattern
# 调用它帮助我们递归的去获取所有路由 注:
# 1. 根路由是没有上一级的,也就不存在分发,所以一开始pre_namespace值为None
# 2. 给根路由的url前面都加上"/",所以一开始pre_url值为"/"
# 3. md.urlpatterns根路由的路由关系列表
recursion_url(None, "/", md.urlpatterns, url_ordered_dict)
return url_ordered_dict
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 权限的批量操作
在上面我们已经学会两个前置知识点: formset的使用 + 自动发现项目中的URL
该小节 我们来完成权限的批量操作!!
基本思路:
集合1 - 自动发现,获取项目中所有的URL -- name集合
集合2 - 获取数据库中已添加的所有的URL -- name集合
对比两个集合,整理出应该添加、删除、修改的权限有哪些?!
- 数据库URL > 项目发现的URL, 删除权限.
- 数据库URL < 项目发现的URL, 增加权限.
- 数据库URL & 项目发现的URL, 更新权限.
我就直接粘贴代码啦, 注释写得很清楚了!! (本来想截图的, 篇幅太长了, 截不好 ╮( ̄▽ ̄"")╭ 这样反而省事了
提醒几点:
■ 批量操作的页面有三个面板, 当第一个面板执行post请求, 提交有错误时, 会在第一个面板上显示错误.
但一定要注意此时第二个第三个面板中的数据也是要有的!! 我做了以下两个努力:
- 1. 我使用了Django的CBV的写法,让代码看起来更加清晰
- 2. 我封装了一个方法get_mutil_data, 获取需在页面上展示的数据,GET、POST请求都要执行它!!
你听起来可能云里雾里的,但相信我,你看代码就很容易明白啦!!
■ Form组件中,细品下这行代码 ★核心思想在于,menu和pid不能同时为空但有且只有一个有值!!
# 获得应该归属的二级菜单!!
self.fields['pid_id'].choices += models.Permission.objects.filter(pid__isnull=True) \
.exclude( menu__isnull=True).values_list('id', 'title')
# 其实还有种写法是等效的!!
self.fields['pid_id'].choices += models.Permission.objects.filter(menu__isnull=False) \
.exclude(pid__isnull=False).values_list('id', 'title')
■ 在批量添加的代码中,细品下批量进行添加,减轻数据库负担,就不用每次连接数据库进行增加.
此处结合new_object.validate_unique()的思考!!
■ 实现权限的批量增加,权限批量更新,一个个的删除
新增/更新
- "☆★★!!!"
<button type="submit">按钮 或者 <input type="submit">表单控件 都可以点击后通过form表单post请求进行提交
- 记得在前端form表单中写上 {% csrf_token %} {{ formset.management_form }}
- <form method="post" action="?type=generate">
<form method="post" action="?type=update">
■ 其它
- Layui弹层layer中select没CSS样式或渲染失效的解决方法
必须给表单体系所在的父元素加上 class="layui-form"
- Django中form组件的所有内置字段
https://blog.csdn.net/weixin_30835933/article/details/98952153
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
■ apps/rbac/views/menu/routers.py
from django.urls import path
from . import views
urlpatterns = [
path('list/', views.menu_list, name='menu_list'), # 一级菜单列表+二级菜单列表+非菜单权限列表
# 一级菜单的CUD
path('add/', views.menu_add, name='menu_add'),
path('edit/<int:pk>/', views.menu_edit, name='menu_edit'),
path('del/<int:pk>/', views.menu_del, name='menu_del'),
# 二级菜单的CUD
path('second/add/<int:first_menu_id>/', views.menu_second_add, name='menu_second_add'),
path('second/edit/<int:pk>/', views.menu_second_edit, name='menu_second_edit'),
path('second/del/<int:pk>/', views.menu_second_del, name='menu_second_del'),
# 非菜单权限的CUD
path('permission/add/<int:second_menu_id>/', views.permission_add, name='permission_add'),
path('permission/edit/<int:pk>/', views.permission_edit, name='permission_edit'),
path('permission/del/<int:pk>/', views.permission_del, name='permission_del'),
# 批量操作
# path('mutil/permissions/', views.multi_permissions, name='mutil_permissions'),
path('mutil/permissions/', views.MultiPermissionsView.as_view(), name='multi_permissions'),
path('multi/permissions/del/<int:pk>/', views.multi_permissions_del, name='multi_permissions_del'),
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
■ apps/rbac/views/menu/form.py
class MultiAddPermissionForm(forms.Form): # 特别注意哦!这里使用的是forms.Form
"""用于批量添加"""
title = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
url = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
name = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
menu_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': "layui-input"}),
required=False,
)
pid_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': "layui-input", "style": "width:10px"}),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['autocomplete'] = "off"
self.fields['menu_id'].choices += models.Menu.objects.values_list('id', 'title')
# 我们的默认是menu_id和pid_id一有值一无值,可能存在两个都无值,但不会存在两个都有值
# pid__isnull=True证明是二级菜单权限了,那么menu_id一定要有值
# So,这里有排除了menu__isnull=True 保证不会出现两个都没值的情况
# 两个都没值 - 应该是一开始添加的权限没进行分配的时候
self.fields['pid_id'].choices += models.Permission.objects.filter(pid__isnull=True).exclude(
menu__isnull=True).values_list('id', 'title')
class MultiEditPermissionForm(forms.Form):
"""用于批量更新"""
id = forms.IntegerField(
widget=forms.HiddenInput()
)
title = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
url = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
name = forms.CharField(
widget=forms.TextInput(attrs={'class': "layui-input"})
)
menu_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': "layui-input"}),
required=False,
)
pid_id = forms.ChoiceField(
choices=[(None, '-----')],
widget=forms.Select(attrs={'class': "layui-input"}),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['autocomplete'] = "off"
self.fields['menu_id'].choices += models.Menu.objects.values_list('id', 'title')
self.fields['pid_id'].choices += models.Permission.objects.filter(pid__isnull=True).exclude(
menu__isnull=True).values_list('id', 'title')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
■ apps/rbac/views/menu/views.py
class MultiPermissionsView(View):
generate_formset = None
update_formset = None
delete_row_list = []
def get_mutil_data(self):
"""获取需在页面上展示的数据,GET、POST请求都要执行它!Hhh"""
# step1: 自动发现,获取项目中所有的URL -- name集合
# eg: {"rbac:menu_list":{"name":"rbac:menu_list","url":"/rbac/menu/list/"},...,...}
all_url_dict = get_all_url_dict()
router_name_set = set(all_url_dict.keys()) # 获得路由对应的name 集合!
# step2: 获取数据库中已添加的所有的URL -- name集合
# permissions --> queryset对象[{},{}..]
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"])
# step3: 对比两个集合,整理出应该添加、删除、修改的权限有哪些?!
# 1> 计算出应该增加的name -- 程序中有db中没有,相减得应该往db中添加的 So,循环的是all_url_dict
if not self.generate_formset: # ◆ 若是POST请求,这个if函数体的代码就不用执行
generate_name_set = router_name_set - permission_name_set
generate_formset_class = formset_factory(MultiAddPermissionForm, extra=0)
# formset批量增加
self.generate_formset = generate_formset_class(initial=[
row_dict for name, row_dict in all_url_dict.items() if name in generate_name_set
])
# 2> 计算出应该更新的name -- db中有程序中也有,两者取完交集就是要更新的
if not self.update_formset: # ◆ 若是POST请求,这个if函数体的代码就不用执行
# Q:循环谁? A:循环permission_dict,因为db中还有menu_id,pid_id
# 思考:
# 需要更新的记录,name值肯定一样、url还需要判断是否一样
# 若不一样,应该以程序中的为准
# <这里我们让用户自行选择,因为程序中的可能因为手贱写错了,多打了一个字符>,不然项目的某些功能会出问题.
for name, value in permission_dict.items():
router_row_dict = all_url_dict.get(name)
# and第一个条件表明db中有程序中也有!! 若router_row_dict的bool值为False,表明db中有,程序中没有,这里直接不管
# if not router_row_dict:
# continue
if router_row_dict and value['url'] != router_row_dict['url']:
# permission_dict中对应记录的url字段的值进行了修改,用于提示用户,这个赋值并没有同步数据库哈Hhh
value['url'] = '路由和数据库中不一致'
update_name_list = router_name_set & permission_name_set
update_formset_class = formset_factory(MultiEditPermissionForm, extra=0)
# formset批量更新 ☆必须要有个隐藏的ID
self.update_formset = update_formset_class(initial=[
row_dict for name, row_dict in permission_dict.items() if name in update_name_list
])
# 3> 计算出应该删除的name -- db中有程序中没有,相减得应该在db中删除的 So,循环的是permission_dict
delete_name_set = permission_name_set - router_name_set
# 不会批量删除,一股脑的批量删除不好,因为可能是百度合作的地址之类的!!
self.delete_row_list = \
[row_dict for name, row_dict in permission_dict.items() if name in delete_name_set]
def get(self, request, *args, **kwargs):
self.get_mutil_data()
return render(request, 'rbac/menu/multi_permissions.html', {
'generate_formset': self.generate_formset, # formset批量增加 的数据
'delete_row_list': self.delete_row_list, # 删除的数据列表 的数据
'update_formset': self.update_formset, # formset批量更新 的数据 ☆必须要有个隐藏的ID
})
def post(self, request, *args, **kwargs):
post_type = request.GET.get('type')
generate_formset_class = formset_factory(MultiAddPermissionForm, extra=0)
update_formset_class = formset_factory(MultiEditPermissionForm, extra=0)
# 批量添加
if post_type == 'generate':
formset = generate_formset_class(data=request.POST)
# print(request.POST)
if formset.is_valid():
object_list = []
has_error = False
post_row_list = formset.cleaned_data # 通过表单验证成功的数据,但还需手动进行唯一索引验证!!
for i in range(0, formset.total_form_count()):
row_dict = post_row_list[i]
print(row_dict)
try:
new_object = models.Permission(**row_dict)
"""
不得不提的是,validate_unique是先看ORM表中哪个字段(eg:name字段)有唯一索引,
然后去db数据库中跟已有数据进行比对!!
这里,我们的代码逻辑是连接一次数据库,添加多条数据,
所以 新增的那些行数据如果有name值相同的,该行代码是检查不出来的.
但回过头来想想,name值是程序员自己设定的,设定重复了,就是自己蠢.
再者,你回顾自动发现url的代码逻辑,name作为字段的key值,发现了重复的name,直接就覆盖了!!
So,此处模版中 待新增表格中的name字段必不可能重复.
综合下来,只要不是人为的修改,添加到db中的name值也必不可能重复!!
>>> my_dict = {}
>>> my_dict["1"] = 2
>>> my_dict
{'1': 2}
>>> my_dict["1"] = 3
>>> my_dict
{'1': 3}
"""
new_object.validate_unique() # 未抛出异常,则唯一索引验证通过
object_list.append(new_object)
except Exception as e:
formset.errors[i].update(e) # 唯一索引的错误添加到formset中!!
self.generate_formset = formset # (¯﹃¯)
has_error = True
if not has_error:
# **批量进行添加,减轻数据库负担 就不用每次连接数据库进行增加 放心大胆的用!!
models.Permission.objects.bulk_create(object_list, batch_size=100)
else:
# is_valid()未验证通过
# 错误信息在formset里,赋值给了类实例对象的generate_formset,根据查找规则,会用它,而不会用类里的.
self.generate_formset = formset # (¯﹃¯)
# 批量更新
if post_type == 'update':
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') # 拿到那个隐藏的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() # 鬼知道为啥这里又一行行的save到db中.Hhh
except Exception as e:
formset.errors[i].update(e)
self.update_formset = formset
else:
self.update_formset = formset
self.get_mutil_data()
return render(request, 'rbac/menu/multi_permissions.html', {
'generate_formset': self.generate_formset,
'delete_row_list': self.delete_row_list,
'update_formset': self.update_formset,
})
def multi_permissions_del(request, pk):
"""批量页面的权限删除"""
url = memory_reverse(request, 'rbac:multi_permissions')
if request.method == 'GET':
return render(request, 'rbac/delete.html', {'cancel': url})
models.Permission.objects.filter(id=pk).delete()
return redirect(url)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147