1. 程式人生 > >Django 許可權管理-後臺根據使用者許可權動態生成選單

Django 許可權管理-後臺根據使用者許可權動態生成選單

Django許可權管理

實現目標:

1、管理使用者,新增角色,使用者關聯角色

2、新增許可權、角色關聯許可權

3、新增動作、許可權關聯動作

4、新增選單、許可權關聯選單

實現動態生成使用者許可權選單(可設定多級選單巢狀)、根據前臺URL自動選中選單並摺疊其餘選單

最終實現類似這樣的效果:

選單一   選單1.1   選單1.2     選單1.2.1       訂單管理       分類管理

選單二

一、首先是建立表格

models

from django.db import models


# Create your models here.
class User(models.Model):
    username 
= models.CharField(max_length=32) password = models.CharField(max_length=64) class Meta: verbose_name_plural = '使用者表' def __str__(self): return self.username class Role(models.Model): role = models.CharField(max_length=32) class Meta: verbose_name_plural
= '角色表' def __str__(self): return self.role class User2Role(models.Model): u = models.ForeignKey(User, on_delete=models.CASCADE) r = models.ForeignKey(Role, on_delete=models.CASCADE) class Meta: verbose_name_plural = '使用者分配角色' def __str__(self): return
'%s-%s' % (self.u.username, self.r.role) class Menu(models.Model): caption = models.CharField(max_length=32) parent = models.ForeignKey('self', related_name='p', null=True, blank=True, on_delete=models.CASCADE) def __str__(self): return '%s' % (self.caption,) class Permission(models.Model): caption = models.CharField(max_length=32) url = models.CharField(max_length=32) menu = models.ForeignKey(Menu, null=True, blank=True, on_delete=models.CASCADE) class Meta: verbose_name_plural = 'URL表' def __str__(self): return '%s-%s' % (self.caption, self.url) class Action(models.Model): caption = models.CharField(max_length=32) code = models.CharField(max_length=32) class Meta: verbose_name_plural = '操作表' def __str__(self): return self.caption class Permission2Action(models.Model): p = models.ForeignKey(Permission, on_delete=models.CASCADE) a = models.ForeignKey(Action, on_delete=models.CASCADE) class Meta: verbose_name_plural = '許可權表' def __str__(self): return '%s-%s:-%s?t=%s' % (self.p.caption, self.a.caption, self.p.url, self.a.code) class Permission2Action2Role(models.Model): p2a = models.ForeignKey(Permission2Action, on_delete=models.CASCADE) r = models.ForeignKey(Role, on_delete=models.CASCADE) class Meta: verbose_name_plural = '角色分配許可權' def __str__(self): return '%s=>%s' % (self.r.role, self.p2a)

建立表後,用django的admin在表中新增一些資料

1、使用者表:建立幾個使用者

2、角色表:建立幾個角色,如:CEO\CTO\開發\客服\業務員

3、給使用者分配角色

4、URL表:建立幾個管理選單,如:分類管理\報表管理\訂單管理\使用者管理

5、操作表:增\刪\改\查

6、許可權表:給URL新增操作內容

7、角色分配許可權:

8、選單表:設定三層選單,如:選單一:選單1.1:選單1.1.1

二、url

新增資料後建立url

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('logout/', views.logout),
    path('index/', views.index),
]

三、login

編輯login,登入成功後,將使用者資訊儲存在session中,並通過MenuHelper類獲取使用者的許可權和選單,也儲存在session中,例項化MenuHelper類

def login(request):
    if request.method == "GET":
        return render(request, 'login.html')
    else:
        username = request.POST.get('username')
        pwd = request.POST.get('pwd')
        obj = models.User.objects.filter(username=username, password=pwd).first()
        if obj:
            # 登入成功,獲取當前使用者資訊
            # 放到session中
            request.session['user_info'] = {'nid': obj.id, 'username': obj.username}

            # 獲取當前使用者的所有許可權,獲取所有選單,獲取在選單中顯示的許可權(葉子節點)
            # 放到session中
            MenuHelper(request, obj.username)
            return redirect('/index')
        else:
            return redirect('/login')

四、MenuHelper類

首先獲取當前使用者,通過reques.path_info獲取當前使用者訪問的url

呼叫類的session_data方法:判斷該使用者當前session中是否已經有內容,如果有內容則與取出session中的內容,否則通過使用者名稱分別獲取當前使用者的角色列表、許可權列表、最終顯示的選單列表以及所有選單,隨後跳轉至index

