Django - 權限(2)- 動態顯示單級權限菜單

1、權限組件

一、上篇隨筆中,咱們只是設計好了權限控制的表結構,有三個模型,五張表,兩個多對多關係,而且簡單實現了對用戶的權限控制,咱們會發現那樣寫有一個問題,就是權限控制寫死在了項目中,而且沒有實現與咱們的業務邏輯解耦,當其餘項目要使用權限控制時,要再重複寫一遍權限控制的代碼,所以咱們頗有必要將權限控制的功能開發成一個組件(可插拔)。html

  組件其實就是一個包,將一個與功能相關的代碼關聯到一塊兒,當其餘項目要使用該功能時將組件導入便可,下面咱們試着來將權限控制寫成一個組件,以客戶管理系統爲例,利用權限控制組件,實現動態顯示權限菜單的功能,目錄結構以下: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
    ├── 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作路由分發):sql

    客戶管理:django

      客戶列表:/customer/list/瀏覽器

      添加客戶:/customer/add/session

      刪除客戶:/customer/del/(\d+)/app

      修改客戶:/customer/edit/(\d+)/框架

    帳單管理:函數

      帳單列表:/payment/list/學習

      添加帳單:/payment/add/

      刪除帳單:/payment/del/(\d+)/

      修改帳單:/payment/edit/(\d+)/

  (4)在權限組件表中錄入相關信息:

    錄入權限列表,建立角色(併爲角色分配權限),建立用戶(併爲用戶分配角色);

    這樣用戶登陸時,就能夠根據當前登陸用戶找到其全部權限再將權限信息放入session,之後每次訪問時候須要先去session檢查是否有權訪問。

    上篇中咱們已經完成這些,接下來將權限和項目解耦而且完成動態顯示權限菜單功能。

2、動態顯示權限菜單(單級菜單)

  準備工做已經就緒,再按照如下步驟進行:

一、登陸頁面

  在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;

二、

  在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

三、

客戶列表和帳單列表屬於菜單列表,咱們須要渲染到左側菜單中,根據用戶權限判斷是否顯示,而客戶列表和帳單列表的左側菜單是在公共的模板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)再傳給返回的頁面,這種方法是能夠知足需求,但同時也存在代碼重複的問題,就是在不一樣的視圖函數中重複寫了同樣的邏輯代碼。下面介紹的自定義標籤的擴展就能夠完美解決。

四、自定義標籤擴展功能實現點擊菜單增長相應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已有的和自定義的過濾器)和自定義標籤的存在乎義,即提升代碼的複用性。

3、補充知識點

一、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   顯示按某字段搜索的功能;

二、自定義標籤和過濾器

  (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 %}

三、Font Awesome - 一套絕佳的圖標字體庫和CSS框架

官網:http://fontawesome.dashgame.com/

相關文章
相關標籤/搜索