Django - 权限(2)- 动态显示单级权限菜单
一、权限组件
1、上篇随笔中,我们只是设计好了权限控制的表结构,有三个模型,五张表,两个多对多关系,并且简单实现了对用户的权限控制,我们会发现那样写有一个问题,就是权限控制写死在了项目中,并且没有实现与我们的业务逻辑解耦,当其他项目要使用权限控制时,要再重复写一遍权限控制的代码,因此我们很有必要将权限控制的功能开发成一个组件(可插拔)。
组件其实就是一个包,将一个与功能相关的代码关联到一起,当其他项目要使用该功能时将组件导入即可,下面我们试着来将权限控制写成一个组件,以客户管理系统为例,利用权限控制组件,实现动态显示权限菜单的功能,目录结构如下:
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 ├── urls.py └── views.py
(1)rbac/models.py中代码(权限管理的模型类),如下:
from django.db import models class User(models.Model): """ 用户表 """ name = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=32) roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role') def __str__(self): return self.name
class Role(models.Model): """ 角色表 """ title = models.CharField(verbose_name='角色名称', max_length=32) permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission')
def __str__(self): return self.title
class Permission(models.Model): """ 权限表 """ url = models.CharField(verbose_name='含正则的URL', max_length=32) title = models.CharField(verbose_name='标题', max_length=32) is_menu = models.BooleanField(verbose_name='是否是菜单', default=False) icon = models.CharField(verbose_name='图标', max_length=32, blank=True)
def __str__(self): return self.title
(2)web/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)
def __str__(self): return self.name
class Payment(models.Model): """ 付费记录表 """ customer = models.ForeignKey(verbose_name='关联客户', to='Customer',on_delete=models.CASCADE) money = models.IntegerField(verbose_name='付费金额') create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)
(3)以上客户管理系统中的URL和对应的功能有(将url与视图函数对应关系配置在web下的urls.py中,再由全局的urls.py做路由分发):
客户管理:
客户列表:/customer/list/
添加客户:/customer/add/
删除客户:/customer/del/(\d+)/
修改客户:/customer/edit/(\d+)/
账单管理:
账单列表:/payment/list/
添加账单:/payment/add/
删除账单:/payment/del/(\d+)/
修改账单:/payment/edit/(\d+)/
(4)在权限组件表中录入相关信息:
录入权限列表,创建角色(并为角色分配权限),创建用户(并为用户分配角色);
这样用户登录时,就可以根据当前登录用户找到其所有权限再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。
上篇中我们已经完成这些,接下来将权限和项目解耦并且完成动态显示权限菜单功能。
二、动态显示权限菜单(单级菜单)
准备工作已经就绪,再按照以下步骤进行:
1、登录页面
在web目录下的urls.py中新增加一个url与登录视图函数的对应关系:
登录:/login/
在web目录下的views.py中创建登录的视图函数,代码如下;
from rbac.service.setsession import initial_session def login(request): if request.method == 'POST': user = request.POST.get('user') pwd = request.POST.get('pwd') user_obj = User.objects.filter(name=user, password=pwd).first() if user_obj: request.session['user_id'] = user_obj.pk # 用户id注入session # 将权限列表和权限菜单列表注入session initial_session(user_obj, request) return redirect('/customer/list/') return render(request, 'login.html')
然后在web目录下创建templates文件夹,并在其中创建login.html;
2、在rbac目录下创建service文件夹,并在其中创建middlewares.py和setsession.py
middlewares.py(利用中间件做用户登录和判断权限,中间件要加入全局settings中),代码如下:
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import redirect, HttpResponse import re class PermissionMiddleWare(MiddlewareMixin): def process_request(self, request): # 设置白名单放行 for reg in ["/login/", "/admin/*"]: ret = re.search(reg, request.path) if ret: return None # 检验是否登录 user_id = request.session.get('user_id') if not user_id: return redirect('/login/') # 检验权限 permission_list = request.session.get('permission_list') for reg in permission_list: reg = '^%s$' % reg ret = re.search(reg, request.path) if ret: return None return HttpResponse('无权访问')
setsession.py(将当前登录人的所有权限注入session中),代码如下:
def initial_session(user_obj, request): """ 将当前登录人的所有权限列表和所有菜单权限列表注入session :param user_obj: 当前登录用户对象 :param request: 请求对象HttpRequest """ # 查询当前登录人的所有权限列表 ret = Role.objects.filter(user=user_obj).values('permissions__url', 'permissions__title', 'permissions__icon', 'permissions__is_menu').distinct() permission_list = [] permission_menu_list = [] for item in ret: permission_list.append(item['permissions__url']) if item['permissions__is_menu']: permission_menu_list.append({ 'url': item['permissions__url'], 'title': item['permissions__title'], 'icon': item['permissions__icon'], }) print('权限列表', permission_list) print('菜单权限列表', permission_menu_list) # 将当前登录人的权限列表注入session中 request.session['permission_list'] = permission_list # 将当前登录人的菜单权限列表注入session中 request.session['permission_menu_list'] = permission_menu_list
3、客户列表和账单列表属于菜单列表,我们需要渲染到左侧菜单中,根据用户权限判断是否显示,而客户列表和账单列表的左侧菜单是在公共的模板base.html中定义的,我们只看左侧菜单的部分,如下代码:
<div class="menu-body"> <div class="static-menu"> {% for item in permission_menu_list %} <a href="{{ item.url }}"> <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }} </a> {% endfor %} </div> </div>
至此,我们已经实现了动态渲染标签的功能,也就是用户拥有的菜单权限会在页面左侧菜单中显示,没有权限的则不显示。但是还有一个小问题,鼠标滑过相应菜单有一个样式,但是鼠标移走样式消失。如何解决呢?你可能会想到在客户列表和账单列表对应的视图函数中分别加上这样一段逻辑代码:即当前视图函数对应url与用户请求url相同时,给从当前用户session中取出的当前用户菜单全列列表中的字典添加一个键值对(class:active)再传给返回的页面,这种方法是可以满足需求,但同时也存在代码重复的问题,就是在不同的视图函数中重复写了一样的逻辑代码。下面介绍的自定义标签的扩展就可以完美解决。
4、自定义标签扩展功能实现点击菜单增加相应active类名:
1)保证全局settings.py中的INSTALLED_APPS配置了当前应用rbac;
2)在rbac目录下创建templatetags模块(文件夹);
3)在templatetags中创建任意的.py文件(如:my_tags.py),然后就可以在里边写自定义的标签扩展的函数了,如下:
from django import template register = template.Library() @register.inclusion_tag("menu.html") # django会自动去templates中寻找 def get_menu_styles(request): permission_menu_list = request.session.get("permission_menu_list") for item in permission_menu_list: if re.search("^{}$".format(item["url"]), request.path): item["class"] = "active" return {"permission_menu_list": permission_menu_list} # 返回给menu.html
4)在rbac目录下创建templates文件夹,并在其中创建任意的.html文件(如:menu.html),该页面存放左侧菜单部分,变量可以由自定义标签函数返回,内容如下:
<div class="static-menu"> {% for item in permission_menu_list %} <a href="{{ item.url }}" class="{{ item.class }}">
<span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }} </a> {% endfor %} </div>
5)在模板页面base.html中显示菜单的位置先导入之前创建的my_tags.py文件,再调用自定义标签函数,如下:
<div class="menu-body"> {% load my_tags %} {% get_menu_styles request %} # 依次写函数名和参数,空格隔开 </div>
分析:当调用自定义标签函数get_menu_styles时,就会执行my_tags.py中相应函数并将返回值返回给函数对应装饰器中定义的menu.html页面,渲染成html页面后再将渲染结果返回到调用get_menu_styles的页面,这样做的好处是,当多个视图函数需要返回给浏览器同一个模板页面且都需要给这个模板页面的相同地方传递变量且该变量是由相同的业务逻辑产生,这个时候我们可以利用自定义标签函数,变量统一由自定义标签函数返回给一个页面(这个页面是将模板页面中共同使用这个变量的代码块提取出来所构成,如例中的menu.html),再在模板页面中调用该函数,避免了在视图函数中重复写相同的业务逻辑代码。这也是过滤器(包括django已有的和自定义的过滤器)和自定义标签的存在意义,即提高代码的复用性。
三、补充知识点
1、admin补充 - list_editable和search_fields
我们之前了解过Django提供的admin,其实admin的功能相当强大,我们目前了解的仅仅是九牛一毛,上篇中我们学习了如何在admin中自定义显示样式,今天我们再学习两个:list_editable和search_fields。还是以权限控制中的权限表为例:
上篇中我们为Permission表创建了三个字段(id、url、title),今天又加了两个(is_menu、icon),当我们在admin为permission表按如下定义时:
# 自定义类,类名自己定,但必须继承ModelAdmin class PermissionConfig(admin.ModelAdmin): list_display = ['pk', 'title', 'url', 'is_menu', 'icon'] list_editable = ['url', 'is_menu', 'icon'] search_fields = ['title'] ordering = ['pk'] # 按照主键从低到高
admin.site.register(Permission, PermissionConfig)
效果如图:
总结:
1)list_editable 字段在展示的同时可以编辑;
2)search_fields 显示按某字段搜索的功能;
2、自定义标签和过滤器
(1)首先保证settings.py中的INSTALLED_APPS配置了当前app,否则django无法找到自定义的标签和过滤器;
(2)在app中创建templatetags模块(也就是包,包的名字只能是templatetags);
(3)在templatetags中创建任意的.py文件(如:my_tags.py),然后就可以在里边写自定义的标签和过滤器了,如下:
from django.utils.safestring import mark_safe from django import template register = template.Library() # register的名字是固定的,不可改变 @register.filter # 自定义过滤器(filter) def filter_multi(v1, v2): return v1 * v2 @register.simple_tag # 自定义标签(simple_tag) def simple.tag_multi(v1, v2): return v1 * v2 @register.simple_tag # 自定义标签(simple_tag) def my_input(id, arg): result = "<input type='text' id='%s' class='%s' />" % (id, arg) return mark_safe(result)
(4)在使用自定义simple_tag和filter的html文件中先导入之前创建的my_tags.py,再使用,如下:
{% load my_tags %} # 导入 # num=12 {{ num|filter_multi:2 }} # 24 {{ num|filter_multi:"[22,333,4444]" }} {% simple_tag_multi 2 5 %} # 参数不限,但不能放在if、for语句中 {% simple_tag_multi num 5 %}
总结:
1)自定义过滤器的函数对参数有限制,只能是一个或者两个,当你想传超过两个的参数时,只能自己想办法,比如可以把参数放到列表中再传入,而自定义标签的函数参数可以有任意多个;
2)django在查找自定义标签和过滤器文件时,会依次查找INSTALLED_APPS中已配置的app下的templatetags模块中与load后同名的py文件,若两个app中的templatetags都有相同的py文件且文件中定义的同名的过滤器或者标签函数,那么后者会覆盖前者,因此尽量避免py文件同名。
3)自定义simple_tag不可以放在if、for语句中,而filter可以用在if等语句后,如下:
{% if num|filter_multi:30 > 100 %} {{ num|filter_multi:30 }} {% endif %}
3、Font Awesome - 一套绝佳的图标字体库和CSS框架
官网:http://fontawesome.dashgame.com/