class MenuHelper(object):
    def __init__(self, request, username):
        # 當前請求的request物件
        self.request = request
        # 當前使用者名稱
        self.username = username
        # 當前url,如使用者訪問127.0.0.1:8000/index.html?p=123 會獲得:index.html
        self.current_url = request.path_info

        # 當前使用者的所有許可權
        self.permission2action_dict = None
        # 當前使用者選單中顯示的所有許可權(葉子節點)
        self.menu_leaf_list = None
        # 所有選單
        self.menu_list = None

        self.session_data()

    def session_data(self):
        permission_dict = self.request.session.get('permission_info')
        if permission_dict:
            self.permission2action_dict = permission_dict['permission2action_dict']
            self.menu_leaf_list = permission_dict['menu_leaf_list']
            self.menu_list = permission_dict['menu_list']
        else:
            # 獲取當前使用者的角色列表
            role_list = models.Role.objects.filter(user2role__u__username=self.username)

            # 獲取當前使用者的許可權列表(url+action)
            permission2action_list = models.Permission2Action.objects.\
                filter(permission2action2role__r__in=role_list).\
                values('p__url', 'a__code').distinct()

            permission2action_dict = {}
            for item in permission2action_list:
                if item['p__url'] in permission2action_dict:
                    permission2action_dict[item['p__url']].append(item['a__code'])
                else:
                    permission2action_dict[item['p__url']] = [item['a__code'], ]

            # 獲取選單的葉子節點,即:選單的最後一層應該顯示的許可權
            menu_leaf_list = list(models.Permission2Action.objects.
                                  filter(permission2action2role__r__in=role_list).
                                  exclude(p__menu__isnull=True).
                                  values('p_id', 'p__url', 'p__caption', 'p__menu').distinct())

            # 獲取所有選單列表
            menu_list = list(models.Menu.objects.values('id', 'caption', 'parent_id'))

            self.request.session['permission_info'] = {
                'permission2action_dict': permission2action_dict,
                'menu_leaf_list': menu_leaf_list,
                'menu_list': menu_list,
            }

            # self.permission2action_list = permission2action_list
            # self.menu_leaf_list = menu_leaf_list
            # self.menu_list = menu_list

    def menu_data_list(self):
        # 設定一個空的葉子節點字典
        menu_leaf_dict = {}
        # 首先設定葉子父id節點為空
        open_left_parent_id = None

        for item in self.menu_leaf_list:
            # 將獲取的葉子節點列表的每一個值轉換為字典形式,並重新設定key,新增child,status,open欄位
            item = {
                'id': item['p_id'],
                'url': item['p__url'],
                'caption': item['p__caption'],
                'parent_id': item['p__menu'],
                'child': [],
                'status': True,  # 是否顯示
                'open': False,  # 是否開啟
            }
            # 判斷每一個葉子節點的父節點,將每個葉子節點的內容新增至父節點id作為的key中
            # 判斷父節點id作為的key是否在葉子節點字典中存在,如果存在,則將item值append進入
            if item['parent_id'] in menu_leaf_dict:
                menu_leaf_dict[item['parent_id']].append(item)
            # 如果不存在,則直接在列表中生成一個key是葉子節點父節點id的,值為item的資料
            else:
                menu_leaf_dict[item['parent_id']] = [item, ]

            # 判斷使用者輸入的url是否與現在的url匹配,item['url']可以寫成一個正則表示式,用match進行匹配
            # 如果匹配上,將葉子節點的open置為true,並將葉子節點的父節點id進行賦值
            import re
            if re.match(item['url'], self.current_url):
                item['open'] = True
                open_left_parent_id = item['parent_id']

        # 設定一個選單空字典
        menu_dict = {}

        # 將選單列表轉換為字典,並增加child,status,open欄位
        # 將列表中的id作為key,列表中的值作為值
        for item in self.menu_list:
            item['child'] = []
            item['status'] = False
            item['open'] = False
            menu_dict[item['id']] = item

        # 迴圈葉子字典,設定選單字典中對應的child內容為葉子字典的值
        for k, v in menu_leaf_dict.items():
            menu_dict[k]['child'] = v
            # 設定選單字典的parent_id的值為葉子字典的key(也就是葉子中的parent)
            parent_id = k
            # 設定選單字典中的status狀態為True,並迴圈設定父級選單的status為True
            while parent_id:
                menu_dict[parent_id]['status'] = True
                parent_id = menu_dict[parent_id]['parent_id']

        # 判斷葉子父級id,將open設定為True,並迴圈設定父級選單的open為True
        while open_left_parent_id:
            menu_dict[open_left_parent_id]['open'] = True
            open_left_parent_id = menu_dict[open_left_parent_id]['parent_id']

        # print('迴圈許可權使用者url字典,將使用者許可權取得的id匹配選單列表id並設定["child"]值為使用者許可權內容')
        # print('設定parent_id變數為:使用者許可權url的id')
        # print('如果有,選單id的["status"]設定為True')
        # print('並且將parent_id的值設定為:選單字典中選單id的["parent"],等待下一次迴圈')
        # for k, v in menu_dict.items():
        #     print(k, v)
        # #####################處理選單的等級關係#########################
        # menu_dict 應用:多級評論,多級選單

        result = []
        # 按父子關係,將選單列表中的值,層疊放入一個result中
        # 這裡需要注意的是,只需要尋找一層的父id,並將自己放入,無需一層一層尋找到上一層的父節點。
        for row in menu_dict.values():
            if not row['parent_id']:
                result.append(row)
            else:
                menu_dict[row['parent_id']]['child'].append(row)

        return result

    def menu_content(self, child_list):

        response = ''
        tpl = """
            <div class="item %s">
                <div class="title">%s</div>
                <div class="content">%s</div>
            </div>
        """
        for row in child_list:
            if not row['status']:
                continue
            active = ''
            if row['open']:
                active = 'active'
            if 'url' in row:
                response += '<a class="%s" href="%s">%s</a>' % (active, row['url'], row['caption'])
            else:
                title = row['caption']
                content = self.menu_content(row['child'])
                response += tpl % (active, title, content)

        return response

    def menu_tree(self):
        response = ''
        tpl = """
            <div class="item %s">
                <div class="title">%s</div>
                <div class="content">%s</div>
            </div>
        """
        for row in self.menu_data_list():
            if not row['status']:
                continue
            active = ''
            if row['open']:
                active = 'active'
            title = row['caption']
            content = self.menu_content(row['child'])
            response += tpl % (active, title, content)

        return response

    def action(self):
        """
        檢查當前使用者是否對當前URL有訪問權,並獲取對當前URL有什麼許可權
        :return:
        """

        action_list = []

        for k, v in self.permission2action_dict.items():
            if re.match(k, self.current_url):
                action_list = v
                break

        return action_list

