一、權限組件
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/