五、index

index使用了一個裝飾器,判斷使用者session中是否有使用者資訊,如果有使用者資訊,使用MenuHelper類例項化一個物件,呼叫物件的action方法,獲得action_list

如果列表為空則返回無權訪問

否則返回選單樹(呼叫了類的menu_data_list方法,迴圈遞迴的生成選單樹,返回的是後臺生成的html程式碼

以及許可權列表

最後通過許可權列表中的內容分別進行操作,並返回至前臺(這個位置沒有編寫完成,僅僅寫了一個舉例)

def permission(func):
    def inner(request, *args, **kwargs):
        user_info = request.session.get('user_info')
        if not user_info:
            return redirect('/login.html')
        obj = MenuHelper(request, user_info['username'])
        action_list = obj.action()
        if not action_list:
            return HttpResponse('無許可權訪問')
        kwargs['menu_string'] = obj.menu_tree()
        kwargs['action_list'] = action_list
        return func(request, *args, **kwargs)
    return inner


@permission
def index(request, *args, **kwargs):
    actions_list = kwargs.get('actions_list')
    menu_string = kwargs.get('menu_string')
    if "GET" in actions_list:
        result = models.User.objects.all()
    else:
        result = []
    return render(request, 'index.html', {
        'menu_string': menu_string,
        'actions_list': actions_list,
        'result': result,
    })

六、login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/login/" method="POST">
        {% csrf_token %}
        <input type="text" name="username" />
        <input type="text" name="pwd" />
        <input type="submit" value="提交" />
    </form>
</body>
</html>

七、index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .content{
            margin-left: 20px;
            display: none;
        }
        .content a{
            display: block;
        }
        .active > .content{
            display: block;
        }
    </style>
</head>
<body>
    <div style="float: left;width: 20%;">
        {{ menu_string|safe }}
    </div>
    <div style="float: left;width: 80%;">
    </div>

</body>
</html>